Changer le thème

Déploiement Docker Compose en production : healthcheck, redémarrage et journaux

Easton editorial illustration: observability control panel

Les alertes SMS s’enchaînent sur le téléphone. Vous ouvrez le terminal : le disque est à 99 % — les journaux des conteneurs occupent 50 Go.

Ce n’est pas le pire scénario. L’an dernier, sur un projet, le conteneur API affichait « running », mais la connexion à la base était coupée depuis longtemps ; toutes les requêtes renvoyaient 500. Trois heures de diagnostic pour trouver la cause. Selon une étude Last9, une « mort apparente » de conteneur coûte en moyenne 3,2 heures de investigation.

Beaucoup d’équipes déploient Docker Compose en production pour la première fois avec seulement un mapping de ports et des volumes, puis lancent les conteneurs. Pas de healthcheck, pas de rotation des journaux, et un restart: always au hasard. Résultat : le conteneur semble tourner alors qu’il est déjà mort ; les fichiers de log grossissent jusqu’à saturer le disque ; un service en crash redémarre en boucle et épuise CPU et mémoire.

Cet article détaille les trois réglages essentiels en production — healthcheck, politique de redémarrage, gestion des journaux — avec des exemples, des commandes de vérification par type de service, des étapes de dépannage et un modèle docker-compose.yml prêt à copier.

Healthcheck — s’assurer que le conteneur est vraiment vivant

Un statut running ne prouve pas que l’application fonctionne. Base injoignable, port non écouté, processus bloqué : Docker ne le détecte pas seul. Le healthcheck agit comme un « moniteur de pouls » qui vérifie périodiquement que l’application répond encore correctement.

Syntaxe de configuration

Dans docker-compose.yml, un bloc healthcheck ressemble à ceci :

healthcheck:
  test: ["CMD-SHELL", "pg_isready -U postgres"]
  interval: 10s      # vérification toutes les 10 secondes
  timeout: 5s        # chaque test attend au plus 5 secondes
  retries: 5         # 5 échecs consécutifs avant unhealthy
  start_period: 30s  # 30 secondes de grâce au démarrage

Ces paramètres doivent être cohérents. timeout ne doit pas dépasser interval, sinon la vérification suivante démarre avant la fin de la précédente. Ne supprimez pas start_period : une base met du temps à démarrer ; un délai trop court provoque des faux positifs.

Commandes de healthcheck par service courant

Chaque service se vérifie différemment. Exemples fréquents :

PostgreSQL

healthcheck:
  test: ["CMD-SHELL", "pg_isready -U postgres -d mydb"]
  interval: 10s
  timeout: 5s
  retries: 5
  start_period: 30s

pg_isready est l’outil natif PostgreSQL pour savoir si la base accepte les connexions.

MySQL / MariaDB

healthcheck:
  test: ["CMD-SHELL", "mysqladmin ping -h localhost -u root -p$$MYSQL_ROOT_PASSWORD"]
  interval: 10s
  timeout: 5s
  retries: 5
  start_period: 30s

Échappez le mot de passe avec $$, sinon YAML interprète $ comme une variable.

Redis

healthcheck:
  test: ["CMD-SHELL", "redis-cli ping | grep PONG"]
  interval: 10s
  timeout: 3s
  retries: 3

La commande ping de Redis renvoie PONG ; grep confirme le résultat attendu.

Serveur Web (vérification HTTP)

healthcheck:
  test: ["CMD-SHELL", "curl -f http://localhost:8080/health || exit 1"]
  interval: 30s
  timeout: 10s
  retries: 3
  start_period: 10s

L’option -f fait échouer curl si le code HTTP n’est pas 2xx, ce qui déclenche l’échec du healthcheck.

Piège : les images Alpine n’ont souvent pas curl. Installez-le (apk add curl) ou utilisez wget :

test: ["CMD-SHELL", "wget --no-verbose --tries=1 --spider http://localhost:8080/health || exit 1"]

Contrôle de l’ordre de démarrage

La base n’est pas prête, l’API démarre déjà, échec de connexion, crash — on l’a tous vu. depends_on avec condition: service_healthy règle le problème :

services:
  postgres:
    image: postgres:16
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 30s

  api:
    build: ./api
    depends_on:
      postgres:
        condition: service_healthy  # l'API démarre quand postgres est healthy

Docker Compose attend que postgres soit healthy avant de lancer api. Fini le « la base n’est pas prête et l’API se connecte quand même ».

Politique de redémarrage — récupération maîtrisée après échec

Un conteneur planté, on veut le relancer automatiquement. Mais si la cause profonde reste, le redémarrage en boucle gaspille CPU et mémoire et masque la panne réelle.

Syntaxe de configuration

La politique se configure dans le bloc deploy :

deploy:
  restart_policy:
    condition: on-failure   # redémarrer seulement en cas d'échec
    delay: 5s               # attendre 5 s avant de redémarrer
    max_attempts: 3         # au plus 3 tentatives
    window: 120s            # succès si stable 120 s après redémarrage

condition propose trois valeurs :

  • none : pas de redémarrage automatique
  • on-failure : redémarrage si sortie anormale (code ≠ 0)
  • any : redémarrage dans tous les cas

Recommandation en production

En production, préférez on-failure à always.

Pourquoi ? restart: always relance le conteneur quelle que soit la cause : bug applicatif, base injoignable, mauvaise config. Boucle de crash, journaux saturés, CPU épuisé.

Avec on-failure et max_attempts, Docker s’arrête après quelques tentatives. L’exploitation voit le conteneur arrêté et peut traiter la cause racine.

Réglage des paramètres

delay est l’intervalle entre tentatives. Trop court : le conteneur redémarre avant un nettoyage complet ; trop long : récupération lente. Souvent 5 à 10 secondes conviennent.

window est souvent négligé : durée pendant laquelle le conteneur doit rester stable pour compter le redémarrage comme réussi. Avec window: 120s, un nouveau crash dans les 120 s ne réinitialise pas le compteur max_attempts, ce qui évite de compter comme « réussi » un service qui replante aussitôt.

Healthcheck et redémarrage ensemble

Les deux mécanismes travaillent en chaîne :

  1. Échecs consécutifs du healthcheck (retries) → conteneur unhealthy
  2. Avec restart_policy, Docker tente un redémarrage
  3. Le healthcheck repart à zéro après redémarrage
  4. Si le healthcheck passe, retour à la normale ; sinon nouvelles tentatives jusqu’à max_attempts

Vous obtenez une récupération automatique limitée, sans boucle infinie.

Gestion des journaux — éviter de saturer le disque

L’alerte à 3 h du matin, disque à 99 %, 50 Go de logs — ça m’est arrivé plus d’une fois. Le driver json-file par défaut ne purge pas les anciens fichiers ; sans rotation, le disque finit par être plein.

Configuration de la rotation

Ajoutez un bloc logging dans docker-compose.yml :

logging:
  driver: "json-file"
  options:
    max-size: "10m"      # 10 Mo max. par fichier
    max-file: "3"        # 3 fichiers conservés
    compress: "true"     # compression des anciens fichiers

Au total, environ 30 Mo (10 Mo × 3). Au-delà de 10 Mo, Docker crée un nouveau fichier ; au-delà de 3 fichiers, le plus ancien est supprimé ou compressé.

Les fichiers sont sous /var/lib/docker/containers/<container-id>/<container-id>-json.log. Vérifiez l’occupation avec :

du -sh /var/lib/docker/containers/*/*-json.log

Choix du driver

Docker propose json-file, syslog, fluentd, journald, local, etc. Pour la plupart des cas, json-file ou local suffit.

La documentation officielle indique que local est plus efficace que json-file et gère la rotation sans max-size/max-file. Pour des volumes énormes (dizaines de Go par jour), envisagez local :

logging:
  driver: "local"

Inconvénient : docker logs ne lit pas directement les journaux local ; il faut souvent mode: "non-blocking" dans la config pour la compatibilité.

Collecte centralisée (optionnel)

En mono-serveur, json-file ou local suffit. Avec des dizaines de machines et des centaines de conteneurs, une solution centralisée aide :

  • Fluentd : collecte légère, adaptée aux petits clusters
  • ELK Stack (Elasticsearch + Logstash + Kibana) : puissant, coût de déploiement élevé
  • Loki + Grafana : approche cloud native, bonne intégration Prometheus

La configuration détaillée dépasse ce article. Exemple d’idée avec Fluentd :

logging:
  driver: "fluentd"
  options:
    fluentd-address: "localhost:24224"
    tag: "docker.{{.Name}}"

Fluentd envoie les journaux vers l’adresse configurée pour analyse centralisée.

Modèle de configuration complet

En combinant healthcheck, redémarrage et journaux, vous obtenez un docker-compose.yml de niveau production. Exemple avec PostgreSQL, Redis et une API :

version: '3.8'

services:
  postgres:
    image: postgres:16
    environment:
      POSTGRES_USER: myuser
      POSTGRES_PASSWORD: mypassword
      POSTGRES_DB: mydb
    volumes:
      - postgres_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U myuser -d mydb"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 30s
    deploy:
      restart_policy:
        condition: on-failure
        delay: 5s
        max_attempts: 3
        window: 120s
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "3"
        compress: "true"

  redis:
    image: redis:7-alpine
    healthcheck:
      test: ["CMD-SHELL", "redis-cli ping | grep PONG"]
      interval: 10s
      timeout: 3s
      retries: 3
      start_period: 5s
    deploy:
      restart_policy:
        condition: on-failure
        delay: 5s
        max_attempts: 3
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "3"

  api:
    build:
      context: ./api
      dockerfile: Dockerfile
    ports:
      - "8080:8080"
    environment:
      DATABASE_URL: postgres://myuser:mypassword@postgres:5432/mydb
      REDIS_URL: redis://redis:6379
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy
    healthcheck:
      test: ["CMD-SHELL", "curl -f http://localhost:8080/health || exit 1"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 10s
    deploy:
      restart_policy:
        condition: on-failure
        delay: 10s
        max_attempts: 3
        window: 120s
    logging:
      driver: "json-file"
      options:
        max-size: "50m"
        max-file: "5"
        compress: "true"

volumes:
  postgres_data:

Points clés du modèle

Ordre de démarrage : l’API attend que postgres et redis passent en healthy. Base et cache prêts avant les connexions de l’application.

Taille des journaux : postgres et redis : 10 Mo × 3 en général ; API plus verbeuse : 50 Mo × 5. Ajustez selon le volume réel.

Délai de redémarrage : postgres lent au boot → delay 5 s ; API plus rapide → delay 10 s pour laisser le healthcheck respirer.

start_period : postgres 30 s pour l’init ; redis 5 s ; API 10 s selon le temps de boot habituel.

Copiez le modèle, adaptez variables et images. Pour MongoDB, MinIO, etc., appliquez le même trio healthcheck / redémarrage / journaux.

Pièges courants et dépannage

Même avec une bonne config, des problèmes peuvent survenir. Pièges fréquents et pistes de diagnostic.

Healthcheck en échec permanent

Symptôme : statut unhealthy alors que l’application semble OK.

Étapes :

  1. Vérifiez la présence des outils :

    docker exec <container> which curl
    docker exec <container> which pg_isready

    Sur Alpine, installez curl ou passez à wget.

  2. Exécutez la commande du healthcheck à la main :

    docker exec <container> curl -f http://localhost:8080/health

    Une erreur peut venir de l’endpoint /health lui-même.

  3. Détail de l’état :

    docker inspect --format='{{json .State.Health}}' <container> | jq

    Historique des tests, causes et horodatages.

Redémarrages en boucle

Symptôme : le conteneur redémarre en permanence, journaux pleins de relances.

Étapes :

  1. Code de sortie et message d’erreur :

    docker inspect --format='{{.State.ExitCode}}' <container>
    docker inspect --format='{{.State.Error}}' <container>

    Codes utiles : 1 = erreur générale, 137 = OOM, 139 = segmentation fault.

  2. Nombre de redémarrages :

    docker inspect --format='{{.RestartCount}}' <container>

    Si le compteur est très élevé, vérifiez que max_attempts s’applique.

  3. Journaux récents :

    docker logs --tail 100 <container>

Disque plein à cause des journaux

Symptôme : alerte espace disque, gros répertoire /var/lib/docker/containers.

Étapes :

  1. Fichiers les plus volumineux :

    du -sh /var/lib/docker/containers/*/*-json.log | sort -rh | head -5
  2. Rotation effective ou non :

    docker inspect --format='{{.HostConfig.LogConfig}}' <container>

    Config: {} signifie souvent l’absence de rotation.

  3. Vidage temporaire (dépannage uniquement) :

    truncate -s 0 /var/lib/docker/containers/<id>/<id>-json.log

    À long terme, configurez la rotation dans compose.

Commandes de diagnostic rapide

# État de santé de tous les conteneurs
docker ps --format "table {{.Names}}\t{{.Status}}"

# Historique du healthcheck d'un conteneur
docker inspect --format='{{json .State.Health}}' <container>

# Code de sortie et nombre de redémarrages
docker inspect --format='ExitCode: {{.State.ExitCode}}, RestartCount: {{.RestartCount}}' <container>

# Taille des fichiers de journaux
du -sh /var/lib/docker/containers/*/*-json.log | sort -rh

# 100 dernières lignes de journaux
docker logs --tail 100 <container>

Synthèse

En production, ces trois réglages ne sont pas optionnels : le healthcheck évite un conteneur « qui semble vivant », la politique de redémarrage permet une récupération limitée, la gestion des journaux protège le disque.

3,2 heures
temps moyen de diagnostic d’une mort apparente de conteneur

Liste des réglages essentiels :

  • Healthcheck : test + interval + timeout + retries + start_period
  • Redémarrage : condition: on-failure + max_attempts: 3
  • Rotation des journaux : max-size: 10m + max-file: 3 + compress: true

Plan en trois étapes :

  1. Ouvrez votre docker-compose.yml actuel : ajoutez au minimum healthcheck et rotation des journaux s’ils manquent.
  2. Déployez un service de test avec le modèle ci-dessus ; vérifiez healthcheck et rotation.
  3. Gardez les commandes de dépannage sous la main pour la prochaine alerte nocturne.

Ne laissez pas vos conteneurs « à nu » en production. Avec ces trois protections, vous gagnez récupération automatique, diagnostic plus rapide et disque préservé.

FAQ

Comment régler interval et timeout du healthcheck de façon raisonnable ?
interval : 10 à 30 secondes ; timeout : 3 à 10 secondes. Le timeout ne doit pas dépasser interval, sinon la prochaine vérification démarre avant la fin de la précédente. Pour une base de données, prévoyez un start_period plus long (30-60 s) pour l'initialisation.
restart: always ou on-failure en production ?
En production, on-failure avec max_attempts est recommandé. always redémarre dans tous les cas, y compris erreur de config ou bug, ce qui provoque des boucles de crash. on-failure ne redémarre qu'en cas de sortie anormale, avec un plafond de tentatives pour alerter l'exploitation.
Quelle taille et combien de fichiers de journaux conserver ?
Pour un service courant : max-size: 10m et max-file: 3, soit environ 30 Mo au total. Pour un gros volume (API), 50m × 5 convient. Ajustez selon le volume réel et activez compress: true pour gagner de l'espace.
Le healthcheck échoue en boucle alors que l'application semble OK : que faire ?
Vérifiez que les outils de la commande existent (curl, pg_isready) ; les images Alpine en manquent souvent. Exécutez la commande à la main, puis docker inspect pour l'historique du healthcheck et la cause précise.
À quoi sert depends_on avec condition: service_healthy ?
Le conteneur ne démarre qu'après le passage du healthcheck du service dépendant. Plus fiable qu'un simple depends_on : évite qu'une API se connecte avant que la base soit prête. Le service dépendant doit avoir un healthcheck configuré.
Comment voir rapidement l'espace disque pris par les journaux des conteneurs ?
Commande : du -sh /var/lib/docker/containers/*/*-json.log | sort -rh. Si un fichier est anormalement gros, vérifiez que la rotation des journaux est bien appliquée sur ce conteneur.

10 min de lecture · Publié le: 12 avr. 2026 · Mis à jour le: 27 juil. 2026

Commentaires

Connectez-vous avec GitHub pour laisser un commentaire

Easton BlogEaston Blog