Docker pour Développeurs Node.js : Guide Pratique de Conteneurisation
Mark Toledo
13 mars 2026

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 upsuffit 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 quenpm 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.
