Ship to production: NODE_ENV, process manager, a correct Docker image, and multi-core scaling with cluster.
Open this lesson in KodokonNODE_ENV is merely a library convention: the Node core almost entirely ignores it. But Express, among others, uses it to cache views and hide stack traces; some frameworks are several times faster with NODE_ENV=production. On the dependencies side, npm ci --omit=dev installs exactly the lockfile, without dev dependencies - reproducible and faster than npm install. Since Node 20.6, --env-file loads a .env file natively, with no dependency.
NODE_ENV=production node server.js
npm ci --omit=dev
node --env-file=.env server.jsA Node process always ends up dying: memory overrun, bug, exception. The role of the process manager is to restart it. Two schools: on a bare machine, pm2 (or systemd) supervises, restarts, and reloads without downtime; in a container, it's the orchestrator (Kubernetes, ECS) that plays this role - one process per container, and pm2 becomes useless, even harmful, because it hides crashes from the orchestrator.
npm install -g pm2
pm2 start server.js -i max --name api
pm2 reload api
pm2 logs api --lines 100Two Docker pitfalls specific to Node. One: the image should run as the unprivileged node user, provided by the official images. Two: Node isn't designed to be PID 1 - it doesn't reap zombie processes; launch the container with docker run --init (or init: true in Compose) to insert a minimal init. Finally, write CMD in exec form: the shell form inserts /bin/sh, which doesn't forward SIGTERM - the graceful shutdown from the previous lesson would never trigger.
FROM node:20-slim AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
FROM node:20-slim
ENV NODE_ENV=production
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
USER node
EXPOSE 3000
CMD ["node", "server.js"]A single Node process uses only one core. The cluster module creates one worker process per core: the primary listens on the port and distributes connections to the workers in round-robin (the default behavior, except on Windows). Each worker has its own memory and its own event loop: no shared state - sessions and caches must live in Redis or an equivalent. For pure computation, prefer worker_threads, which shares memory via SharedArrayBuffer.
import cluster from "node:cluster";
import { createServer } from "node:http";
import { availableParallelism } from "node:os";
if (cluster.isPrimary) {
const count = availableParallelism();
for (let i = 0; i < count; i += 1) cluster.fork();
cluster.on("exit", (worker) => {
console.log(`worker ${worker.process.pid} down`);
cluster.fork();
});
} else {
createServer((req, res) => {
res.end(`pid ${process.pid}`);
}).listen(3000);
}