Learnille server production deployment guide
Learnille server production deployment guide
This guide covers the production deployment of the server package through Dokploy. The server is an independent repository and receives its runtime configuration from Dokploy’s environment injection or Infisical; it does not load .env files in production.
The deployment is blocked until the account, infrastructure, and DNS owners have completed the items marked Required. Never commit credentials to the repository or paste them into tickets or chat. Store them in the production Infisical project at /server.
Deployment gates
Complete these gates before deploying the application:
- Production infrastructure exists and is reachable from the server host.
- Every required production secret is present in Infisical under the production environment and
/serverpath. - Payment, email, SMS, OAuth, notification, moderation, video, chat, and storage providers have production credentials and approved webhook/redirect URLs.
- The OpenSearch endpoint has been verified as Learnille infrastructure. Do not launch against an endpoint whose ownership is uncertain.
- The production domain, short-link domain, email DNS records, OAuth consent screen, and payment webhooks are configured.
- A database backup and rollback target have been confirmed.
Required accounts and infrastructure
The following table is the production hand-off list. Account action means an account or resource must be created or activated before a secret can be supplied. Server configuration lists the variable names currently validated by server/src/config.ts.
| Service | Account action | Server configuration | Status / acceptance check |
|---|---|---|---|
| Paystack | Activate the business account and obtain live keys. Configure the production webhook URL. | PAYSTACK_SECRET_KEY, PAYSTACK_PUBLIC_KEY, PAYSTACK_WEBHOOK_SECRET, PAYSTACK_WEBHOOK_URL | Required for Paystack payments and withdrawals. |
| Flutterwave | Activate the business account and obtain live credentials. Configure production webhooks and payout permissions. | FLUTTERWAVE_PUBLIC_KEY, FLUTTERWAVE_SECRET_KEY, FLUTTERWAVE_WEBHOOK_SECRET, FLUTTERWAVE_CLIENT_ID, FLUTTERWAVE_CLIENT_SECRET | Required because these values are mandatory at startup. |
| Brevo or approved SMTP provider | Create production SMTP credentials. Add SPF and DKIM records through the DNS owner. | SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASSWORD | Send a verification email and password-reset email from production. |
| Novu | Create or select the production project and obtain its API/secret key. Publish the required workflows and confirm the bridge URL. | NOVU_SECRET_KEY; NOVU_PATH is used by the Novu sync command | Trigger a production notification and verify delivery. |
| InstantDB, self-hosted | Provision the production self-hosted InstantDB service, persistent storage, app, admin token, schema, and permissions. Confirm the server and CLI can reach the self-hosted endpoint. | INSTANT_APP_ID, INSTANT_APP_ADMIN_TOKEN | Run schema and permission pushes against the production app, then verify room creation. The current CLI scripts must be checked for the self-hosted endpoint before execution. |
| GetStream | Create the production app and obtain the API key and secret. | GET_STREAM_API_KEY, GET_STREAM_SECRET_KEY | Create a production chat channel or token through the server and verify it from each client surface. |
| Bunny Stream | Create the production video library, add prepaid credit to the account, and obtain the library ID and API key. Configure the upload/webhook settings. The free trial is too short to treat as production funding. | BUNNY_STREAM_API_KEY, BUNNY_STREAM_LIBRARY_ID, optional BUNNY_STREAM_WEBHOOK_SECRET | Confirm the prepaid balance is active, then upload a small test video and verify playback and webhook processing. |
| OneSignal | Configure one production app per surface: student web, student mobile, instructor, consultant, and admin. Record app IDs and server API keys. | ONESIGNAL_PROVIDER_ID; surface-specific application credentials must also be confirmed in the notification configuration | Send a test notification to each surface. The current server validation exposes only the provider ID, so the surface credential mapping needs verification before launch. |
| Google OAuth | Complete the consent screen with app name, logo, and privacy-policy URL. Create the production OAuth client and configure authorized origins and callback URLs. | GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET | Complete a sign-in and callback flow on the production domain. |
| OpenExchangeRates | Obtain a production app ID. | OPENEXCHANGERATES_APP_ID; optional OPENEXCHANGERATES_API_URL | Fetch a current exchange-rate response. |
| CurrencyAPI | Obtain a production API key. | CURRENCYAPI_API_KEY; optional CURRENCYAPI_API_URL | Fetch a current exchange-rate response. |
| OpenRouter | Obtain an API key and agree on a monthly moderation budget and alert threshold. | OPENROUTER_API_KEY; optional OPENROUTER_BASE_URL | Run a moderation request and confirm budget alerts. |
| Perspective | Obtain an API key and agree on a monthly moderation budget and alert threshold. | PERSPECTIVE_API_KEY | Run a moderation request and confirm failure handling when the provider is unavailable. |
| SMS: Termii preferred; Prelude fallback | Configure Termii as the production provider and obtain production credentials. Keep Prelude available only as a fallback if Termii is unavailable or rejected. | Termii: OTP_SMS_PROVIDER=termii, TERMII_API_KEY, TERMII_BASE_URL, TERMII_SENDER_ID, TERMII_CHANNEL, plus the configured PIN values. Fallback: PRELUDE_API_KEY with OTP_SMS_PROVIDER=prelude. | Send and verify a production OTP through Termii before launch. |
| Neon | Create the production database and provide the connection string. Confirm the database CA handling. | DB_CONN_STRING, DB_HOST, DB_USER, DB_NAME, DB_PASSWORD, DB_PORT, DB_CA_CERT | The current code accepts DB_CA_CERT; Neon does not provide a CA file in the expected form. Resolve or patch this before launch and verify TLS connectivity. |
| Redis | Choose managed Redis or a private Redis service on the VPS. Keep it private and enable authentication. | REDIS_HOST, REDIS_PORT, optional REDIS_USER, REDIS_PASSWORD, and optional cache/queue-specific variables | Verify cache, throttling, and BullMQ queue connections independently. |
| Cloudflare R2 | Create two S3-compatible buckets, one public and one private. Create an access key limited to those buckets and configure CORS for signed browser uploads. | SIMPLE_STORAGE_URL, SIMPLE_STORAGE_ACCESS_KEY, SIMPLE_STORAGE_SECRET_KEY, SIMPLE_STORAGE_REGION, SIMPLE_STORAGE_BUCKET, SIMPLE_STORAGE_PUBLIC_BUCKET, SIMPLE_STORAGE_PRIVATE_BUCKET, PRIVATE_STORAGE_BUCKET, SIMPLE_STORAGE_PROTOCOL | Upload, download, multipart upload, and private signed download tests must pass. R2 replaces the local MinIO deployment for production. |
| Soketi | Run the websocket server on the VPS, restrict it to the private network or reverse proxy, and configure TLS at the edge. | SOKETI_APP_ID, SOKETI_APP_KEY, SOKETI_SECRET_KEY, SOKETI_HOST, SOKETI_PORT | Verify websocket connection, authentication, and reconnect behavior. |
| Shlink | Provision the service and a dedicated short-link domain. | SHLINK_API_KEY, SHLINK_BASE_URL | Create and resolve a production short link. |
| OpenSearch | Verify that the existing host belongs to Learnille, then confirm credentials, index naming, and network access. | OPENSEARCH_HOST, OPENSEARCH_USERNAME, OPENSEARCH_PASSWORD, optional OPENSEARCH_INDEX_PREFIX | Health check, index creation, and a product reindex must succeed. |
Production configuration
Infisical
Create or verify the production environment in the Infisical project. Store the server secrets at /server. The package scripts default to INFISICAL_ENV=prod and INFISICAL_PATH=/server; set these explicitly in deployment automation so a staging secret set cannot be selected accidentally.
The server validates configuration during startup. In addition to provider values above, production must supply the application, security, database, queue, and upload values below:
| Area | Required variables |
|---|---|
| Application | PORT, BACKEND_URL, FRONTEND_URL, COMPANY_NAME, LOGO_URL, DOMAIN, SYSTEM_EMAIL |
| Database | DB_CONN_STRING, DB_HOST, DB_USER, DB_NAME, DB_PASSWORD, DB_PORT, DB_CA_CERT where required by the final TLS configuration |
| Security and crypto | JWT_SECRET_KEY, SESSION_SECRET, KEYS_DIR, COMPANION_SECRET, COMPANION_PRESIGNER_SECRET, BEACON_SECRET, BULLMQ_ADMIN_PASSWORD |
| Queues and throttling | REDIS_HOST, REDIS_PORT, THROTTLE_TTL, THROTTLE_LIMIT |
| Uploads | COMPANION_UPLOAD_URLS, SIMPLE_STORAGE_URL, SIMPLE_STORAGE_ACCESS_KEY, SIMPLE_STORAGE_SECRET_KEY, and the R2 bucket variables |
| Runtime | NODE_ENV=production, CORS_ORIGIN, SITE_URL, PUBLIC_URL where used |
Optional values should be set only when the corresponding feature is enabled. Do not add a provider secret merely because it appears in a local environment.
The startup key verifier also requires KEY_PRIV and KEY_PUB in every non-docs environment. In production it additionally requires both CA_PEM and HTTP_CA_CRT, and checks that the PEM values have the expected format. These are not currently represented in the Zod configuration table, but a missing value stops application startup.
Domain and DNS checklist
- Point the API hostname to the production edge or reverse proxy.
- Configure the frontend origin in
FRONTEND_URLandCORS_ORIGIN. - Configure the short-link hostname in Shlink and
SHLINK_BASE_URL. - Add the SMTP provider’s SPF and DKIM records. Add DMARC according to the email policy.
- Register the exact Google OAuth origins and callback URLs.
- Register Paystack and Flutterwave webhook URLs over HTTPS.
- Configure Bunny Stream, OneSignal, Soketi, and R2 allowed origins as applicable.
- Confirm certificates renew automatically.
Provisioning order
Provision dependencies in this order so the API is not started against partial state:
- Dokploy application, VPS networking, reverse proxy, TLS, health check, and log retention.
- Neon database, Redis, Cloudflare R2 buckets, self-hosted InstantDB, Soketi, Shlink, and OpenSearch connectivity.
- Provider accounts, DNS records, OAuth configuration, webhook endpoints, and production credentials.
- Infisical production variables and a controlled secret-access policy.
- Confirm the database schema strategy. The repository currently contains TypeORM migration files, including an initial schema migration; an empty production database must not be created through synchronization.
- Application build and production artifact verification.
- Database migrations, database seed data, InstantDB schema and permissions, and Novu workflow synchronization.
- Process start.
- OpenSearch index creation and content reindexing.
- Smoke tests and monitoring checks.
Deploy the server with Dokploy
Configure a Dokploy application from the independent server repository and use the repository’s Dockerfile. The Dockerfile builds the application with pnpm run build:raw and starts it with pnpm run start:prod by default. The container exposes port 3000; set the Dokploy service port to the value used by the deployed application and route the production API hostname to it.
Inject the production variables through Dokploy. If the container is expected to fetch secrets from Infisical at runtime instead, configure the required Infisical authentication variables in Dokploy and keep INFISICAL_ENV=prod and INFISICAL_PATH=/server explicit. Use one secret-injection model consistently; do not inject partial values through Dokploy while expecting the container to complete the set from Infisical.
Run database and initialization commands as Dokploy pre-deploy/release commands or as controlled one-off jobs using the same image and production environment. The commands below do not print or copy secret values.
pnpm run migrations:runThe Docker image build already installs dependencies and generates Swagger metadata through build:raw. Do not run a second application build as part of the release command.
If the production database is empty, stop here unless the complete schema baseline has been reviewed and is represented by migrations. Do not use TypeORM synchronization to create or repair the production schema.
The current src/seeder.ts passes synchronize: true to DatabaseModule.register(). Because that explicit option is spread over the module’s default, pnpm run seed:prod is not safe to run against production until the seeder is changed to use synchronize: false and the change is reviewed. This is a code-level deployment blocker, not a documentation choice.
After migrations are successful, seed only idempotent reference data with the corrected production-safe seeder, then push external schemas and workflows. If the Dokploy environment only injects variables directly, invoke the underlying commands without an extra infisical run wrapper. If the image is configured to authenticate to Infisical, use the package scripts that wrap Infisical instead.
pnpm run seed:prodpnpm run sync:novuFor sync:novu, verify that NOVU_PATH is the production bridge URL and that the command receives the production NOVU_SECRET_KEY from Infisical. Do not run a development or staging bridge against the production project.
The current instant:schema:prod and instant:perms:prod package scripts load env/.env.production with dotenv; they do not load Infisical themselves. They must either be changed to consume the Infisical-injected environment or replaced with an equivalent production command before launch. This is especially important for self-hosted InstantDB because the CLI target must be explicit and must not silently default to a hosted service. Do not create a production .env file as a workaround.
After that script/tooling correction, run both pnpm run instant:schema:prod and pnpm run instant:perms:prod against the self-hosted production app, then verify that the server can create and read a room before starting public traffic.
Dokploy’s runtime command should be pnpm run start:prod, or the equivalent node -r ./path-alias-register.js dist/main.js with NODE_ENV=production and the complete production environment already injected.
Do not use TypeORM synchronization in production. The database module disables synchronization for production; schema changes must be applied through reviewed migrations.
Start the Dokploy service
Start the process only after the database migration, corrected seed, InstantDB schema and permissions, and Novu workflow synchronization have completed:
Dokploy starts the container with the Dockerfile’s default pnpm run start:prod command after the release commands succeed. Do not use PM2 in production; Dokploy owns the container lifecycle, restarts, deployment history, and logs.
The server bootstrap creates the local ./uploads directory automatically, validates configuration, initializes the database, Redis, queues, mailer, Novu, OpenSearch, and InstantDB clients, writes openapi.json, and listens on PORT. Those clients still require their remote services and credentials to be reachable before startup.
Initialization that still requires the running server
OpenSearch has no production bootstrap script in the current server/package.json. After the server is healthy, create or verify the Learnille indexes and run the approved product/content reindex operation through the server’s OpenSearch administration flow or a separately documented worker command. Do not invent a shell command for this step until the production index owner and index prefix are confirmed.
The server also needs post-start verification for health indicators, queue processors, payment webhooks, notification delivery, storage, and websocket connectivity. These are runtime checks, not schema initialization.
Verification checklist
Check each item against production after deployment:
- API health endpoint reports the database, Redis, and OpenSearch dependencies as healthy.
- Swagger is reachable only at the intended API domain and is protected according to the production access policy.
- A new user can complete Google OAuth or password registration and receive an email or SMS OTP.
- Paystack and Flutterwave test transactions reach the correct production webhook handlers.
- A verification email, password-reset email, Novu notification, and OneSignal notification are delivered.
- R2 public upload, private upload, signed download, and multipart upload work.
- Bunny Stream upload and playback work.
- Bunny Stream has an active prepaid credit balance; do not rely on the short free trial for production traffic.
- GetStream chat and self-hosted InstantDB room creation work.
- Currency conversion succeeds through the selected provider and fails safely when that provider is unavailable.
- Moderation requests work, provider failures are observable, and OpenRouter/Perspective spend alerts are active.
- Soketi websocket connections work through the production edge.
- Shlink creates and resolves a short link.
- OpenSearch indexing and search work against the verified Learnille host.
- Logs, error tracking, queue failures, database backups, and uptime alerts are visible to the on-call team.
Rollback
- Roll back the Dokploy deployment or route traffic to the previous healthy container.
- Keep the database intact unless the release includes an approved rollback migration.
- Re-deploy the previous known-good image through Dokploy.
- Confirm health, queue processing, authentication, payments, and uploads.
- Record the failed release, affected migrations, provider errors, and the follow-up action.
Never roll back by deleting production data or by running an unreviewed destructive migration.
Unique infrastructure and service names
Paystack, Flutterwave, Brevo, Novu, self-hosted InstantDB, GetStream, Bunny Stream, OneSignal, Google OAuth, OpenExchangeRates, CurrencyAPI, OpenRouter, Perspective, Prelude, Termii, Neon, Redis, Cloudflare R2, Soketi, Shlink, OpenSearch, and the production VPS.