DEV Community

Cover image for Docker Compose en production : les 5 erreurs que personne ne te dit
Roméo DOSsOu
Roméo DOSsOu

Posted on

Docker Compose en production : les 5 erreurs que personne ne te dit

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

La cause racine

volumes:
  - ./postgres-data:/var/lib/postgresql/data
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

La correction

services:
  db:
    volumes:
      - pgdata:/var/lib/postgresql/data    # volume nommé

volumes:
  pgdata:
    driver: local    # Docker gère l'emplacement
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

💣 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.
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
# docker-compose.yml — référencer, jamais écrire en dur
environment:
  POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}    # lu depuis .env
Enter fullscreen mode Exit fullscreen mode
# Vérifier avant de démarrer
docker compose config
# Toutes les ${VARIABLES} doivent être remplacées par de vraies valeurs
Enter fullscreen mode Exit fullscreen mode

💣 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 secondesproxy_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";
    }
}
Enter fullscreen mode Exit fullscreen mode

💣 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
Enter fullscreen mode Exit fullscreen mode

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 ✅
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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


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)