Production & Deployment
Node.js Advanced
Chapter 8 of 8 · Production & Deployment
Production & Deployment
Getting an app to work locally is the easy part. Getting it to run reliably under real load, restart cleanly after crashes, deploy without dropping requests, surface problems before users do, and recover quickly when something goes wrong — that's production. This final chapter is a checklist-driven survey of everything a Node.js application needs before it earns the right to serve real traffic: a production-ready Docker image, the 12-factor disciplines, structured logging, metrics, zero-downtime deployment strategies, and an incident-response mindset.
The 12-Factor App
A methodology from Heroku (2011) that has become the reference standard for cloud-native applications. Every factor is directly applicable to Node.js.
| # | Factor | What it means for Node |
|---|---|---|
| I | Codebase | One repo, many deploys. No per-environment branches. |
| II | Dependencies | package.json + package-lock.json. No assumed system-wide packages. npm ci in production. |
| III | Config | All config in environment variables — never in code or committed files. process.env.DATABASE_URL, not hardcoded strings. |
| IV | Backing services | DB, Redis, S3 are attached resources accessed by URL. Swap dev DB for prod DB by changing one env var, no code change. |
| V | Build/Release/Run | Strictly separate stages: npm ci (build) → image tag (release) → container start (run). Never patch a running container. |
| VI | Processes | Stateless and share-nothing. Session state in Redis, files in S3 — not in the process's memory or local disk. |
| VII | Port binding | app.listen(process.env.PORT). The app exports its own HTTP server; the platform maps the port. |
| VIII | Concurrency | Scale out by running more processes (containers), not by adding threads. Node's cluster module or multiple containers behind a load balancer. |
| IX | Disposability | Fast startup (<5s), graceful shutdown on SIGTERM. Jobs in a queue survive a crash; in-flight requests drain before exit. |
| X | Dev/prod parity | Same Node version, same backing service versions (same Redis image in dev as prod). Docker Compose keeps parity. |
| XI | Logs | Write to stdout only. Never open log files in the app. The platform collects, routes, and stores logs. |
| XII | Admin processes | One-off tasks (migrations, scripts) run in the same environment as the app, as ephemeral processes, not special long-lived servers. |
Docker — Production-Ready Dockerfile
Structured Logging with Pino
Plain text logs are for humans reading a terminal. Production logs go into Elasticsearch, Datadog, or CloudWatch — they need to be machine-parseable JSON. Pino is the fastest Node.js JSON logger (asynchronous, minimal overhead) and is the default in Fastify, but works equally well with Express.
Metrics with prom-client
Logs tell you what happened. Metrics tell you the rate, latency distribution, and error ratio across all requests in real time. Prometheus scrapes a /metrics endpoint; Grafana visualises it.
Zero-Downtime Deployment
| Strategy | How it works | Rollback | Cost |
|---|---|---|---|
| Rolling | Replace instances one (or a few) at a time. Traffic is load-balanced across old and new instances during the rollout. | Roll forward or restart previous image | No extra infra — same instance count |
| Blue/Green | Spin up the new version alongside the old (blue=old, green=new). Switch load balancer to green when smoke tests pass. Blue stays ready as instant rollback. | Instant — flip load balancer back | Double the infra during switch |
| Canary | Route a small percentage of traffic (e.g. 5%) to the new version. Monitor error rate and latency. Gradually increase if healthy. | Drain canary traffic back to stable | Low — small canary pool only |
Graceful Shutdown in Production
Security Hardening Checklist
Container Security
Run as a non-root user (Dockerfile USER appuser). Use a minimal base image (alpine). Scan images with docker scout cves or Trivy before shipping. Never bake secrets into the image — mount them as environment variables or via a secrets manager at runtime.
Dependency Security
npm ci (not npm install) in Docker builds — exact lockfile, no drift. npm audit --audit-level=high in CI; fail the build on high/critical. Pin your base Node image to a specific digest, not just a tag.
Secrets Management
Never commit .env files. In production: use your platform's secrets store (AWS Secrets Manager, Kubernetes Secrets, HashiCorp Vault). Inject at runtime as environment variables. Validate at startup — fail fast with a clear error if a required secret is missing.
Network Security
Only expose the port the app listens on. Internal services (Redis, DB) should not be reachable from outside the cluster. Use TLS for all service-to-service communication. Helmet middleware for HTTP security headers. Rate limiting at the gateway or per-route.
Incident Response
When something goes wrong in production, the first five minutes determine whether it's a 10-minute incident or a 4-hour outage. Having the process internalised before the alarm goes off is what separates a calm response from a panic.
Production Readiness Checklist
- All config comes from environment variables (no hardcoded strings)
- App binds to
process.env.PORT - SIGTERM handler drains in-flight requests before exit
- uncaughtException and unhandledRejection log and exit (letting orchestrator restart)
- Structured JSON logs to stdout — no file appenders
- LOG_LEVEL configurable via env; secrets redacted
- GET /health/live and GET /health/ready endpoints present
- Dockerfile uses multi-stage build and non-root user
- npm ci (not npm install) in Dockerfile
- .env files in .dockerignore and .gitignore
- npm audit passes at --audit-level=high in CI
- Prometheus /metrics endpoint exposed (or equivalent)
- Default prom-client metrics collected (event loop, GC, heap)
- DB connection pool sized for expected concurrency
- All external service calls have timeouts set
- Circuit breakers on critical downstream dependencies
- BullMQ workers handle SIGTERM (worker.close() in shutdown sequence)
- DB schema migrations run separately from app startup
- Rolling/canary deployment — app handles mixed v1/v2 traffic gracefully
- Runbook documented: how to check status, rollback, restart, take heap snapshot
💡 The Node.js Maturity Ladder
Looking back across all three courses: you started with the event loop and callbacks, moved through async patterns, streams, HTTP servers, and testing in the Fundamentals course; added child processes, worker threads, database integration, environment config, process management, and PM2 in Intermediate; and in this Advanced course covered profiling, memory management, WebSockets, security, caching, message queues, microservices, and production deployment.
The thread running through all of it: Node's single-threaded event loop is both its superpower and its constraint. Keep work off the main thread, never block, always handle errors, always set timeouts, always clean up — and it'll handle tens of thousands of concurrent connections on modest hardware without complaint.
Coding Challenges
Challenge 1 — Production Server Bootstrap
Write a createServer(app, options) factory that wires up everything a production Node.js server needs at startup: validates required env vars (fail-fast with a clear error listing every missing variable), configures Pino structured logging with request middleware, mounts /health/live and /health/ready routes, mounts a Prometheus /metrics route, installs SIGTERM/SIGINT graceful shutdown (30s drain timeout, then force exit), and handles uncaughtException/unhandledRejection by logging fatal and exiting. Return { server, logger, shutdown } so callers can reference them. Write a test that verifies the missing-env-var validation throws before the server starts.
Challenge 2 — Dockerfile Audit Tool
Write a Node.js script audit-dockerfile.js <path> that reads a Dockerfile and checks for the following best practices, printing a PASS/FAIL/WARN result for each: (1) uses a specific image tag or digest (not :latest), (2) uses multi-stage build (FROM … AS), (3) has a non-root USER instruction, (4) uses npm ci not npm install, (5) has a HEALTHCHECK instruction, (6) CMD uses array form not shell form, (7) .env is not COPYed (check for COPY .env patterns). Exit code 0 if no FAILs, 1 if any FAILs. Test it against a deliberately broken Dockerfile and a clean one.
Challenge 3 — Observability Middleware Stack
Build an Express middleware factory createObservability({ serviceName, logLevel }) that returns an array of middleware providing: Pino HTTP request/response logging (with requestId generated via crypto.randomUUID() and attached to all logs in the request), Prometheus request counter and duration histogram (labelled by method and route, using req.route.path not req.path to avoid high-cardinality label explosion), and an error-catching middleware that logs at error level with the requestId and returns a sanitised JSON error (no stack traces to the client). Write integration tests using supertest that verify: requestId appears in response headers, a slow route records a duration above a threshold, and a thrown error returns 500 with no stack trace in the body.
Node.js Advanced — Complete
8 chapters · Performance Profiling · Memory Management · WebSockets · Security · Caching · Message Queues · Microservices · Production & Deployment