Node.js

Docker pour Développeurs Node.js : Guide Pratique de Conteneurisation

Mark Toledo

Mark Toledo

13 mars 2026

Docker pour Développeurs Node.js : Guide Pratique de Conteneurisation

Docker est devenu incontournable dans le développement Node.js. Fini le classique « ça marche sur ma machine » : un conteneur embarque tout ce dont votre application a besoin pour tourner de manière identique en développement, en staging et en production. Ce guide vous montre comment conteneuriser vos projets Node.js de manière professionnelle, du Dockerfile basique aux builds multi-stage en passant par Docker Compose.

Pourquoi Docker pour Node.js ?

Avant de plonger dans la technique, rappelons les gains concrets :

  • Reproductibilité : même version de Node, mêmes dépendances, même OS, partout.
  • Isolation : chaque service tourne dans son propre conteneur sans conflit de ports ni de versions.
  • Déploiement simplifié : une image Docker se déploie identiquement sur n'importe quelle infrastructure.
  • Scalabilité : orchestrer plusieurs instances avec Kubernetes ou Docker Swarm devient trivial.
  • Onboarding rapide : un docker compose up suffit pour lancer tout l'environnement de développement.

Écrire un Dockerfile pour Node.js

Le Dockerfile basique

Commençons par un Dockerfile simple pour une API Express :

FROM node:22-alpine

WORKDIR /app

COPY package*.json ./

RUN npm ci --only=production

COPY . .

EXPOSE 3000

CMD ["node", "src/index.js"]

Décortiquons chaque instruction :

  • FROM node:22-alpine : image de base Alpine Linux (légère, ~50 Mo vs ~350 Mo pour l'image complète).
  • WORKDIR /app : définit le répertoire de travail dans le conteneur.
  • COPY package*.json ./ : copie uniquement les fichiers de dépendances d'abord (optimise le cache Docker).
  • RUN npm ci : installe les dépendances de manière déterministe (plus fiable que npm install).
  • COPY . . : copie le reste du code source.
  • EXPOSE 3000 : documente le port exposé.
  • CMD : commande de démarrage.

Le fichier .dockerignore

Indispensable pour ne pas copier de fichiers inutiles dans l'image :

node_modules
npm-debug.log
.git
.gitignore
.env
.env.*
Dockerfile
docker-compose.yml
.dockerignore
coverage
.nyc_output
dist
out
*.md

Ce fichier réduit drastiquement la taille du contexte de build et évite de fuiter des secrets.

Multi-Stage Builds

Les builds multi-stage sont la clé pour des images de production légères. Le principe : utiliser une première étape pour compiler, puis copier uniquement le nécessaire dans l'image finale.

Application TypeScript

# === Étape 1 : Build ===
FROM node:22-alpine AS builder

WORKDIR /app

COPY package*.json ./
RUN npm ci

COPY tsconfig.json ./
COPY src/ ./src/

RUN npm run build

# === Étape 2 : Production ===
FROM node:22-alpine AS production

WORKDIR /app

# Créer un utilisateur non-root
RUN addgroup -g 1001 -S nodejs && \
    adduser -S nodeapp -u 1001

COPY package*.json ./
RUN npm ci --only=production && npm cache clean --force

COPY --from=builder /app/dist ./dist

USER nodeapp

EXPOSE 3000

HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
  CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1

CMD ["node", "dist/index.js"]

Points importants :

  • Utilisateur non-root : bonne pratique de sécurité — ne jamais exécuter Node en root.
  • npm ci --only=production : n'installe que les dépendances de production dans l'image finale.
  • HEALTHCHECK : permet à Docker (et aux orchestrateurs) de vérifier que l'application répond.
  • Taille finale : typiquement 80-150 Mo au lieu de 400-800 Mo.

Application NestJS

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

FROM node:22-alpine AS production
WORKDIR /app
RUN addgroup -g 1001 -S nodejs && adduser -S nestapp -u 1001
COPY package*.json ./
RUN npm ci --only=production && npm cache clean --force
COPY --from=builder /app/dist ./dist
USER nestapp
EXPOSE 3000
CMD ["node", "dist/main.js"]

Docker Compose pour le Développement

Stack complète : API + Base de données + Redis

Docker Compose orchestre plusieurs services. Voici une configuration typique pour un projet Node.js :

# docker-compose.yml
services:
  api:
    build:
      context: .
      dockerfile: Dockerfile.dev
    ports:
      - "3000:3000"
      - "9229:9229"  # Port de débogage Node.js
    volumes:
      - .:/app
      - /app/node_modules  # Préserve les node_modules du conteneur
    environment:
      - NODE_ENV=development
      - DATABASE_URL=postgresql://user:password@db:5432/myapp
      - REDIS_URL=redis://cache:6379
    depends_on:
      db:
        condition: service_healthy
      cache:
        condition: service_started
    command: npm run dev

  db:
    image: postgres:17-alpine
    ports:
      - "5432:5432"
    environment:
      POSTGRES_USER: user
      POSTGRES_PASSWORD: password
      POSTGRES_DB: myapp
    volumes:
      - pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U user -d myapp"]
      interval: 5s
      timeout: 3s
      retries: 5

  cache:
    image: redis:7-alpine
    ports:
      - "6379:6379"
    volumes:
      - redisdata:/data

volumes:
  pgdata:
  redisdata:

Dockerfile de développement

Le Dockerfile de dev est différent de celui de production — il inclut les devDependencies et le hot-reload :

# Dockerfile.dev
FROM node:22-alpine

WORKDIR /app

RUN npm install -g nodemon

COPY package*.json ./
RUN npm ci

COPY . .

EXPOSE 3000 9229

CMD ["nodemon", "--inspect=0.0.0.0:9229", "src/index.js"]

Commandes essentielles

# Lancer toute la stack
docker compose up -d

# Voir les logs en temps réel
docker compose logs -f api

# Reconstruire après modification du Dockerfile
docker compose up -d --build

# Exécuter une commande dans le conteneur
docker compose exec api npm run migrate

# Arrêter et supprimer tout
docker compose down -v

Gestion des Variables d'Environnement

Bonnes pratiques

Ne codez jamais de secrets en dur. Docker offre plusieurs mécanismes :

1. Fichier .env (développement uniquement)

# .env
DATABASE_URL=postgresql://user:password@db:5432/myapp
JWT_SECRET=dev-secret-change-in-production
API_KEY=sk-dev-12345
# docker-compose.yml
services:
  api:
    env_file:
      - .env

2. Variables dans le Compose (staging)

services:
  api:
    environment:
      - NODE_ENV=staging
      - LOG_LEVEL=debug

3. Docker Secrets (production)

services:
  api:
    secrets:
      - db_password
      - jwt_secret
    environment:
      - DB_PASSWORD_FILE=/run/secrets/db_password

secrets:
  db_password:
    file: ./secrets/db_password.txt
  jwt_secret:
    file: ./secrets/jwt_secret.txt

Dans votre code Node.js, lisez les secrets depuis le fichier :

const fs = require('fs')

function getSecret(name) {
  const filePath = process.env[`${name}_FILE`]
  if (filePath) {
    return fs.readFileSync(filePath, 'utf8').trim()
  }
  return process.env[name]
}

const dbPassword = getSecret('DB_PASSWORD')

Volumes et Persistance

Types de volumes

Docker propose trois types de montage :

services:
  api:
    volumes:
      # Volume nommé — géré par Docker, persiste entre restarts
      - nodedata:/app/data

      # Bind mount — lie un dossier local au conteneur (dev)
      - ./src:/app/src

      # Volume anonyme — protège node_modules du bind mount
      - /app/node_modules

Problème classique : node_modules en bind mount

Quand vous montez votre code source dans le conteneur, le node_modules local peut écraser celui du conteneur (et vice versa). La solution :

volumes:
  - .:/app              # Monte le code source
  - /app/node_modules   # Protège node_modules du conteneur

Cette astuce crée un volume anonyme pour node_modules à l'intérieur du conteneur, indépendant de votre dossier local.

Débogage dans un Conteneur

Déboguer avec VS Code

Configurez le débogueur VS Code pour se connecter au processus Node.js dans le conteneur :

// .vscode/launch.json
{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "Docker: Attach to Node",
      "type": "node",
      "request": "attach",
      "port": 9229,
      "address": "localhost",
      "localRoot": "${workspaceFolder}",
      "remoteRoot": "/app",
      "restart": true,
      "skipFiles": ["<node_internals>/**"]
    }
  ]
}

Assurez-vous que le port 9229 est exposé et que Node.js est lancé avec --inspect=0.0.0.0:9229.

Inspecter un conteneur en panne

# Voir les logs
docker compose logs api --tail 50

# Shell interactif dans un conteneur en cours d'exécution
docker compose exec api sh

# Lancer un conteneur éphémère pour debug
docker compose run --rm api sh

# Voir les processus
docker compose top api

# Voir l'utilisation des ressources
docker stats

Optimiser la Taille de l'Image

Comparer les tailles

# Voir la taille de vos images
docker images | grep mon-api

# Analyser les couches
docker history mon-api:latest

Techniques d'optimisation

1. Choisir la bonne image de base

Image Taille Usage
node:22 ~350 Mo Rarement justifié
node:22-slim ~80 Mo Bon compromis
node:22-alpine ~50 Mo Recommandé
distroless ~30 Mo Sécurité maximale

2. Réduire les couches

# ❌ Crée 3 couches
RUN apt-get update
RUN apt-get install -y curl
RUN apt-get clean

# ✅ Une seule couche
RUN apt-get update && \
    apt-get install -y --no-install-recommends curl && \
    apt-get clean && \
    rm -rf /var/lib/apt/lists/*

3. Nettoyer le cache npm

RUN npm ci --only=production && npm cache clean --force

Mise en Production

Checklist de production

Avant de déployer votre conteneur en production, vérifiez ces points :

  • Image multi-stage : seul le code compilé et les dépendances de production sont inclus.
  • Utilisateur non-root : l'application tourne sous un utilisateur dédié.
  • Healthcheck : Docker sait si l'application est fonctionnelle.
  • Pas de secrets dans l'image : utilisez des variables d'environnement ou Docker Secrets.
  • Logs sur stdout/stderr : Docker capture automatiquement les sorties standard.
  • Gestion du signal SIGTERM : votre application s'arrête proprement.

Gestion propre de l'arrêt

const server = app.listen(3000)

process.on('SIGTERM', () => {
  console.log('SIGTERM reçu, arrêt gracieux...')
  server.close(() => {
    console.log('Serveur fermé')
    process.exit(0)
  })

  // Force l'arrêt après 10 secondes
  setTimeout(() => {
    console.error('Arrêt forcé après timeout')
    process.exit(1)
  }, 10000)
})

Pipeline CI/CD

# .github/workflows/deploy.yml
jobs:
  build-and-push:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Build et tag l'image
        run: |
          docker build -t mon-api:${{ github.sha }} .
          docker tag mon-api:${{ github.sha }} mon-api:latest

      - name: Lancer les tests dans le conteneur
        run: |
          docker run --rm mon-api:${{ github.sha }} npm test

      - name: Push vers le registry
        run: |
          docker push mon-api:${{ github.sha }}
          docker push mon-api:latest

Conclusion

Docker n'est plus un luxe pour les développeurs Node.js — c'est un standard. Avec un Dockerfile bien écrit, des builds multi-stage et Docker Compose, vous gagnez en fiabilité, en vitesse d'onboarding et en confiance lors des déploiements. Commencez par conteneuriser un projet existant avec le Dockerfile basique, puis itérez vers les optimisations multi-stage et la gestion avancée des secrets. L'investissement initial se rentabilise dès la première mise en production sans surprise.