Credit : Logo officiel
Docker pour debutants : conteneuriser une app Node.js
Docker pour débutants : conteneuriser une app Node.js
Il y a deux semaines, un client me transmet une application Node.js qu'il faut déployer en production. Le README dit : "installer Node 18, MongoDB 5, Redis, et configurer trois variables d'environnement". Sur le papier facile, en réalité un cauchemar : la version de Node sur Debian 12 par défaut est trop ancienne, MongoDB demande un dépôt externe, Redis tourne déjà sur ce serveur pour un autre projet. Trois heures de bidouille pour faire tourner ce qu'un docker compose up -d aurait fait en cinq minutes.
Docker est aujourd'hui le standard de fait pour empaqueter des applications. Voici comment je conteneurise une application Node.js Express depuis zéro sur un VPS IONOS sous Debian 12.
Installer Docker proprement
Le paquet docker.io de Debian fonctionne mais reste en retard sur les versions. Pour la production, j'installe toujours Docker depuis le dépôt officiel.
apt update
apt install ca-certificates curl gnupg -y
install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/debian/gpg | \
gpg --dearmor -o /etc/apt/keyrings/docker.gpg
chmod a+r /etc/apt/keyrings/docker.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \
https://download.docker.com/linux/debian $(. /etc/os-release && echo $VERSION_CODENAME) stable" \
| tee /etc/apt/sources.list.d/docker.list > /dev/null
apt update
apt install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin -y
Vérification :
docker --version
docker compose version
docker run --rm hello-world
Sortie attendue :
Docker version 26.1.4, build 5650f9b
Docker Compose version v2.27.0
Hello from Docker!
This message shows that your installation appears to be working correctly.
Utiliser Docker sans sudo
Ajoutez votre utilisateur au groupe docker (attention : équivaut à donner les droits root) :
usermod -aG docker deployer
newgrp docker
Sur un serveur de production, je préfère restreindre l'accès Docker aux scripts de CI/CD plutôt qu'à des utilisateurs interactifs.
L'application Node.js de démonstration
Créons une app Express minimaliste avec un endpoint santé et une connexion MongoDB.
Fichier package.json :
{
"name": "mon-app",
"version": "1.0.0",
"main": "server.js",
"scripts": {
"start": "node server.js",
"dev": "node --watch server.js"
},
"dependencies": {
"express": "^4.19.2",
"mongoose": "^8.4.0"
},
"engines": {
"node": ">=20"
}
}
Fichier server.js :
const express = require('express');
const mongoose = require('mongoose');
const app = express();
const PORT = process.env.PORT || 3000;
const MONGO_URI = process.env.MONGO_URI || 'mongodb://localhost:27017/monapp';
mongoose.connect(MONGO_URI)
.then(() => console.log('MongoDB connected'))
.catch(err => console.error('Mongo error:', err));
app.get('/', (req, res) => {
res.json({ message: 'API en ligne', timestamp: new Date() });
});
app.get('/health', (req, res) => {
const dbState = mongoose.connection.readyState === 1 ? 'up' : 'down';
res.json({ status: 'ok', db: dbState });
});
app.listen(PORT, () => {
console.log(`Serveur demarre sur le port ${PORT}`);
});
Le Dockerfile
Un Dockerfile bien fait suit deux principes : multi-stage build (séparer build et runtime) et utilisateur non-root.
# Stage 1: dependances
FROM node:20-alpine AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production && npm cache clean --force
# Stage 2: image finale
FROM node:20-alpine
WORKDIR /app
# Securite: user non-root
RUN addgroup -g 1001 -S nodejs && \
adduser -S -u 1001 -G nodejs nodejs
# Copier les dependances depuis le stage deps
COPY --from=deps --chown=nodejs:nodejs /app/node_modules ./node_modules
COPY --chown=nodejs:nodejs . .
USER nodejs
EXPOSE 3000
# Healthcheck integre
HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
CMD wget --quiet --tries=1 --spider http://localhost:3000/health || exit 1
CMD ["node", "server.js"]
Fichier .dockerignore (crucial pour la taille de l'image) :
node_modules
npm-debug.log
.git
.gitignore
.env
.env.local
Dockerfile
docker-compose*.yml
README.md
coverage
.vscode
Pourquoi Alpine ?
L'image node:20-alpine pèse environ 180 Mo contre 1 Go pour node:20. Sur un VPS avec 50 Go de disque, la différence se voit. Le seul piège : Alpine utilise musl libc, qui peut poser problème avec certains modules natifs (bcrypt, sharp). Si vous rencontrez des erreurs Error loading shared library, basculez sur node:20-slim (~250 Mo, basé sur Debian).
Construire et lancer le conteneur
docker build -t mon-app:1.0 .
docker run -d --name mon-app -p 3000:3000 mon-app:1.0
docker logs -f mon-app
curl http://localhost:3000/health
Sortie attendue :
{"status":"ok","db":"down"}
Le db: down est normal : on n'a pas encore branché MongoDB. C'est là que Docker Compose entre en scène.
Docker Compose : orchestrer plusieurs services
Une app moderne dépend rarement d'un seul process. Avec Compose, un fichier YAML décrit toute la stack.
Fichier docker-compose.yml :
services:
app:
build: .
container_name: mon-app
ports:
- "3000:3000"
environment:
NODE_ENV: production
MONGO_URI: mongodb://mongo:27017/monapp
PORT: 3000
depends_on:
mongo:
condition: service_healthy
restart: unless-stopped
networks:
- app-network
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
mongo:
image: mongo:7
container_name: mongo
volumes:
- mongo-data:/data/db
- ./mongo-init:/docker-entrypoint-initdb.d:ro
networks:
- app-network
restart: unless-stopped
healthcheck:
test: ["CMD", "mongosh", "--eval", "db.adminCommand('ping')"]
interval: 10s
timeout: 5s
retries: 5
nginx:
image: nginx:alpine
container_name: nginx
ports:
- "80:80"
volumes:
- ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
depends_on:
- app
networks:
- app-network
restart: unless-stopped
volumes:
mongo-data:
driver: local
networks:
app-network:
driver: bridge
Lancement :
docker compose up -d
docker compose ps
docker compose logs -f app
Sortie de docker compose ps :
NAME IMAGE STATUS PORTS
mon-app mon-app:1.0 Up 2 minutes (healthy) 0.0.0.0:3000->3000/tcp
mongo mongo:7 Up 2 minutes (healthy)
nginx nginx:alpine Up 2 minutes 0.0.0.0:80->80/tcp
Volumes : la persistance des données
docker volume ls
docker volume inspect mon-app_mongo-data
Le volume named mongo-data est stocké dans /var/lib/docker/volumes/. Les données survivent à docker compose down (mais pas à docker compose down -v, qui supprime aussi les volumes).
Pour une sauvegarde MongoDB :
docker compose exec mongo mongodump --archive --gzip > backup-$(date +%F).gz
Pour restaurer :
docker compose exec -T mongo mongorestore --archive --gzip < backup-2026-05-01.gz
Voir aussi Automatiser ses backups en bash + cron pour industrialiser.
Networking : comment les conteneurs communiquent
Dans le réseau app-network, chaque conteneur est résolu par son nom de service. Mon app accède à MongoDB via mongo:27017, jamais via localhost:27017 (qui pointerait sur le conteneur lui-même).
docker network ls
docker network inspect mon-app_app-network
Tester depuis l'intérieur d'un conteneur :
docker compose exec app sh
wget -qO- http://mongo:27017
Exposer ou pas ?
MongoDB et Nginx ont une différence cruciale : seul Nginx expose un port à l'hôte (80:80). MongoDB n'a aucun port mappé : seul le conteneur app peut y accéder, via le réseau interne. C'est la base de la sécurité Docker : n'exposez jamais que ce que l'extérieur doit voir.
Reverse proxy avec Nginx
Fichier nginx.conf :
server {
listen 80;
server_name monapp.fr;
location / {
proxy_pass http://app:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
}
}
Pour ajouter HTTPS, voir Reverse proxy Nginx avec SSL.
Variables d'environnement et secrets
Ne stockez jamais les mots de passe en clair dans le docker-compose.yml. Utilisez un fichier .env à la racine :
# .env
MONGO_USER=app_user
MONGO_PASSWORD=Sup3r-S3cret-2026!
NODE_ENV=production
Dans docker-compose.yml :
services:
mongo:
image: mongo:7
environment:
MONGO_INITDB_ROOT_USERNAME: ${MONGO_USER}
MONGO_INITDB_ROOT_PASSWORD: ${MONGO_PASSWORD}
Ajoutez .env à votre .gitignore. Pour des secrets vraiment sensibles en production, utilisez plutôt Docker Secrets (mode Swarm) ou un gestionnaire externe comme Vault.
Optimiser la taille de l'image
Une image bien construite pèse moins de 200 Mo pour une app Node typique. Quelques techniques que j'applique :
- Multi-stage build : déjà appliqué ci-dessus.
- Ordre des couches : copier
package*.jsonAVANT le code source. Le cache Docker évite de réinstaller les dépendances à chaque modification du code. .dockerignorerigoureux : pas denode_modules, pas de.git, pas de tests.npm ci --only=production: ne pas installer les devDependencies.- Nettoyage final :
npm cache clean --forceaprès l'install.
Mesurez l'impact :
docker images mon-app
REPOSITORY TAG IMAGE ID CREATED SIZE
mon-app 1.0 abc123def456 2 minutes ago 185MB
Si vous dépassez 500 Mo, il y a un problème. Inspectez les couches :
docker history mon-app:1.0
Commandes Docker que j'utilise tous les jours
# Voir les conteneurs actifs
docker ps
# Voir TOUS les conteneurs (même arrêtés)
docker ps -a
# Logs en temps réel
docker compose logs -f --tail=100 app
# Entrer dans un conteneur
docker compose exec app sh
# Reconstruire après changement
docker compose up -d --build
# Arrêter et nettoyer
docker compose down
# Arrêter et supprimer aussi les volumes
docker compose down -v
# Nettoyer les images orphelines
docker system prune -a
# Voir l'espace disque utilisé
docker system df
Erreurs courantes et leur fix
Error response from daemon: driver failed programming external connectivity
Cause : un autre process écoute déjà sur le port mappé (souvent Nginx ou Apache du serveur hôte).
Solution :
ss -tlnp | grep :80
systemctl stop nginx # ou changer le port dans docker-compose.yml
npm ci can only install with an existing package-lock.json
Cause : le package-lock.json est dans .dockerignore ou n'existe pas.
Solution : générez-le localement avec npm install et commitez-le. Retirez package-lock.json du .dockerignore.
Cannot connect to MongoDB: ECONNREFUSED 127.0.0.1:27017
Cause : votre app utilise localhost au lieu du nom de service Docker.
Solution : remplacez MONGO_URI=mongodb://localhost:27017/... par mongodb://mongo:27017/... dans le docker-compose.yml.
Permission denied while trying to connect to the Docker daemon socket
Cause : votre utilisateur n'est pas dans le groupe docker.
Solution :
usermod -aG docker $USER
newgrp docker
No space left on device
Cause : les images, volumes et logs Docker remplissent /var/lib/docker.
Solution :
docker system df
docker system prune -a --volumes
Attention : --volumes supprime les données persistantes des volumes orphelins. Faites un backup avant.
Pour aller plus loin
- Déployer PrestaShop avec Docker Compose — un cas concret e-commerce.
- Reverse proxy Nginx avec SSL — exposer vos conteneurs en HTTPS.
- Déployer une application avec GitHub Actions — pipeline CI/CD complet vers Docker.
- Créer une API REST avec Node.js et Express — la base avant de conteneuriser.
- Automatiser ses backups en bash + cron — industrialiser la sauvegarde des volumes.
Démarrage automatique au boot
Docker s'active au boot par défaut sur Debian 12. Pour vérifier :
systemctl is-enabled docker
systemctl enable docker
Les conteneurs avec restart: unless-stopped redémarrent automatiquement après un reboot serveur. C'est la politique que je mets sur tous mes services en production. Pour des conteneurs critiques, préférez restart: always.
Vérifiez après reboot :
docker compose ps
ss -tlnp | grep :80
Le conteneur, c'est la nouvelle unité de déploiement
Docker change la donne pour qui gère plusieurs applications sur un serveur : finies les conflits de versions, finies les surprises au déploiement, finie la dépendance à l'OS hôte. Une fois que votre Dockerfile et votre docker-compose.yml sont propres, déployer votre app sur un nouveau serveur prend trois minutes : git clone, docker compose up -d, terminé. C'est ce gain de reproductibilité qui en fait l'outil incontournable pour tout déploiement moderne.