Skip to content

Deployment & Production Readiness

Guide 38

Deploying a Ndigo app is mostly assembly: build the packages and app, provide configuration and secrets, choose a process model, expose the health surface, and make shutdown orderly. The framework deliberately keeps the runtime thin — no hidden daemon, no required orchestrator — so the checklist is explicit.

This guide collects the production decisions that are otherwise spread across the clustering, management, telemetry, TLS, and static-file guides.

Audience: Node application developers and operators · Prerequisites: Guides 01, 24, 33, and 37 · Reference app: apps/05-ndigo-api, apps/16-ndigo-redis-fanout

  • Build and start an app with the CLI or its own scripts.
  • Provide environment, profiles, and secrets without baking them in.
  • Choose single-process vs clustered deployment and know what changes.
  • Shut down without dropping in-flight work.
  • Expose health, metrics, logs, and traces to the platform.
  • Terminate TLS at the edge and verify the proxy/static posture.

The standard lifecycle is build, then serve:

Terminal window
pnpm install --frozen-lockfile
pnpm build # packages and app build targets
ndigo serve ./[apps/05-ndigo-api](https://github.com/ndigo/ndigo/tree/main/apps/05-ndigo-api) --port 3000

ndigo serve runs the app’s start script under the supervisor (guide 37); ndigo dev is the watch mode. A plain node/tsx start command is equally valid when you do not need the supervisor. Build artifacts belong in the image or release bundle; do not ship node_modules from a dev install.

The app’s package.json start script should bind the port from the environment (PORT) and import the built entry, not a TypeScript source file. Set NODE_ENV=production and a real NODE_OPTIONS memory limit per slot.

Configuration is resolved from the environment chain (guide 03): CLI args, builder properties, process.env, config files, configServer, defaults. Production rules:

  • Profiles — activate prod explicitly (NDIGO_PROFILES_ACTIVE=prod or --profiles.active prod); do not rely on a default profile for production values.
  • One source of truth per key — a config file for stable defaults, the environment for deploy-specific values, a config server for fleet-wide live values.
  • Secrets — use {cipher:...} values and deliver NDIGO_SECRET_KEY from the platform’s secret store, never the repo, image, or logs. A missing key fails at lookup, not silently.
  • Validation — declare @environment schemas so a missing/malformed key fails at boot, before the port binds.
  • Live refresh — @refreshScope rebuilds configuration-derived clients after config:refresh; verify the remote source’s ETag/poll behavior before relying on it.

Start with one process per container/pod. For more cores or a single-host deploy, the cluster supervisor replicates worker slots:

Terminal window
ndigo serve ./[apps/05-ndigo-api](https://github.com/ndigo/ndigo/tree/main/apps/05-ndigo-api) --port 3000 --cluster 4

Each slot gets NDIGO_CLUSTER_SLOT, NDIGO_CLUSTER_SIZE, and PORT = base + slot (guide 25). The supervisor relays SIGINT/SIGTERM into each child’s own graceful shutdown and restarts crashed slots with bounded backoff; a crash loop fails closed instead of flapping forever (guide 24).

What changes under clustering:

  • In-process state is per slot. Caches, rate-limit counters, and schedulers multiply unless you use the shared drivers (Guides 19, 22, 23, 28).
  • Scheduled work needs single-executor arbitration — @scheduled({ lock }) or a cluster-wide leader; otherwise every slot runs the task.
  • WebSocket fan-out needs the Redis pub/sub bridge when clients may land on different slots (guide 15).
  • Health probes must target a single slot (the supervisor’s port mapping) or the platform’s load balancer, not an arbitrary child.

The canonical order is stop serving, drain producers, close consumers/ transports, then close the context (guide 04 and Lifecycle & Ownership):

  1. Stop accepting new requests (server stop).
  2. registry.close / broker close — unsubscribe, stop heartbeat, drop sessions.
  3. fanout.close / publisher close by its owner.
  4. context.close — lifecycle stop in descending phase order, then bean destroy/close.

runNodeApplication and ndigo serve install the SIGINT/SIGTERM handler; a second signal, a throwing close, or a teardown past timeoutMs (default 10s) force-exits non-zero. Set the platform’s termination grace period above your drain time. A clustered supervisor waits for children and reaps stragglers.

Expose the management surface deliberately (guide 33):

  • GET /health — liveness/readiness groups; use a component check for the database/Redis when the app cannot serve without it.
  • GET /metrics — the meter snapshot (Prometheus text if configured); scrape it from the platform, not the public internet.
  • GET /loggers, /env, /configprops — opt-in operational reads; guard them or keep them off the public listener.
  • Probes — point readiness at /health/readiness (or the group that includes dependencies) and liveness at a cheap /health/liveness.
  • Telemetry — export traces/metrics/logs (guide 32); correlate request, job, and scheduled-task spans; flush on shutdown.

Do not expose the management surface on the same public listener without a guard. If the platform supports a separate management port, prefer it.

Terminate TLS at the edge (load balancer, reverse proxy, or the Node server itself). For the Node listener, provide the key and certificate together:

Terminal window
NDIGO_TLS_KEY=/run/secrets/tls.key
NDIGO_TLS_CERT=/run/secrets/tls.crt
NDIGO_TLS_CA=/run/secrets/ca.crt # optional client/chain CA

The server starts an HTTP/2 secure listener with allowHTTP1 fallback; HTTP/3 stays at the edge . A half-configured TLS pair fails at startup rather than silently serving plaintext.

Behind a proxy, configure trustedProxies (guide 28) so rate limiting, sessions, and remoteAddress see the real client. Forward X-Forwarded-For and X-Forwarded-Proto; keep the proxy’s hop count minimal.

  • Serve precompressed twins (.br/.gz) with Vary: Accept-Encoding; the static handler negotiates them (guide 10).
  • Give hashed assets a long Cache-Control and HTML a short/no-cache policy.
  • Use a CDN in front of static assets when the app is multi-region; the framework’s ETag/Range semantics survive a caching proxy.
  • Do not serve user-uploaded files from the app’s source tree; use a dedicated root or object storage.
  • SPA history fallback belongs at the edge or the static controller, not in a catch-all controller that shadows 404s.
  • pnpm install --frozen-lockfile && pnpm build in the release lane.
  • NODE_ENV=production, PORT, memory limit, and log destination set.
  • NDIGO_PROFILES_ACTIVE=prod, config validated at boot, secrets from the platform.
  • Process model chosen; shared cache/locks/jobs/fan-out wired if clustered.
  • Graceful shutdown timeout < platform termination grace period.
  • /health readiness includes dependencies; probes configured.
  • /metrics, /loggers, /env, /configprops guarded or not exposed.
  • TLS at the edge or NDIGO_TLS_* on the listener; trusted proxies set.
  • Static assets cached/precompressed; uploads on a durable volume.
  • Rate limits, CORS, CSRF, and security headers verified in the deployed origin (guide 28).
  • A rollback path exists (previous image + migration compatibility).
  • A dev server in production. tsx src/index.ts is fine for a demo, not a release; build and run the output.
  • Secrets in the image. {cipher:...} values are safe; the key is not.
  • No readiness dependency check. A pod that accepts traffic before the database is ready returns 500s instead of waiting.
  • Cluster without shared state. In-memory caches/locks/rate limits multiply per slot.
  • Killing before draining. A short termination grace period truncates in-flight requests; set it above the shutdown timeout.
  • Trusting every proxy. Configure the exact networks (guide 28).