Exécuter des applications multi-conteneurs en production nécessite bien plus qu’un simple fichier docker-compose.yml basique. Bien que Docker Compose soit couramment associé au développement local, c’est un outil puissant pour déployer des charges de travail en production dans des environnements mono-hôte et de petits clusters lorsqu’il est correctement configuré. Ce guide vous accompagne pas à pas pour renforcer votre configuration Docker Compose en production avec des health checks, des politiques de redémarrage, des limites de ressources, une gestion des secrets, un logging centralisé et un proxy inverse Nginx avec terminaison SSL.

Prérequis

  • Serveur Linux avec Docker Engine 24+ et Docker Compose v2 installés
  • Nom de domaine pointant vers l’adresse IP publique de votre serveur
  • Familiarité de base avec les concepts Docker (images, conteneurs, volumes, réseaux)
  • Certificat SSL ou volonté d’utiliser Let’s Encrypt avec Certbot
  • Accès SSH à votre serveur de production
  • Au moins 2 Go de RAM et 2 cœurs CPU disponibles pour votre stack

Configuration Docker Compose pour la Production

Un docker-compose.yml prêt pour la production diffère significativement d’une configuration de développement. Vous avez besoin de politiques de redémarrage explicites, de health checks, de contraintes de ressources et de systèmes de fichiers en lecture seule lorsque c’est possible.

Politiques de Redémarrage

Les politiques de redémarrage garantissent que vos conteneurs se remettent automatiquement des crashes, des OOM kills ou des redémarrages du serveur hôte:

services:
  web:
    image: myapp:latest
    restart: unless-stopped
    deploy:
      restart_policy:
        condition: on-failure
        delay: 5s
        max_attempts: 3
        window: 120s

Utilisez unless-stopped pour la plupart des services. Cela redémarre les conteneurs après les crashes et les redémarrages du serveur, mais respecte les arrêts manuels. Pour les services critiques qui doivent toujours fonctionner, utilisez always. Évitez no en production — cela laisse les conteneurs crashés inactifs jusqu’à une intervention manuelle.

Health Checks

Les health checks permettent à Docker de surveiller si votre application fonctionne réellement, pas seulement si le processus est en cours d’exécution:

services:
  api:
    image: myapp-api:latest
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 40s

Le start_period donne à votre application le temps de s’initialiser avant que Docker ne commence à compter les vérifications échouées. Configurez-le plus haut que le temps de démarrage moyen de votre application. Utilisez des endpoints de santé spécifiques plutôt que de vérifier si un port est ouvert — un processus peut écouter sur un port tout en étant dans un état défaillant.

Limites de Ressources

Sans limites de ressources, un seul conteneur incontrôlé peut consommer toute la mémoire du système et tout faire tomber:

services:
  api:
    image: myapp-api:latest
    deploy:
      resources:
        limits:
          cpus: "1.0"
          memory: 512M
        reservations:
          cpus: "0.25"
          memory: 128M

Définissez limits au maximum qu’un service devrait utiliser et reservations pour garantir des ressources minimales. Surveillez vos conteneurs avec docker stats pendant une semaine avant de fixer les limites définitives. Des limites trop strictes provoquent des OOM kills et des performances dégradées.

Systèmes de Fichiers en Lecture Seule

Minimisez la surface d’attaque en exécutant les conteneurs avec des systèmes de fichiers racine en lecture seule:

services:
  api:
    image: myapp-api:latest
    read_only: true
    tmpfs:
      - /tmp
      - /var/run
    volumes:
      - app-data:/app/data

Cela empêche les processus malveillants d’écrire dans le système de fichiers du conteneur. Utilisez tmpfs pour les répertoires nécessitant un accès temporaire en écriture et des volumes nommés pour les données persistantes.

Gestion des Secrets et Variables d’Environnement

Les identifiants codés en dur dans votre fichier compose représentent un risque de sécurité. Docker Compose prend en charge plusieurs approches pour la gestion des secrets.

Utilisation de Fichiers .env

Créez un fichier .env à côté de votre fichier compose:

# .env - Ne JAMAIS commiter ce fichier
POSTGRES_PASSWORD=votre_mot_de_passe_securise
API_SECRET_KEY=autre_valeur_securisee
REDIS_PASSWORD=mot_de_passe_redis

Référencez-les dans votre fichier compose:

services:
  db:
    image: postgres:16
    environment:
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}

Ajoutez .env à votre .gitignore immédiatement. Pour les pipelines CI/CD, injectez les variables d’environnement depuis votre gestionnaire de secrets (GitHub Secrets, AWS Secrets Manager ou HashiCorp Vault).

Docker Secrets

Pour une gestion plus sécurisée, utilisez Docker secrets qui sont montés comme fichiers plutôt que comme variables d’environnement:

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

services:
  db:
    image: postgres:16
    secrets:
      - db_password
    environment:
      POSTGRES_PASSWORD_FILE: /run/secrets/db_password

De nombreuses images Docker officielles prennent en charge la convention du suffixe _FILE, lisant le secret depuis un fichier monté au lieu d’une variable d’environnement. Cela garde les identifiants hors de la sortie de docker inspect et des listes de variables d’environnement du processus.

Proxy Inverse et SSL avec Nginx

Exposer les ports d’application directement est non sécurisé et peu flexible. Utilisez Nginx comme proxy inverse avec terminaison SSL:

services:
  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
      - ./nginx/conf.d:/etc/nginx/conf.d:ro
      - certbot-webroot:/var/www/certbot:ro
      - certbot-certs:/etc/letsencrypt:ro
    depends_on:
      api:
        condition: service_healthy
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost/health"]
      interval: 30s
      timeout: 5s
      retries: 3

  certbot:
    image: certbot/certbot
    volumes:
      - certbot-webroot:/var/www/certbot
      - certbot-certs:/etc/letsencrypt
    entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew; sleep 12h & wait $${!}; done'"

volumes:
  certbot-webroot:
  certbot-certs:

La configuration Nginx doit rediriger vers vos services internes:

upstream api_backend {
    server api:8080;
}

server {
    listen 80;
    server_name example.com;

    location /.well-known/acme-challenge/ {
        root /var/www/certbot;
    }

    location / {
        return 301 https://$host$request_uri;
    }
}

server {
    listen 443 ssl http2;
    server_name example.com;

    ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

    location / {
        proxy_pass http://api_backend;
        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;
    }
}

Remarquez que seul le service Nginx expose des ports vers l’hôte. Tous les autres services communiquent via le réseau interne Docker, réduisant considérablement la surface d’attaque.

Logging et Monitoring

Les déploiements en production nécessitent un logging structuré avec rotation pour éviter l’épuisement du disque.

Logging JSON File avec Rotation

services:
  api:
    image: myapp-api:latest
    logging:
      driver: json-file
      options:
        max-size: "10m"
        max-file: "5"
        tag: "{{.Name}}"

Sans max-size et max-file, les logs Docker croissent indéfiniment et peuvent remplir votre disque. Le driver json-file est celui par défaut et fonctionne bien pour la plupart des configurations. Pour les environnements multi-hôtes, envisagez de transférer les logs vers un système centralisé.

Transfert vers des Systèmes Externes

Pour l’observabilité en production, transférez les logs vers un service d’agrégation:

services:
  api:
    image: myapp-api:latest
    logging:
      driver: syslog
      options:
        syslog-address: "tcp://logserver:514"
        tag: "myapp-api"

Les alternatives incluent les drivers fluentd et gelf pour les stacks ELK ou Graylog. Quel que soit votre choix, configurez toujours la rotation des logs au niveau du daemon Docker comme filet de sécurité dans /etc/docker/daemon.json:

{
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "10m",
    "max-file": "3"
  }
}

Monitoring de Base

Ajoutez le monitoring des conteneurs avec un stack léger:

services:
  prometheus:
    image: prom/prometheus:latest
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml:ro
      - prometheus-data:/prometheus
    deploy:
      resources:
        limits:
          memory: 256M

  cadvisor:
    image: gcr.io/cadvisor/cadvisor:latest
    volumes:
      - /:/rootfs:ro
      - /var/run:/var/run:ro
      - /sys:/sys:ro
      - /var/lib/docker/:/var/lib/docker:ro
    deploy:
      resources:
        limits:
          memory: 128M

Comparaison: Docker Compose vs Kubernetes vs Docker Swarm

FonctionnalitéDocker ComposeKubernetesDocker Swarm
Complexité de configurationFaibleÉlevéeMoyenne
Support multi-hôteNon (hôte unique)Oui (cluster)Oui (cluster)
Auto-scalingNonOui (HPA)Limité
Découverte de servicesBasé sur DNSDNS + IngressBasé sur DNS
Gestion des secretsBasée sur fichiersIntégrée (etcd)Intégrée
Mises à jour progressivesManuelIntégréeIntégrée
Health checksOuiOui (liveness/readiness)Oui
Limites de ressourcesOuiOui (requests/limits)Oui
Courbe d’apprentissageDouceRaideModérée
Idéal pourHôte unique, petites équipesGrande échelle, multi-équipePetits clusters

Docker Compose est le bon choix lorsque vous déployez sur un serveur unique ou un petit nombre de serveurs, que votre équipe est petite et que vous n’avez pas besoin d’auto-scaling. De nombreux produits SaaS réussis fonctionnent entièrement sur Docker Compose en production.

Scénario Réel

Vous déployez une application web composée d’une API Node.js, d’une base de données PostgreSQL, d’un cache Redis et d’un proxy inverse Nginx. Le serveur dispose de 4 Go de RAM et 2 cœurs CPU.

Voici le fichier compose complet de production:

services:
  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx/conf.d:/etc/nginx/conf.d:ro
      - certbot-certs:/etc/letsencrypt:ro
    depends_on:
      api:
        condition: service_healthy
    restart: unless-stopped
    deploy:
      resources:
        limits:
          cpus: "0.25"
          memory: 64M
    logging:
      driver: json-file
      options:
        max-size: "5m"
        max-file: "3"

  api:
    image: myapp-api:1.2.3
    env_file: .env
    secrets:
      - db_password
      - api_key
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_healthy
    restart: unless-stopped
    read_only: true
    tmpfs:
      - /tmp
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 30s
    deploy:
      resources:
        limits:
          cpus: "1.0"
          memory: 512M
        reservations:
          cpus: "0.25"
          memory: 128M
    logging:
      driver: json-file
      options:
        max-size: "10m"
        max-file: "5"

  db:
    image: postgres:16-alpine
    volumes:
      - pgdata:/var/lib/postgresql/data
    secrets:
      - db_password
    environment:
      POSTGRES_PASSWORD_FILE: /run/secrets/db_password
      POSTGRES_DB: myapp
    restart: unless-stopped
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 10s
      timeout: 5s
      retries: 5
    deploy:
      resources:
        limits:
          cpus: "0.5"
          memory: 1G
        reservations:
          memory: 256M
    logging:
      driver: json-file
      options:
        max-size: "10m"
        max-file: "3"

  redis:
    image: redis:7-alpine
    command: redis-server --requirepass ${REDIS_PASSWORD}
    volumes:
      - redisdata:/data
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD}", "ping"]
      interval: 10s
      timeout: 5s
      retries: 3
    deploy:
      resources:
        limits:
          cpus: "0.25"
          memory: 256M
    logging:
      driver: json-file
      options:
        max-size: "5m"
        max-file: "3"

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

volumes:
  pgdata:
  redisdata:
  certbot-certs:

Déployez avec:

docker compose -f docker-compose.prod.yml up -d
docker compose -f docker-compose.prod.yml ps
docker compose -f docker-compose.prod.yml logs --tail=50

Cette configuration alloue environ 1,8 Go sur les 4 Go disponibles, laissant de la marge pour le système d’exploitation et les pics d’utilisation.

Pièges et Cas Particuliers

L’ordre de démarrage des conteneurs ne garantit pas la disponibilité. Utiliser depends_on seul n’attend que le démarrage du conteneur, pas que le service interne soit prêt. Combinez toujours depends_on avec condition: service_healthy et des health checks appropriés.

Les conteneurs orphelins s’accumulent. Lorsque vous supprimez un service de votre fichier compose, l’ancien conteneur continue de fonctionner. Exécutez toujours docker compose up -d --remove-orphans pour nettoyer les conteneurs obsolètes.

Les permissions de volumes causent des échecs silencieux. Si votre conteneur s’exécute en tant qu’utilisateur non-root, le volume monté peut ne pas être accessible en écriture. Pré-créez les volumes avec les bonnes propriétés ou utilisez un pattern de conteneur init.

Docker Compose ne télécharge pas les nouvelles images par défaut lors de la reconstruction. L’exécution de docker compose up -d réutilise les images en cache. Utilisez docker compose pull && docker compose up -d pour vous assurer de déployer la dernière version.

L’expansion des variables du fichier .env peut générer des conflits. Docker Compose interpole ${VAR} dans le fichier compose avant de le transmettre au conteneur. Si la configuration de votre application utilise des caractères $, échappez-les avec $$ dans le fichier compose.

La rotation des logs doit être configurée par service et globalement. Les options de log au niveau du service ne remplacent pas les valeurs par défaut du daemon pour les autres conteneurs. Configurez les deux pour une couverture complète.

Les réseaux bridge ont des problèmes de résolution DNS avec les underscores. Les noms de service contenant des underscores peuvent ne pas se résoudre correctement dans les anciennes versions de Docker. Utilisez des tirets dans les noms de service pour une compatibilité maximale.

Résumé

  • Utilisez restart: unless-stopped et des health checks sur chaque service de production
  • Définissez des limites de mémoire et CPU avec deploy.resources pour éviter les conteneurs incontrôlés
  • Gérez les secrets avec Docker secrets ou des fichiers .env — ne codez jamais les identifiants en dur
  • Placez Nginx comme proxy inverse devant les services applicatifs avec terminaison SSL
  • Configurez la rotation des logs avec max-size et max-file pour éviter l’épuisement du disque
  • Utilisez depends_on avec condition: service_healthy pour un ordonnancement fiable du démarrage
  • Exécutez docker compose up -d --remove-orphans pour nettoyer les conteneurs obsolètes à chaque déploiement
  • Docker Compose est prêt pour la production pour les déploiements mono-hôte et les petites équipes

Articles Connexes