Docker Compose en production : bonnes pratiques

Docker Compose en production : bonnes pratiques

Déployez vos applications avec Docker Compose de manière fiable en production. Ce guide couvre les networks, volumes nommés, healthchecks, restart policies, secrets et la gestion des logs pour un setup qui survit aux reboots et aux pannes.

Introduction

Docker Compose est parfait pour de petites à moyennes infrastructures (jusqu'à ~10-20 conteneurs sur un VPS). Mais en production, on n'utilise pas un docker-compose comme en dev. Il faut prévoir :

  • Healthchecks pour la reprise auto en cas de crash
  • Restart policies pour redémarrer après reboot
  • Volumes nommés (pas de bind mounts à l'aveugle)
  • Networks isolés
  • Gestion des secrets
  • Logs limités

Ce guide vous donne le template prod que vous pouvez réutiliser.

Prérequis

Étape 1 : Vérifier Docker

docker --version
docker compose version

Docker Compose v2 (docker compose, pas docker-compose) est requis.

Étape 2 : Structure de projet recommandée

Pour chaque app, organisez :

/opt/myapp/
├── docker-compose.yml
├── .env                  # variables d'env (gitignore)
├── .env.example          # template public
├── secrets/              # secrets sensibles (gitignore)
│   ├── db_password.txt
│   └── api_key.txt
├── data/                 # bind mounts si nécessaire
│   └── ...
└── config/               # configs personnalisées
    └── nginx.conf

Permissions :

sudo mkdir -p /opt/myapp/{secrets,data,config}
sudo chmod 700 /opt/myapp/secrets

Étape 3 : Template docker-compose.yml de production

# /opt/myapp/docker-compose.yml

services:
  app:
    image: monapp:1.2.3       # ⚠️ pin la version exacte, pas "latest"
    container_name: myapp
    restart: unless-stopped
    ports:
      - "127.0.0.1:3000:3000" # Bind sur localhost uniquement, exposé via Nginx
    environment:
      - NODE_ENV=production
      - DATABASE_URL=postgresql://app:${DB_PASSWORD}@db:5432/myapp
    secrets:
      - db_password
    volumes:
      - app_data:/data
      - ./config/app.conf:/etc/app/app.conf:ro
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_healthy
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
      interval: 30s
      timeout: 5s
      retries: 3
      start_period: 30s
    networks:
      - frontend
      - backend
    logging:
      driver: json-file
      options:
        max-size: "10m"
        max-file: "3"
    deploy:
      resources:
        limits:
          memory: 1G
          cpus: '1.0'

  db:
    image: postgres:16-alpine
    container_name: myapp-db
    restart: unless-stopped
    environment:
      - POSTGRES_DB=myapp
      - POSTGRES_USER=app
      - POSTGRES_PASSWORD_FILE=/run/secrets/db_password
    secrets:
      - db_password
    volumes:
      - db_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U app"]
      interval: 10s
      timeout: 5s
      retries: 5
    networks:
      - backend
    logging:
      driver: json-file
      options:
        max-size: "10m"
        max-file: "3"

  redis:
    image: redis:7-alpine
    container_name: myapp-redis
    restart: unless-stopped
    command: redis-server --requirepass ${REDIS_PASSWORD}
    volumes:
      - redis_data:/data
    healthcheck:
      test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD}", "ping"]
      interval: 10s
      timeout: 3s
      retries: 5
    networks:
      - backend
    logging:
      driver: json-file
      options:
        max-size: "10m"
        max-file: "3"

volumes:
  app_data:
    driver: local
  db_data:
    driver: local
  redis_data:
    driver: local

networks:
  frontend:
    driver: bridge
  backend:
    driver: bridge
    internal: true   # Pas d'accès Internet pour ce network

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

Points clés :

  • restart: unless-stopped : redémarre auto sauf si arrêté manuellement
  • Images pinnées : pas de latest, toujours 1.2.3
  • Healthchecks sur tous les services critiques
  • Volumes nommés : portable, ne pollue pas le système
  • Networks séparés : frontend (web visible) vs backend (DB isolée)
  • Logs limités : 10 MB × 3 fichiers max par container
  • Resources : caps CPU/RAM pour éviter qu'un container saturé bouffe tout
  • Secrets : mots de passe dans des fichiers, pas dans environment

Étape 4 : Fichier .env

sudo nano /opt/myapp/.env
# Database
DB_PASSWORD=will_be_overridden_by_secret_file

# Redis
REDIS_PASSWORD=UnMotDePasseLongRedis_42

# App
APP_PORT=3000

Variables réutilisables avec ${VAR} dans docker-compose.yml.

sudo chmod 600 /opt/myapp/.env

Étape 5 : Secrets via fichiers

echo "UnMotDePasseLongBDD_2024" | sudo tee /opt/myapp/secrets/db_password.txt
sudo chmod 600 /opt/myapp/secrets/db_password.txt

Le container y accède via /run/secrets/db_password et l'environnement avec POSTGRES_PASSWORD_FILE=/run/secrets/db_password.

Avantage vs environment: POSTGRES_PASSWORD=... :

  • Pas visible dans docker inspect
  • Pas exporté dans les logs/env du process

Étape 6 : Démarrer la stack

cd /opt/myapp
sudo docker compose up -d

Vérifiez :

sudo docker compose ps
sudo docker compose logs -f

État attendu :

NAME            STATUS              PORTS
myapp           Up (healthy)        127.0.0.1:3000->3000/tcp
myapp-db        Up (healthy)
myapp-redis     Up (healthy)

Étape 7 : Healthchecks expliqués

Le healthcheck permet à Docker de savoir si un container va bien. Format :

healthcheck:
  test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
  interval: 30s    # Fréquence des checks
  timeout: 5s      # Temps max pour répondre
  retries: 3       # Tentatives avant de marquer "unhealthy"
  start_period: 30s # Période de grâce au démarrage

depends_on: condition: service_healthy attend que le check passe avant de démarrer l'app dépendante (ex: app attend que la DB soit ready).

Étape 8 : Backup automatique des volumes

Les volumes Docker sont stockés dans /var/lib/docker/volumes/. Pour les sauvegarder :

sudo nano /usr/local/bin/backup-docker-volumes.sh
#!/bin/bash
set -e
BACKUP_DIR=/backup/docker
DATE=$(date +%F_%H%M)
mkdir -p $BACKUP_DIR

cd /opt/myapp

# Arrêter brièvement la DB pour un snapshot cohérent (10s max)
docker compose stop db redis

# Backup volumes
for vol in $(docker volume ls -q --filter name=myapp_); do
    docker run --rm \
        -v ${vol}:/source:ro \
        -v ${BACKUP_DIR}:/backup \
        alpine tar -czf /backup/${vol}_${DATE}.tar.gz -C /source .
done

# Redémarrer
docker compose start db redis

# Rotation : garder 7 jours
find $BACKUP_DIR -type f -mtime +7 -delete

echo "Backup OK: $DATE"
sudo chmod +x /usr/local/bin/backup-docker-volumes.sh
sudo crontab -e
0 3 * * * /usr/local/bin/backup-docker-volumes.sh

Pour quelque chose de plus propre, voir le tuto Restic + Backblaze B2.

Étape 9 : Mise à jour propre

cd /opt/myapp

# Pull les nouvelles versions des images
sudo docker compose pull

# Recréer avec nouvelles images
sudo docker compose up -d --remove-orphans

# Nettoyer les vieilles images
sudo docker image prune -f

Pour automatiser, utilisez Watchtower :

  watchtower:
    image: containrrr/watchtower
    restart: unless-stopped
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
    environment:
      - WATCHTOWER_CLEANUP=true
      - WATCHTOWER_SCHEDULE=0 0 4 * * *  # 4h du matin
      - WATCHTOWER_NOTIFICATIONS=email

⚠️ Watchtower met à jour vers la dernière tag. Si vous pinnez à 1.2.3, il n'updatera pas. Si vous voulez du 1.x automatique, utilisez la tag 1 ou 1.2.

Étape 10 : Logs centralisés

Par défaut les logs Docker sont stockés en JSON dans /var/lib/docker/containers/. Pour centraliser :

sudo docker compose logs --tail=100 -f

Pour du multi-services en agrégé, voir le tuto Loki + Promtail.

Étape 11 : Resource limits (très important)

Sans limites, un container qui fuit peut bouffer toute la RAM/CPU :

services:
  app:
    deploy:
      resources:
        limits:
          memory: 1G
          cpus: '1.0'
        reservations:
          memory: 512M
          cpus: '0.5'

⚠️ En mode "compose" (pas swarm), deploy: est partiellement supporté. Préférez :

services:
  app:
    mem_limit: 1g
    cpus: 1.0

Étape 12 : Reverse proxy frontal

Le port 3000 est bindé sur 127.0.0.1 uniquement. Pour exposer en HTTPS, ajoutez Nginx ou Traefik en frontal :

# /etc/nginx/sites-available/myapp
server {
    listen 443 ssl http2;
    server_name app.votre-domaine.com;
    
    ssl_certificate /etc/letsencrypt/live/app.votre-domaine.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/app.votre-domaine.com/privkey.pem;
    
    location / {
        proxy_pass http://127.0.0.1:3000;
        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;
    }
}

Dépannage

Container redémarre en boucle

sudo docker compose logs app

Causes fréquentes :

  • Variable d'env manquante
  • Volume avec mauvaises permissions
  • Healthcheck qui fail au démarrage (augmentez start_period)

"no space left on device"

Nettoyez :

sudo docker system prune -af --volumes

⚠️ Supprime tout ce qui n'est pas utilisé. Soyez sûr avant.

Logs énormes qui remplissent le disque

Vérifiez que logging: est bien configuré dans chaque service. Sinon, les logs Docker peuvent grossir indéfiniment.

Container ne voit pas un autre

Ils doivent être sur le même network. Vérifiez :

sudo docker compose config
sudo docker network inspect myapp_backend

Commandes utiles

# État de la stack
sudo docker compose ps

# Logs
sudo docker compose logs -f service_name

# Restart un service
sudo docker compose restart app

# Re-build et redémarrer
sudo docker compose up -d --force-recreate

# Stop sans supprimer
sudo docker compose stop

# Stop + remove containers (mais garde volumes)
sudo docker compose down

# Stop + remove TOUT (incluant volumes ⚠️)
sudo docker compose down -v

# Exec dans un container
sudo docker compose exec app sh

# Inspect un service
sudo docker compose config

# Voir les ressources
sudo docker stats

# Networks
sudo docker network ls

# Volumes
sudo docker volume ls
sudo docker volume inspect myapp_db_data

Conclusion

Avec ce template, votre app est :

  • Résiliente : redémarre automatiquement en cas de crash
  • Sécurisée : secrets isolés, backend non exposé
  • Maintenable : volumes nommés, networks séparés
  • Observée : healthchecks, logs limités

Pour aller plus loin :

  • Migrez vers Docker Swarm ou Kubernetes quand vous avez 10+ services à orchestrer
  • Utilisez Portainer ou Komodo pour gérer plusieurs stacks via UI
  • Mettez en place Watchtower pour les updates auto contrôlés

Ressources

Join our Discord community server

For any questions, suggestions, or just to chat with the community, join us on Discord!

900+Members