Skip to content

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:

  1. Production infrastructure exists and is reachable from the server host.
  2. Every required production secret is present in Infisical under the production environment and /server path.
  3. Payment, email, SMS, OAuth, notification, moderation, video, chat, and storage providers have production credentials and approved webhook/redirect URLs.
  4. The OpenSearch endpoint has been verified as Learnille infrastructure. Do not launch against an endpoint whose ownership is uncertain.
  5. The production domain, short-link domain, email DNS records, OAuth consent screen, and payment webhooks are configured.
  6. 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.

ServiceAccount actionServer configurationStatus / acceptance check
PaystackActivate the business account and obtain live keys. Configure the production webhook URL.PAYSTACK_SECRET_KEY, PAYSTACK_PUBLIC_KEY, PAYSTACK_WEBHOOK_SECRET, PAYSTACK_WEBHOOK_URLRequired for Paystack payments and withdrawals.
FlutterwaveActivate 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_SECRETRequired because these values are mandatory at startup.
Brevo or approved SMTP providerCreate production SMTP credentials. Add SPF and DKIM records through the DNS owner.SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASSWORDSend a verification email and password-reset email from production.
NovuCreate 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 commandTrigger a production notification and verify delivery.
InstantDB, self-hostedProvision 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_TOKENRun 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.
GetStreamCreate the production app and obtain the API key and secret.GET_STREAM_API_KEY, GET_STREAM_SECRET_KEYCreate a production chat channel or token through the server and verify it from each client surface.
Bunny StreamCreate 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_SECRETConfirm the prepaid balance is active, then upload a small test video and verify playback and webhook processing.
OneSignalConfigure 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 configurationSend 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 OAuthComplete 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_SECRETComplete a sign-in and callback flow on the production domain.
OpenExchangeRatesObtain a production app ID.OPENEXCHANGERATES_APP_ID; optional OPENEXCHANGERATES_API_URLFetch a current exchange-rate response.
CurrencyAPIObtain a production API key.CURRENCYAPI_API_KEY; optional CURRENCYAPI_API_URLFetch a current exchange-rate response.
OpenRouterObtain an API key and agree on a monthly moderation budget and alert threshold.OPENROUTER_API_KEY; optional OPENROUTER_BASE_URLRun a moderation request and confirm budget alerts.
PerspectiveObtain an API key and agree on a monthly moderation budget and alert threshold.PERSPECTIVE_API_KEYRun a moderation request and confirm failure handling when the provider is unavailable.
SMS: Termii preferred; Prelude fallbackConfigure 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.
NeonCreate 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_CERTThe 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.
RedisChoose 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 variablesVerify cache, throttling, and BullMQ queue connections independently.
Cloudflare R2Create 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_PROTOCOLUpload, download, multipart upload, and private signed download tests must pass. R2 replaces the local MinIO deployment for production.
SoketiRun 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_PORTVerify websocket connection, authentication, and reconnect behavior.
ShlinkProvision the service and a dedicated short-link domain.SHLINK_API_KEY, SHLINK_BASE_URLCreate and resolve a production short link.
OpenSearchVerify that the existing host belongs to Learnille, then confirm credentials, index naming, and network access.OPENSEARCH_HOST, OPENSEARCH_USERNAME, OPENSEARCH_PASSWORD, optional OPENSEARCH_INDEX_PREFIXHealth 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:

AreaRequired variables
ApplicationPORT, BACKEND_URL, FRONTEND_URL, COMPANY_NAME, LOGO_URL, DOMAIN, SYSTEM_EMAIL
DatabaseDB_CONN_STRING, DB_HOST, DB_USER, DB_NAME, DB_PASSWORD, DB_PORT, DB_CA_CERT where required by the final TLS configuration
Security and cryptoJWT_SECRET_KEY, SESSION_SECRET, KEYS_DIR, COMPANION_SECRET, COMPANION_PRESIGNER_SECRET, BEACON_SECRET, BULLMQ_ADMIN_PASSWORD
Queues and throttlingREDIS_HOST, REDIS_PORT, THROTTLE_TTL, THROTTLE_LIMIT
UploadsCOMPANION_UPLOAD_URLS, SIMPLE_STORAGE_URL, SIMPLE_STORAGE_ACCESS_KEY, SIMPLE_STORAGE_SECRET_KEY, and the R2 bucket variables
RuntimeNODE_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_URL and CORS_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:

  1. Dokploy application, VPS networking, reverse proxy, TLS, health check, and log retention.
  2. Neon database, Redis, Cloudflare R2 buckets, self-hosted InstantDB, Soketi, Shlink, and OpenSearch connectivity.
  3. Provider accounts, DNS records, OAuth configuration, webhook endpoints, and production credentials.
  4. Infisical production variables and a controlled secret-access policy.
  5. 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.
  6. Application build and production artifact verification.
  7. Database migrations, database seed data, InstantDB schema and permissions, and Novu workflow synchronization.
  8. Process start.
  9. OpenSearch index creation and content reindexing.
  10. 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.

Terminal window
pnpm run migrations:run

The 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.

Terminal window
pnpm run seed:prod
pnpm run sync:novu

For 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

  1. Roll back the Dokploy deployment or route traffic to the previous healthy container.
  2. Keep the database intact unless the release includes an approved rollback migration.
  3. Re-deploy the previous known-good image through Dokploy.
  4. Confirm health, queue processing, authentication, payments, and uploads.
  5. 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.