Docker Compose en production : les 5 erreurs que personne ne te dit
La plupart des tutoriels Docker Compose t'apprennent à faire tourner une app sur ton ordinateur. Mais quand tu passes en production — un vrai serveur, de vrais utilisateurs, de vraies données — tu découvres des pièges que ces tutoriels n'ont jamais mentionnés.
Cet article ne construit rien from scratch. Il autopsie 5 erreurs réelles, montre ce qui casse, explique pourquoi, et donne la correction exacte. C'est le contenu que les ingénieurs seniors gardent pour eux — et que les juniors paient en nuits blanches pour apprendre.
Niveau requis : tu sais ce qu'est Docker Compose et tu as déjà écrit un
docker-compose.yml. Si tu es débutant complet, commence par le TUTORIAL.md avant cet article.
Le fichier qui a l'air correct — mais qui contient 5 bombes
Voici un docker-compose.yml typique. Il a l'air propre. Il fonctionne en local. Chaque ligne a l'air raisonnable.
version: '3.8'
services:
db:
image: postgres:15
environment:
POSTGRES_PASSWORD: supersecret123 # 💣 Bombe #3
volumes:
- ./postgres-data:/var/lib/postgresql/data # 💣 Bombe #2
api:
build: ./backend
environment:
FLASK_DEBUG: "true" # 💣 Bombe #3
depends_on:
- db # 💣 Bombe #1
nginx:
image: nginx:alpine # 💣 Bombe #4
ports:
- "80:80"
# 💣 Bombe #5 : absence de restart/logs
Allons désarmer chaque bombe.
💣 Erreur #1 — Le health check qui ment
Le symptôme
api_1 | OperationalError: could not connect to server
api_1 | Connection refused (host "db", port 5432)
api_1 exited with code 1
Cette erreur apparaît dans les 30 premières secondes du démarrage. En local : invisible (la DB démarre en 2 secondes). Sur un VPS chargé : dévastateur.
La cause racine
depends_on: - db dit à Docker : "attends que le conteneur db soit démarré". Pas qu'il soit prêt à accepter des connexions.
PostgreSQL démarre en plusieurs phases. Le process démarre (← Docker s'arrête là), puis il initialise les fichiers, charge la config, crée les bases — et seulement à la fin il ouvre le port 5432. Sur un VPS avec peu de RAM, ces étapes peuvent prendre 20-40 secondes.
La correction
db:
image: postgres:15
healthcheck:
test: ["CMD-SHELL", "pg_isready -U admin -d myapp"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s # ← le paramètre le plus ignoré
api:
depends_on:
db:
condition: service_healthy # ← attend vraiment que DB soit prête
start_period : pendant les 30 premières secondes, les échecs ne comptent pas. C'est la grâce au démarrage. Sans lui, Docker marque la DB unhealthy avant même qu'elle ait eu le temps de s'initialiser.
💣 Erreur #2 — Les volumes qui font disparaître les données
Le symptôme
docker compose down
docker compose up
# Base de données vide. 3 mois de données clients perdus.
La cause racine
volumes:
- ./postgres-data:/var/lib/postgresql/data
Un bind mount monte un dossier de ta machine dans le conteneur. C'est parfait pour du code en développement. Pour une base de données en production : problèmes de permissions, risque de corruption, données liées à un seul serveur.
La commande la plus dangereuse de Docker, avec une seule lettre de différence :
docker compose down # ✅ Conteneurs supprimés, données conservées
docker compose down -v # ❌ Conteneurs ET volumes supprimés → données perdues
La correction
services:
db:
volumes:
- pgdata:/var/lib/postgresql/data # volume nommé
volumes:
pgdata:
driver: local # Docker gère l'emplacement
Et séparer dev et prod avec deux fichiers :
# docker-compose.override.yml (dev uniquement, fusionné automatiquement)
services:
api:
volumes:
- ./backend:/app # live reload uniquement en dev
💣 Erreur #3 — Les secrets exposés
Le symptôme (découvert 48h plus tard)
git log --all -- .env
git show HASH:.env
# POSTGRES_PASSWORD=supersecret123
# Visible dans l'historique Git. Pour toujours.
Des bots scannent GitHub 24h/24. Délai moyen entre le push et la compromission : moins de 4 heures.
Les 3 vecteurs d'exposition
Vecteur 1 : le .env commité dans Git.
Vecteur 2 : un secret passé en ARG dans le Dockerfile.
docker history mon-image
# ENV DATABASE_URL=postgresql://admin:supersecret@db/myapp
# Gravé dans les layers de l'image. Lisible par quiconque y a accès.
Vecteur 3 : Flask en mode debug en production — affiche toutes les variables d'environnement dans le navigateur en cas d'erreur.
La correction
# .gitignore — la ligne la plus importante de ton repo
.env
# docker-compose.yml — référencer, jamais écrire en dur
environment:
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} # lu depuis .env
# Vérifier avant de démarrer
docker compose config
# Toutes les ${VARIABLES} doivent être remplacées par de vraies valeurs
💣 Erreur #4 — Nginx qui dégrade silencieusement
Ces bugs ne génèrent pas d'erreur 500. Ils génèrent des comportements étranges impossibles à diagnostiquer sans savoir où chercher.
Les 4 symptômes
1. L'app reçoit toujours 127.0.0.1 — géolocalisation cassée, rate limiting inutile, logs inutilisables. Cause : headers proxy non configurés.
2. Uploads échouent au-delà d'1MB — valeur par défaut de client_max_body_size. Non documentée dans les tutos. Découverte en production.
3. WebSockets coupés toutes les 60 secondes — proxy_read_timeout par défaut : 60s. Pour un chat en temps réel, c'est catastrophique.
4. Contenu périmé servi aux utilisateurs — Nginx cache les réponses API sans qu'on lui demande.
La configuration production-ready
server {
listen 80;
client_max_body_size 50m; # résout le symptôme #2
server_tokens off; # masque la version Nginx
location /api/ {
proxy_pass http://api:5000;
proxy_read_timeout 300s; # résout le symptôme #3
# Résout le symptôme #1 :
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;
# Résout le symptôme #4 :
proxy_no_cache 1;
add_header Cache-Control "no-store, no-cache, must-revalidate";
}
}
💣 Erreur #5 — Le crash silencieux
Le symptôme
docker compose ps
# NAME STATUS PORTS
# db Up 3 days (healthy) 5432/tcp
# nginx Up 3 days (healthy) 0.0.0.0:80->80/tcp
# api Exited (1) 2h ago ← mort depuis 2 heures
Aucune alerte. Aucun redémarrage. L'app était down depuis 2 heures quand le client a appelé.
La correction en 2 parties
Restart policy :
restart: unless-stopped
# Redémarre si crash ✅
# Redémarre au reboot du serveur ✅
# Respecte docker compose stop ✅
# N'entre pas en restart loop ✅
Pourquoi pas always ? Si ton app crashe en boucle à cause d'un bug, always redémarre en boucle — le serveur se sature et tu ne peux plus diagnostiquer.
Logs bornés :
logging:
driver: "json-file"
options:
max-size: "10m" # 10MB par fichier
max-file: "5" # 5 fichiers max = 50MB total
Sans ça, les logs grossissent indéfiniment. Sur un VPS avec 20GB de disque, ils peuvent tout saturer en quelques semaines — et quand le disque est plein, tout crashe.
La checklist — à garder ouverte à chaque déploiement
# Zone 1 — Health Checks
docker compose config | grep healthcheck
docker compose ps # → tous "(healthy)"
# Zone 2 — Volumes
docker volume ls # → volume nommé présent
# Zone 3 — Secrets
git log --all -- .env # → aucun résultat
docker compose config | grep POSTGRES_PASSWORD # → vraie valeur, pas ${VAR}
# Zone 4 — Nginx
docker exec nginx nginx -t # → "syntax is ok"
grep "X-Real-IP" nginx.conf # → présent
# Zone 5 — Restart & Logs
docker compose config | grep restart # → "unless-stopped" partout
docker compose config | grep max-size # → "10m" partout
La checklist complète avec toutes les commandes de vérification est disponible dans le repo GitHub : PRODUCTION.md
Conclusion
La différence entre un environnement de dev et un environnement de production se résume souvent à ces 5 zones. Les corriger toutes d'un coup, dès le départ, c'est exactement ce qui différencie un livrable junior d'un livrable senior.
Un Compose qui tourne en local, c'est un prototype.
Un Compose qui passe la checklist, c'est un livrable professionnel.
📎 Ressources
- Repo GitHub complet : https://github.com/Bordley/docker-compose-production-checklist.git
- Documentation officielle Docker : docs.docker.com/compose/compose-file
- 12-factor app methodology : 12factor.net
Par **Bordley* — IT Manager & DevOps Engineer, Cotonou, Bénin 🇧🇯*
Contenu DevOps en français pour les ingénieurs africains et les équipes internationales.
Top comments (0)