Retour au cours

infra / docker-compose

Dépendances entre services : depends_on et healthchecks

Leçon 61 exercice

Explication

Ce que vous allez apprendre

  • Comprendre pourquoi depends_on seul ne garantit qu'un ordre, jamais une vraie disponibilité
  • Écrire un healthcheck qui teste réellement si un service répond (pg_isready, redis-cli ping)
  • Choisir entre service_started, service_healthy et service_completed_successfully
  • Calibrer start_period, interval et retries pour éviter les faux "unhealthy"
  • Faire attendre un service qu'un job ponctuel (migration) se termine avec succès

Dans quel contexte ?

Sur le projet boutique-api, le service api échoue systématiquement au tout premier démarrage avec l'erreur "ECONNREFUSED 127.0.0.1:5432", puis fonctionne parfaitement après un simple docker compose restart api. Le service db n'est pas en cause : Postgres est bien "Up", mais il n'a pas encore terminé d'initialiser ses fichiers internes quand l'API tente sa première connexion. C'est exactement ce que cette leçon règle avec un healthcheck.

Un bug très courant en développement

D'abord, un scénario classique : ton API démarre, essaie de se connecter à la base de données immédiatement, et échoue. En relançant juste après, ça marche. Ce comportement étrange a une explication précise.

"Démarré" ne veut pas dire "prêt"

Un conteneur de base de données peut afficher "Up" en quelques millisecondes, alors que le moteur Postgres met encore plusieurs secondes à initialiser ses fichiers et à accepter des connexions. Le conteneur tourne, mais le service à l'intérieur n'est pas encore opérationnel.

Ce que fait vraiment depends_on (et ce qu'il ne fait pas)

Sans condition supplémentaire, depends_on garantit uniquement un ORDRE de démarrage : Docker lance db avant api, mais ne vérifie jamais que db est réellement fonctionnelle. C'est comme allumer le four et enfourner le plat immédiatement, sans attendre qu'il ait atteint la bonne température.

Il reste un problème à résoudre

Un simple ordre de démarrage ne suffit donc pas. Il faut un moyen de savoir si un service est VRAIMENT prêt à recevoir des connexions, pas juste "démarré".

La solution : le healthcheck

Un healthcheck est une commande que Docker exécute périodiquement à l'intérieur du conteneur pour juger s'il est réellement fonctionnel — par exemple pg_isready pour Postgres. C'est un test de santé répété, pas une simple vérification ponctuelle au démarrage.

Combiner les deux : depends_on + condition

En combinant depends_on avec condition: service_healthy, on force Compose à attendre que ce test de santé réussisse avant de démarrer le service suivant. Pour reprendre l'image du four : il attend maintenant d'avoir atteint sa température avant qu'on y mette le plat.

Condition depends_onCe qu'elle vérifieCas d'usage
service_startedLe conteneur a démarré (comportement historique)Dépendance sans vérification fine nécessaire
service_healthyLe healthcheck renvoie un statut OKBase de données, cache, API interne
service_completed_successfullyLe service s'est terminé avec un exit code 0Job ponctuel : migration, seed de données

Bonne pratique

Choisis toujours un healthcheck qui teste une vraie capacité fonctionnelle, comme pg_isready pour Postgres, plutôt qu'un simple ps qui ne vérifie que la présence du process. Un process peut tourner sans être capable de répondre à une seule requête.

Une troisième condition, pour un cas particulier

Il existe une troisième option, service_completed_successfully, très différente des deux autres : elle attend qu'un service se TERMINE avec succès, plutôt que de tourner en continu. C'est utile pour des jobs ponctuels comme une migration de base de données, qu'on reverra dans une leçon dédiée.

Les pièges à connaître

Un start_period trop court sur un healthcheck déclenche de faux "unhealthy" pendant que le service s'initialise encore normalement. Et service_started, le comportement historique, ne protège de rien côté disponibilité réelle : garde-le seulement pour des dépendances qui n'ont pas besoin de vérification fine.

Piège fréquent

Un start_period de 2 secondes sur le healthcheck d'une base Postgres qui met 8 secondes à s'initialiser au premier démarrage marque le service "unhealthy" à tort, ce qui bloque tous les services qui en dépendent avec service_healthy. Mesure le temps de démarrage réel du service avant de fixer start_period.

La leçon suivante s'appuie sur ces mêmes idées d'isolation, mais côté réseau plutôt que côté démarrage.

Commandes & code

depends_on & healthchecks

depends_on sans condition ne garantit que l'ORDRE de démarrage, pas que le service dépendant soit VRAIMENT prêt.

yaml
services:
  api:
    build: ./api
    depends_on:
      db:
        condition: service_healthy    # attend que le healthcheck de "db" soit "healthy"
      cache:
        condition: service_started    # attend juste que le conteneur démarre
    ports:
      - "3000:3000"

  db:
    image: postgres:16
    environment:
      POSTGRES_PASSWORD: changeme
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 5s
      timeout: 3s
      retries: 5
      start_period: 10s      # laisse le temps à l'init avant de compter les échecs

  cache:
    image: redis:7-alpine
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 3s
      retries: 3
bash
# Voir l'état de santé des conteneurs (healthy / unhealthy / starting)
docker compose ps

docker inspect --format='{{json .State.Health}}' $(docker compose ps -q db)
yaml
# Cas concret : une API qui ne doit démarrer qu'après une migration réussie
services:
  migrate:
    build: ./api
    command: ["npm", "run", "migrate"]
    depends_on:
      db:
        condition: service_healthy

  api:
    build: ./api
    depends_on:
      migrate:
        condition: service_completed_successfully   # attend un exit code 0
    ports:
      - "3000:3000"

Résumé

  • condition: service_started : ordre seulement (comportement historique).
  • condition: service_healthy : attend un healthcheck OK.
  • condition: service_completed_successfully : utile pour les jobs one-shot (migrations, seed).
  • Un healthcheck mal calibré (start_period trop court) déclenche des faux unhealthy.

Exercices pratiques

1 disponible
1

Mission : l'API qui échoue une fois sur deux au démarrage

Objectif : Remplacer un depends_on naïf par un healthcheck fiable et calibrer son start_period.

Contexte

Sur boutique-api, le service api déclare déjà depends_on: - db (syntaxe courte, sans condition), mais échoue encore parfois au tout premier démarrage avec "ECONNREFUSED 127.0.0.1:5432", alors que docker compose ps montrait db comme "Up". Le moteur Postgres met environ 8 secondes à s'initialiser complètement au premier démarrage.

Corrige la configuration pour que l'API attende une vraie disponibilité de la base.

Résoudre l’exercice →