Node.js in Docker: best practices voor productie

Leer hoe je Node.js in Docker draait met best practices. Checklist met 12 praktische punten voor images, security en productie-ready containers.

29 juli 20266 min leestijdDoor We Develop Communication

Je Node.js app lokaal draaien is één ding, maar hem betrouwbaar in een container krijgen is een ander verhaal. Docker-images worden te groot, containers draaien als root, en graceful shutdowns werken niet zoals verwacht.

Node.js in Docker draaien vraagt om specifieke keuzes die verschillen van bijvoorbeeld een Python- of Java-container. Denk aan signal handling, het afhandelen van native dependencies, en het slim stapelen van layers voor snelle builds.

In deze checklist lopen we 12 praktische punten door die je helpen een productie-ready Node.js container te bouwen. Van base image keuze tot security hardening en orchestratie-klaar maken.

1. Kies de juiste base image

Niet elke Node.js base image is geschikt voor productie. De officiële node images op Docker Hub bieden verschillende varianten, elk met eigen trade-offs.

  • node:lts, Debian-based, groot (~350MB), maximale compatibiliteit
  • node:lts-slim, Debian slim, ~75MB, werkt met meeste native modules
  • node:lts-alpine, Alpine Linux, ~50MB, soms gedoe met native modules door musl libc
  • gcr.io/distroless/nodejs, Distroless, minimaal, geen shell (goed voor security)

Voor de meeste projecten is node:lts-alpine een prima default. Gebruik een specifieke versie (node:22.11-alpine) in plaats van lts om reproduceerbare builds te garanderen.

2. Gebruik multi-stage builds

Een multi-stage build scheidt de build-fase (met devDependencies, TypeScript compiler, etc.) van de runtime-fase. Zo blijft je productie-image klein en bevat alleen wat echt nodig is.

FROM node:22-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM node:22-alpine AS runtime
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev && npm cache clean --force
COPY --from=builder /app/dist ./dist
USER node
CMD ["node", "dist/server.js"]

Dit patroon werkt extra goed in combinatie met TypeScript. Lees onze praktische gids over TypeScript in Node.js projecten voor details over de build-pipeline.

3. Optimaliseer layer caching

Docker bouwt images in layers, en elke layer wordt gecached. Door je COPY en RUN statements slim te ordenen, voorkom je dat je bij elke code-wijziging opnieuw alle dependencies hoeft te installeren.

  • Kopieer eerst package.json en package-lock.json
  • Draai dan npm ci
  • Kopieer daarna pas je source code

Deze volgorde zorgt dat de (trage) npm ci stap alleen opnieuw draait als je dependencies wijzigen, niet bij elke code-change.

4. Draai niet als root

Officiële Node.js images bevatten een non-root node user. Gebruik deze altijd in productie met USER node. Draai je als root, dan heeft een aanvaller bij een container breakout meteen root-rechten op de host (afhankelijk van je runtime-configuratie).

USER node
WORKDIR /home/node/app
COPY --chown=node:node . .

Let op de --chown flag bij COPY, anders zijn je bestanden eigendom van root en kan je non-root proces ze mogelijk niet schrijven.

5. Gebruik .dockerignore

Net als .gitignore voorkomt een .dockerignore dat onnodige bestanden in je build context belanden. Dit versnelt builds en voorkomt dat secrets per ongeluk in je image belanden.

node_modules
npm-debug.log
.git
.env
.env.local
coverage
.vscode
Dockerfile

Zonder .dockerignore kopieert Docker soms gigabytes aan node_modules of .git history naar de build context, wat je builds onnodig vertraagt.

6. Installeer alleen production dependencies

In je runtime-stage installeer je alleen dependencies die je daadwerkelijk nodig hebt. Gebruik npm ci --omit=dev (of --production voor oudere npm-versies) om devDependencies over te slaan.

RUN npm ci --omit=dev && npm cache clean --force

De npm cache clean stap scheelt nog eens enkele honderden megabytes in je final image. Voor yarn of pnpm gelden vergelijkbare flags (--production, --prod).

7. Gebruik de exec form voor CMD

Er zijn twee manieren om CMD te schrijven in een Dockerfile: de shell form en de exec form. Voor Node.js is de exec form essentieel, omdat alleen dan SIGTERM signals correct bij je proces aankomen.

# Goed, exec form
CMD ["node", "dist/server.js"]

# Fout, shell form, SIGTERM komt niet aan bij Node
CMD node dist/server.js

Zonder correcte signal handling kan je container niet gracefully afsluiten. Meer over dit onderwerp in error handling op schaal in Node.js.

8. Implementeer graceful shutdown

Wanneer een orchestrator zoals Kubernetes of Docker Swarm je container stopt, stuurt hij eerst SIGTERM. Je app moet dan lopende requests afmaken, database-connecties sluiten en pas daarna exit'en.

const server = app.listen(3000);

process.on('SIGTERM', async () => {
  console.log('SIGTERM received, shutting down gracefully');
  server.close(() => {
    console.log('HTTP server closed');
  });
  await db.end();
  process.exit(0);
});

Zonder graceful shutdown krijg je 502-errors tijdens deploys, omdat requests halverwege afgebroken worden. Dit is ook belangrijk als je background jobs en workers draait.

9. Configureer health checks

Docker en orchestrators gebruiken health checks om te bepalen of je container nog reageert. Voeg een HEALTHCHECK toe aan je Dockerfile, of exposeer een /health endpoint dat de orchestrator kan pollen.

HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
  CMD node -e "require('http').get('http://localhost:3000/health', r => process.exit(r.statusCode === 200 ? 0 : 1))"

In Kubernetes gebruik je liever livenessProbe en readinessProbe in je Pod-spec in plaats van de Dockerfile HEALTHCHECK. Dit geeft meer controle over timing en gedrag.

10. Beheer configuratie via environment variables

Hardcode nooit database-URLs, API-keys of andere config in je image. Gebruik environment variables die je bij runtime injecteert via Docker secrets, Kubernetes ConfigMaps of je CI/CD-pipeline.

ENV NODE_ENV=production
ENV PORT=3000

Zet NODE_ENV=production altijd expliciet. Veel npm-packages (zoals Express) schakelen dan optimalisaties in, zoals template caching en verkorte error messages.

11. Scan je images op vulnerabilities

Een kleine image is geen veilige image. Scan je builds regelmatig met tools als Trivy, docker scout, of Snyk om bekende CVEs in je base image en dependencies te detecteren.

docker scout cves my-nodejs-app:latest
trivy image my-nodejs-app:latest

Integreer dit in je CI-pipeline zodat builds falen bij kritieke vulnerabilities. Update regelmatig je base image (node:22-alpine pulls de nieuwste patch-versie) en herbouw je containers.

12. Beperk resources en rechten

In productie wil je voorkomen dat één container de hele host opeet. Zet expliciet memory en CPU limits, en beperk capabilities waar mogelijk.

docker run --memory=512m --cpus=1 \
  --read-only --tmpfs /tmp \
  --cap-drop=ALL \
  my-nodejs-app

Voor Node.js specifiek kun je ook de V8 heap size afstemmen op je container memory limit met NODE_OPTIONS=--max-old-space-size=400. Anders houdt V8 geen rekening met cgroup limits en krijg je OOM kills.

Voor meer context over resource-gedrag en scaling, zie onze gids over clustering en scaling in Node.js.

Extra: combineer met een production readiness check

Een goed geconfigureerde container is maar één onderdeel van productie-klaar zijn. Loop ook onze production readiness checklist voor Node.js apps door om te checken of logging, monitoring en error handling op orde zijn.

Voor de volledige Docker-documentatie verwijzen we naar de officiële Node.js Docker best practices guide en de Docker documentatie over multi-stage builds.

Veelgestelde vragen

Welke Node.js base image kan ik het beste gebruiken in Docker?

Gebruik een officiële node:lts-alpine of node:lts-slim image voor productie. Alpine is het kleinst (~50MB), slim is een goed compromis tussen grootte en compatibiliteit met native modules zoals bcrypt of sharp.

Moet ik mijn Node.js app als root gebruiker draaien in Docker?

Nee, nooit. Gebruik de ingebouwde node user die in officiële images aanwezig is, of maak een eigen non-root user aan met USER node. Dit beperkt de schade bij een container breakout aanzienlijk.

Hoe klein kan ik mijn Node.js Docker image krijgen?

Met multi-stage builds, een Alpine base image en het uitsluiten van devDependencies kom je eenvoudig onder de 100MB. Voor kleinere images kun je distroless of node:lts-slim overwegen, afhankelijk van je dependencies.

Hoe handel ik SIGTERM signals af in een Node.js container?

Zorg dat je Node.js proces SIGTERM signals ontvangt en graceful shutdown implementeert. Gebruik CMD ["node", "server.js"] (exec form) in je Dockerfile en luister expliciet naar process.on('SIGTERM') in je code.

Moet ik nodemon of pm2 in een productie Docker container gebruiken?

Nodemon is alleen voor development. In productie draai je Node.js direct zonder wrapper, zodat Docker of Kubernetes de restart policy kan beheren. Eén proces per container is best practice.

Veelgestelde vragen

Klaar om digitaal te groeien?

Wij helpen Nederlandse bedrijven met webtechnologie en SEO-strategieën die écht werken. Neem vrijblijvend contact op.