Kodokon kodokon.com

In Produktion gehen: Prozessmanager, Docker, cluster

Bring dein Projekt in Produktion: NODE_ENV, Prozessmanager, ein korrektes Docker-Image und Skalierung über mehrere Kerne mit cluster.

10 Min. · 3 Fragen

Diese Lektion in Kodokon öffnen

NODE_ENV ist lediglich eine Konvention der Bibliotheken: Der Node-Kern ignoriert sie fast vollständig. Aber Express und andere nutzen sie, um Views zu cachen und Stacktraces zu verbergen; manche Frameworks sind mit NODE_ENV=production um ein Vielfaches schneller. Auf Seiten der Abhängigkeiten installiert npm ci --omit=dev exakt die Lockfile, ohne Dev-Abhängigkeiten - reproduzierbar und schneller als npm install. Seit Node 20.6 lädt --env-file eine .env-Datei nativ, ganz ohne Abhängigkeit.

BASH
NODE_ENV=production node server.js

npm ci --omit=dev

node --env-file=.env server.js
Die drei Grundbefehle für den Gang in die Produktion.

Ein Node-Prozess stirbt irgendwann immer: Speicherüberlauf, Bug, Ausnahme. Die Rolle des Prozessmanagers ist es, ihn neu zu starten. Zwei Schulen: Auf einer nackten Maschine überwacht, startet und lädt pm2 (oder systemd) ohne Ausfallzeit neu; in einem Container übernimmt der Orchestrator (Kubernetes, ECS) diese Rolle - ein Prozess pro Container, und pm2 wird nutzlos, sogar schädlich, weil es Abstürze vor dem Orchestrator verbirgt.

BASH
npm install -g pm2
pm2 start server.js -i max --name api
pm2 reload api
pm2 logs api --lines 100
pm2: -i max forkt einen Worker pro Kern, reload ohne Ausfallzeit.

Zwei Docker-Fallstricke, die typisch für Node sind. Erstens: Das Image sollte als unprivilegierter Nutzer node laufen, den die offiziellen Images mitbringen. Zweitens: Node ist nicht dafür gebaut, PID 1 zu sein - es räumt keine Zombie-Prozesse ab; starte den Container mit docker run --init (oder init: true in Compose), um ein minimales init einzuschieben. Und schließlich schreibe CMD in Exec-Form: Die Shell-Form schiebt /bin/sh dazwischen, das SIGTERM nicht weiterreicht - das saubere Herunterfahren aus der vorherigen Lektion würde nie ausgelöst.

BASH
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"]
Mehrstufiges Dockerfile: Nutzer node, CMD in Exec-Form.

Ein einzelner Node-Prozess nutzt nur einen Kern. Das Modul cluster erstellt einen Worker-Prozess pro Kern: Der Primary lauscht auf dem Port und verteilt die Verbindungen im Round-Robin an die Worker (das Standardverhalten, außer unter Windows). Jeder Worker hat seinen eigenen Speicher und seine eigene Event-Loop: kein geteilter Zustand - Sessions und Caches müssen in Redis oder etwas Gleichwertigem leben. Für reine Berechnungen bevorzugst du worker_threads, die den Speicher über SharedArrayBuffer teilen.

JAVASCRIPT
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);
}
Ein Worker pro Kern, automatisch neu gestartet, wenn er stirbt.

Wissenscheck

Stelle sicher, dass du die wichtigsten Punkte dieser Lektion behalten hast.

  1. Was ändert NODE_ENV=production tatsächlich?
    • Node.js aktiviert einen aggressiveren JIT-Compiler
    • Bibliotheken wie Express aktivieren ihre Optimierungen (View-Caching, knappe Fehlermeldungen)
    • Node.js verweigert den Start ohne HTTPS
  2. Warum die Exec-Form CMD ["node", "server.js"] in einem Dockerfile bevorzugen?
    • Die Exec-Form startet schneller
    • Die Exec-Form erlaubt die Interpolation von Variablen
    • Die Shell-Form geht über /bin/sh, das SIGTERM nicht an den Node-Prozess weiterreicht
  3. Was passiert mit cluster und 8 Workern mit dem Speicher und dem Anwendungszustand?
    • Jeder Worker ist ein eigener Prozess: Speicher mal 8, kein geteilter Zustand
    • Die Worker teilen sich denselben V8-Heap
    • Der Primary führt die Heaps der Worker regelmäßig zusammen