Il est 09:12. Votre déploiement est « vert » parce que les conteneurs tournent, mais l’API renvoie des 500. Les logs montrent connection refused vers Postgres. Vous ajoutez depends_on, redéployez, et… rien ne change sauf votre niveau de confiance.
Ceci est le piège de dépendance de Docker Compose : depends_on contrôle l’ordre de démarrage, pas la disponibilité. C’est une fonctionnalité de confort, pas un contrat de fiabilité. Si vous la traitez comme telle, votre système vous apprendra l’humilité — généralement pendant une démo.
Ce que depends_on fait réellement (et ce qu’il n’a jamais promis)
Compose doit décider de l’ordre dans lequel il démarre les conteneurs. C’est tout ce que fait depends_on : un graphe orienté qui dit « démarrer A avant B. » Il ne dit pas « A accepte des connexions », « A a terminé les migrations », « A a préchauffé les caches », ou « A ne plantera pas deux secondes plus tard ».
Quand les gens disent « depends_on ne marche pas », ils veulent généralement dire : il a fonctionné exactement comme conçu, et le design n’était pas ce qu’ils supposaient.
Ordre de démarrage vs disponibilité : la distinction franche
- Ordre de démarrage : le processus du conteneur a été lancé (ou du moins Docker a tenté de le lancer).
- Disponibilité : le service est utilisable pour son but (socket à l’écoute, authentification réussie, schéma présent, upstream joignable, etc.).
Ce sont des problèmes séparés, et Compose ne résout que le premier par défaut.
Le détail « service_healthy » (et pourquoi ce n’est pas une solution universelle)
Certaines implémentations de Compose supportent des dépendances conditionnelles comme condition: service_healthy, qui bloquent le démarrage des dépendants sur le healthcheck d’une dépendance. C’est utile, mais ce n’est toujours pas un contrat complet :
- Les healthchecks peuvent être incorrects (trop superficiels, trop lents, trop optimistes).
- Être « healthy » une fois ne signifie pas l’être toujours.
- Votre application a toujours besoin de retries parce que les réseaux et le stockage se moquent de votre YAML.
Voici la vérité opérationnelle : même avec des healthchecks, vous concevez votre application comme si les dépendances pouvaient être en retard, instables ou brièvement indisponibles. Compose vous aide à orchestrer ; il ne vous dédouane pas de la résilience.
Une citation à accrocher au mur : paraphrase de Werner Vogels : « Tout échoue tout le temps ; construisez des systèmes qui s’y attendent. »
Pourquoi « prêt » est difficile : ce qui se passe réellement au démarrage
Sur un laptop propre avec caches chauds et sans charge, il est facile de croire que la disponibilité est instantanée. Dans des environnements réels, le démarrage est un chaos d’E/S, DNS, ordonnancement CPU, latence de stockage, et parfois un fsck surprise que vous n’avez pas demandé.
Phases typiques du démarrage d’une dépendance (les parties que vous oubliez)
- Conteneur créé : couches de système de fichiers montées, namespaces configurés, réseau attaché.
- Entrypoint démarre : le processus commence ; peut fork ; peut attendre des templates de config.
- Service s’initialise : lit les configs, alloue la mémoire, vérifie les permissions.
- Disponibilité du stockage : montages de volumes, replay de journal, récupération après crash, replay WAL.
- Disponibilité réseau : propagation DNS, service se lie aux sockets, règles de firewall.
- Disponibilité applicative : migrations, préchauffage de cache, seed, élection du leader.
Chacune de ces étapes peut retarder le « prêt » de millisecondes à minutes. Et oui, j’ai vu des minutes.
Blague #1 : « It worked on my machine » est juste une autre façon de dire « ma machine a des exigences plus basses. »
Le stockage aggrave le problème (surtout au premier démarrage)
Les bases de données ne sont pas « up » quand le processus existe ; elles le sont quand elles peuvent accepter une connexion et exécuter une requête de façon fiable. Postgres peut rejouer le WAL. MySQL peut mettre à jour des tables système. Redis peut charger un snapshot RDB. Si vous utilisez du stockage réseau, vous ajoutez une couche supplémentaire de variabilité temporelle.
Pour les SRE, l’essentiel est de modéliser la disponibilité des dépendances comme stochastique, pas déterministe. Votre application gère cette réalité de façon élégante, ou elle devient un pager.
Faits et contexte historique (parce que ce n’est pas arrivé par hasard)
Un peu de contexte aide parce que le comportement de Compose est souvent confondu avec les sémantiques de Swarm/Kubernetes, et l’écosystème a évolué par étapes maladroites. Voici des faits concrets qui importent opérationnellement :
- L’objectif originel de Compose était l’ergonomie développeur, pas l’orchestration haute disponibilité. Il était optimisé pour « run the stack locally », pas pour « gérer des brownouts ».
depends_onhistorquement ne faisait qu’imposer l’ordre de démarrage. La disponibilité était explicitement hors scope pendant longtemps car elle dépend fortement de l’application.- Les healthchecks sont arrivés dans Docker plus tard que beaucoup ne le pensent ; les premières configurations Compose utilisaient des scripts ad-hoc « wait-for » parce qu’il n’y avait pas de primitive native.
- Swarm et Kubernetes ont popularisé les concepts explicites de health/readiness, ce qui a amené des équipes à attendre des mêmes sémantiques partout — même là où elles n’existent pas.
- Le
HEALTHCHECKde Docker s’exécute dans le namespace du conteneur, ce qui est utile pour tester l’état interne du service, mais peut manquer des problèmes de joignabilité externe. - Compose v2 est un plugin et pas l’ancien binaire Python ; les détails d’implémentation et les fonctionnalités supportées diffèrent selon l’environnement, ce qui alimente la confusion « ça marche pour moi ».
- L’ordre de démarrage n’est pas l’ordre de redémarrage ; une dépendance qui plante peut revenir plus tard, et Compose ne va pas re-séquencer le monde pour vous.
- Le DNS dans les réseaux Compose est généralement stable mais pas instantané ; les tentatives de connexion précoces peuvent échouer avec des erreurs de résolution de nom chez des clients qui démarrent vite.
- « DB accepte le TCP » n’est pas la même chose que « schéma prêt » ; des migrations peuvent encore être en cours, entraînant timeouts ou erreurs table manquante.
Voilà le piège : le périmètre de l’outil est plus étroit que le problème opérationnel, et nos cerveaux complètent les fonctionnalités manquantes.
Modes de défaillance que vous verrez en production (même si vous jurez que non)
1) Connection refused au démarrage, puis « magiquement » OK
Le conteneur de la base démarre vite. Le processus DB se lie tard. Votre app tente une fois, échoue, et exit. Compose le redémarre, ou vous le faites. Au second essai, ça marche.
Diagnostic : l’app n’a pas de retry/backoff, et vous avez confondu ordre de démarrage et disponibilité.
2) « No such host » ou échecs DNS transitoires
Les clients rapides peuvent tenter de résoudre un nom de service avant que le DNS embarqué soit complètement prêt ou avant que le réseau soit attaché. C’est plus rare maintenant, mais ça arrive encore sous charge ou sur des nœuds lents.
3) Schéma manquant / migrations en cours
Postgres accepte les connexions, mais votre job de migration n’est pas encore exécuté. Votre app démarre, exécute des requêtes et meurt. Vous obtenez une brève coupure et une pile d’alertes inutiles.
4) Conteneur healthy, système unhealthy
Votre healthcheck DB est pg_isready, qui retourne succès. Mais le disque est plein, la base est en lecture seule, ou les connexions sont à leur maximum. Les healthchecks valent ce que vaut leur définition.
5) Rétro-pression et timeouts qui se déguisent en problèmes de démarrage
Votre dépendance est « up » mais douloureusement lente : caches froids, fort I/O wait, CPU volé. L’app timeoute et exit, et tout le monde blâme Compose parce qu’il se trouve à côté.
Blague #2 : depends_on c’est comme dire « je suis arrivé au restaurant en premier » et supposer que le dîner est déjà prêt.
Patrons qui fonctionnent : healthchecks, retries et séquençage sensé
Si vous voulez de la fiabilité, il vous faut des couches. Compose peut aider, mais l’application doit faire la partie adulte : retry, backoff, et échouer de façon contrôlée.
Patron A : Ajouter des healthchecks explicites aux dépendances
Pour les services courants, définissez un healthcheck qui teste une disponibilité significative. Pas seulement « le processus existe ». Préférez une vraie requête ou un ping qui sollicite le bon sous-système.
Exemple : healthcheck Postgres qui valide TCP, auth et une requête basique :
cr0x@server:~$ cat docker-compose.yml
services:
db:
image: postgres:16
environment:
POSTGRES_PASSWORD: example
POSTGRES_USER: app
POSTGRES_DB: appdb
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app -d appdb -h 127.0.0.1 || exit 1"]
interval: 5s
timeout: 3s
retries: 20
start_period: 10s
api:
image: myorg/api:latest
depends_on:
db:
condition: service_healthy
Note opérationnelle : même si vous gatez le démarrage sur la santé, vous avez encore besoin de retries au niveau applicatif pour les redémarrages, failovers et réinitialisations en vol des dépendances.
Patron B : Rendre l’application résiliente (retries avec backoff)
Le meilleur endroit pour gérer la disponibilité d’une dépendance est le client. Votre API doit tolérer que la DB soit en retard de 30–120 secondes sans s’arrêter. Faites-en un log clair, réessayez avec backoff exponentiel, et conservez un signal de liveness séparé pour ne pas accepter le trafic prématurément.
Quand les gens l’évitaient parce que « ça cache les erreurs », ils veulent dire « je préfère les indisponibilités aux démarrages lents. » Vous pouvez toujours alerter sur une disponibilité lente ; vous n’avez pas besoin d’un crash-loop pour ressentir quelque chose.
Patron C : Séparer migrations/initialisation du démarrage de l’app
Exécutez les migrations comme un job one-shot qui bloque la fin du déploiement, pas le démarrage de l’app. En termes Compose, cela peut être un service dédié que vous lancez explicitement, ou un entrypoint qui effectue les migrations avec un verrou et une observabilité claire.
Ne lancez pas les migrations dans 10 réplicas d’app simultanément à moins d’aimer les erreurs « relation already exists » et les disputes sur qui a démarré en premier.
Patron D : Utiliser les politiques de restart de façon intentionnelle
restart: always peut masquer de vrais problèmes en les transformant en crash-loop perpétuel. Parfois acceptable pendant le bootstrap ; pas acceptable en steady-state.
Ma préférence pour la plupart des services :
- Utiliser
restart: unless-stoppedpour les services longue durée en dev/test. - En environnements proches de la prod, associer le restart à un logging sensé, un backoff côté app, et des healthchecks clairs afin que « restarting » ne devienne pas « working ».
Tâches pratiques : commandes, sorties et la décision que vous prenez
Cette section est volontairement pratique. Ce sont les tâches à lancer à 02:00 quand vous essayez de répondre à une question : qu’est-ce qui n’est réellement pas prêt, et pourquoi ?
Tâche 1 : Confirmer ce que Compose pense être en cours
cr0x@server:~$ docker compose ps
NAME IMAGE COMMAND SERVICE STATUS PORTS
stack-db-1 postgres:16 "docker-entrypoint.s…" db running (healthy) 5432/tcp
stack-api-1 myorg/api:latest "/bin/api" api running 0.0.0.0:8080->8080/tcp
Ce que ça signifie : Les conteneurs tournent ; la DB est « healthy » selon son healthcheck.
Décision : Si l’API renvoie encore des erreurs, ce n’est pas un problème d’ordre de démarrage ; passez aux logs et aux tests de connectivité réels.
Tâche 2 : Inspecter le graphe de dépendance et la config fusionnée
cr0x@server:~$ docker compose config
services:
api:
depends_on:
db:
condition: service_healthy
image: myorg/api:latest
db:
environment:
POSTGRES_DB: appdb
POSTGRES_PASSWORD: example
POSTGRES_USER: app
healthcheck:
interval: 5s
retries: 20
start_period: 10s
test:
- CMD-SHELL
- pg_isready -U app -d appdb -h 127.0.0.1 || exit 1
timeout: 3s
image: postgres:16
Ce que ça signifie : Vous validez ce que Compose exécutera réellement (après merges, overrides et interpolation d’environnement).
Décision : Si le healthcheck ou la condition de dépendance manque ici, vous debuggez le mauvais fichier ou la mauvaise implémentation de Compose.
Tâche 3 : Lire les logs de l’API avec timestamps
cr0x@server:~$ docker compose logs --timestamps --tail=200 api
api-1 2026-02-04T08:12:09.441Z ERROR db connect failed: dial tcp 172.22.0.2:5432: connect: connection refused
api-1 2026-02-04T08:12:09.443Z INFO exiting with code 1
api-1 2026-02-04T08:12:11.012Z INFO starting api version=1.9.3
Ce que ça signifie : Le client a essayé une fois et est sorti. C’est le comportement classique « pas de retry/backoff ».
Décision : Corriger la logique de démarrage de l’app, pas Compose. Ajouter des retries et ne tomber en échec sévère qu’après un temps borné.
Tâche 4 : Lire les logs DB autour de l’initialisation
cr0x@server:~$ docker compose logs --timestamps --tail=200 db
db-1 2026-02-04T08:12:03.118Z PostgreSQL init process complete; ready for start up.
db-1 2026-02-04T08:12:04.002Z database system is ready to accept connections
Ce que ça signifie : La DB était prête à 08:12:04Z, mais l’API a tenté à 08:12:09Z et s’est quand même vu refuser la connexion. Ce décalage suggère un problème réseau, une mauvaise adresse, ou un redémarrage DB.
Décision : Valider la cible de connexion de l’API (host, port, TLS) et tester la connectivité depuis l’intérieur du namespace réseau.
Tâche 5 : Valider le DNS et la joignabilité réseau depuis le conteneur API
cr0x@server:~$ docker compose exec api getent hosts db
172.22.0.2 db
Ce que ça signifie : La résolution DNS à l’intérieur du réseau Compose fonctionne.
Décision : Si ceci échoue, vous avez un souci d’attachement réseau ou de nom (mauvais réseau, mauvais nom de service, ou conteneur pas sur le même réseau).
Tâche 6 : Vérifier la connexion TCP vers la DB depuis le conteneur API
cr0x@server:~$ docker compose exec api bash -lc 'timeout 2 bash -lc "
Ce que ça signifie : Le TCP est joignable maintenant.
Décision : Si le TCP est OK mais que l’auth/les requêtes échouent, votre probe de readiness doit être plus profond que « port ouvert ».
Tâche 7 : Effectuer une vraie requête DB depuis le conteneur API
cr0x@server:~$ docker compose exec api bash -lc 'PGPASSWORD=example psql -h db -U app -d appdb -c "select 1;"'
?column?
----------
1
(1 row)
Ce que ça signifie : Le chemin d’auth et la requête basique fonctionnent.
Décision : Si votre app échoue encore, le problème est probablement la config applicative (mauvais DSN), les migrations, ou les limites du pool de connexions — pas Compose.
Tâche 8 : Inspecter les détails de health des conteneurs (ne devinez pas)
cr0x@server:~$ docker inspect --format '{{json .State.Health}}' stack-db-1
{"Status":"healthy","FailingStreak":0,"Log":[{"Start":"2026-02-04T08:12:25.011Z","End":"2026-02-04T08:12:25.042Z","ExitCode":0,"Output":"/var/run/postgresql:5432 - accepting connections\n"}]}
Ce que ça signifie : Le healthcheck retourne « accepting connections ». Il ne valide pas la disponibilité du schéma ni que l’utilisateur API a des privilèges au-delà de la connexion.
Décision : Si vous avez besoin de la disponibilité du schéma, construisez une sonde qui vérifie une table connue ou la version de migration.
Tâche 9 : Vérifier les boucles de restart et les codes de sortie
cr0x@server:~$ docker compose ps --all
NAME SERVICE STATUS
stack-api-1 api restarting (1) 3 seconds ago
stack-db-1 db running (healthy)
Ce que ça signifie : L’API est en crash-loop. Ce n’est pas « en attente », c’est un échec suivi d’un redémarrage.
Décision : Pauser la boucle pour préserver les logs/état, puis corriger l’échec immédiat. Les crash-loops peuvent aussi provoquer un DoS sur votre dépendance.
Tâche 10 : Confirmer les variables d’environnement et le DSN réel utilisé
cr0x@server:~$ docker compose exec api env | egrep 'DATABASE_URL|PGHOST|PGPORT|PGUSER'
DATABASE_URL=postgres://app:example@db:5432/appdb?sslmode=disable
Ce que ça signifie : L’app est configurée pour se connecter à db dans le réseau Compose, pas à localhost.
Décision : Si vous voyez localhost ici, c’est votre bug. Dans les conteneurs, localhost pointe vers le conteneur, pas vers le service DB.
Tâche 11 : Identifier les goulets d’étranglement ressources sur l’hôte (CPU, mémoire, IO)
cr0x@server:~$ docker stats --no-stream
CONTAINER ID NAME CPU % MEM USAGE / LIMIT MEM % NET I/O BLOCK I/O
a12b3c4d5e6f stack-db-1 215.32% 1.2GiB / 2GiB 60.00% 2.1MB / 3.4MB 1.2GB / 900MB
b98c7d6e5f4a stack-api-1 0.32% 55MiB / 512MiB 10.74% 800KB / 700KB 12MB / 4MB
Ce que ça signifie : La DB utilise beaucoup de CPU et fait beaucoup d’I/O block. Cela peut retarder la disponibilité et provoquer des timeouts côté client.
Décision : Si la DB est saturée au démarrage, ajustez start_period, augmentez les timeouts, et considérez la performance du stockage (type de volume, contention I/O hôte).
Tâche 12 : Prouver que le problème est un timing de démarrage en retardant le démarrage de l’app
cr0x@server:~$ docker compose stop api
[+] Stopping 1/1
✔ Container stack-api-1 Stopped
cr0x@server:~$ sleep 15
cr0x@server:~$ docker compose start api
[+] Starting 1/1
✔ Container stack-api-1 Started
Ce que ça signifie : Si cela « répare » le problème, vous avez confirmé une course au démarrage.
Décision : Ne conservez pas le sleep. Implémentez un gating de readiness (healthcheck + condition) et des retries côté app.
Tâche 13 : Vérifier la timeline des events pour attraper redémarrages et transitions de santé
cr0x@server:~$ docker events --since 10m --filter 'container=stack-db-1' --filter 'container=stack-api-1'
2026-02-04T08:12:01.004Z container create a12b3c4d5e6f (name=stack-db-1)
2026-02-04T08:12:01.210Z container start a12b3c4d5e6f (name=stack-db-1)
2026-02-04T08:12:04.120Z container health_status: healthy a12b3c4d5e6f (name=stack-db-1)
2026-02-04T08:12:04.300Z container start b98c7d6e5f4a (name=stack-api-1)
2026-02-04T08:12:09.443Z container die b98c7d6e5f4a (name=stack-api-1, exitCode=1)
Ce que ça signifie : Vous obtenez une timeline exacte : la DB est devenue healthy avant le démarrage de l’API, pourtant l’API est morte. Ça pointe loin d’une naïve problématique de readiness et plutôt vers la config, les permissions, TLS, ou quelque chose que le healthcheck n’a pas testé.
Décision : Élargissez le healthcheck ou ajoutez un logging de readiness côté app qui indique exactement ce qu’il attend.
Tâche 14 : Valider les montages de volumes et permissions pour les dépendances stateful
cr0x@server:~$ docker compose exec db bash -lc 'ls -ld /var/lib/postgresql/data; df -h /var/lib/postgresql/data | tail -1'
drwx------ 19 postgres postgres 4096 Feb 4 08:12 /var/lib/postgresql/data
overlay 80G 78G 2.0G 98% /
Ce que ça signifie : Le disque est plein à 98 %. Postgres peut « démarrer », passer des healthchecks simplistes, puis mal se comporter sous pression d’écriture.
Décision : Traitez la pression disque comme une exigence de disponibilité de premier ordre pour les services stateful. Corrigez la capacité avant de toucher au YAML.
Playbook de diagnostic rapide
Si vous ne retenez qu’une section, que ce soit celle-ci. Quand une stack Compose « démarre » mais ne fonctionne pas, vous voulez trouver rapidement le goulot — pas écrire de la fan fiction sur depends_on.
Première étape : établir la classe de défaillance (crash-loop app vs app up mais en erreur)
cr0x@server:~$ docker compose ps
NAME SERVICE STATUS
stack-api-1 api restarting (1) 2 seconds ago
stack-db-1 db running (healthy)
Si boucle de restart : concentrez-vous sur les logs de l’app et la raison de sortie. Ne poursuivez pas la piste readiness avant de savoir ce qui échoue.
Deuxième étape : lire les logs des deux côtés, alignés dans le temps
cr0x@server:~$ docker compose logs --timestamps --tail=100 api
...application errors...
cr0x@server:~$ docker compose logs --timestamps --tail=100 db
...db startup and readiness...
Décision : Si la DB n’était clairement pas prête quand l’API a tenté, vous avez besoin de gating ou de retries. Si la DB était prête, votre défaillance est probablement liée à la config/auth/schéma/ressources.
Troisième étape : tester depuis le namespace réseau du conteneur en échec
cr0x@server:~$ docker compose exec api getent hosts db
...ip...
cr0x@server:~$ docker compose exec api bash -lc 'timeout 2 bash -lc "
Décision : Échec DNS → câblage réseau. Échec TCP → dépendance pas à l’écoute ou mauvais port. TCP OK mais l’app échoue → auth/schéma/TLS/pool/timeouts.
Quatrième étape : chercher la contention de ressources au niveau hôte
cr0x@server:~$ docker stats --no-stream
...cpu/mem/io...
Décision : Si la DB est liée par l’I/O, vous verrez la « readiness » fluctuer parce que le monde est lent, pas parce que le YAML est faux.
Cinquième étape : valider ce que vous exécutez réellement
cr0x@server:~$ docker compose config
...resolved config...
Décision : Si la config n’est pas celle que vous pensiez, arrêtez. Corrigez la source de vérité (mauvais fichier, mauvais override, mauvaise variable d’environnement) avant de déboguer les symptômes.
Erreurs courantes : symptômes → cause racine → correctif
1) Symptôme : l’API sort immédiatement avec « connection refused »
Cause racine : Le client effectue une seule tentative de connexion au démarrage, échoue vite et exit. depends_on n’a pas aidé parce qu’il n’attend pas la disponibilité.
Correctif : Ajouter retry/backoff dans l’app, ou gatez le démarrage sur un healthcheck significatif (service_healthy) plus une attente bornée côté app.
2) Symptôme : l’API peut résoudre « db » mais ne peut pas se connecter
Cause racine : La DB écoute sur un port différent, liée à une adresse différente, ou elle-même en crash-loop. Parfois la DB démarre puis redémarre à cause d’une corruption de stockage ou d’un souci de config.
Correctif : Vérifier les logs DB, inspecter les bindings de port, s’assurer que la DB écoute l’interface attendue. Tester le TCP depuis l’intérieur du conteneur API.
3) Symptôme : ça marche au second redémarrage ; échoue au déploiement frais
Cause racine : Course au démarrage. Le système dépend accidentellement du timing.
Correctif : Cessez de « réparer » ça par des redémarrages manuels. Rendre la disponibilité explicite avec healthchecks et retries côté client. Ajouter une deadline de démarrage pour échouer clairement si ça ne récupère pas.
4) Symptôme : « relation does not exist » ou « table not found » au démarrage
Cause racine : Les migrations ne sont pas terminées quand l’app démarre, ou plusieurs réplicas lancent les migrations en parallèle.
Correctif : Exécuter les migrations comme un job/étape dédiée. Si vous devez les lancer depuis l’app, utilisez des verrous d’advisory ou un pattern single-runner et loggez l’état des migrations clairement.
5) Symptôme : Le healthcheck DB est healthy, mais l’app timeoute
Cause racine : Le healthcheck teste une condition superficielle (socket ouvert) mais pas la performance, l’auth, ou la disponibilité du schéma. Ou la DB est surchargée (CPU/IO) et lente.
Correctif : Rendre le healthcheck significatif (par ex. une requête). Augmenter les timeouts prudemment. Examiner la contention de ressources hôte et la latence du stockage.
6) Symptôme : Tout est « up », mais les requêtes échouent de façon intermittente
Cause racine : Réinitialisations en vol de dépendances, épuisement de pool de connexions, glitches DNS/réseau éphémères, ou politiques de restart qui cachent des crashes récurrents.
Correctif : Ajouter des circuit breakers et des retries avec jitter. Surveiller les redémarrages et les flaps de santé. Ne pas utiliser les politiques de restart comme substitut à la correction des causes de crash.
7) Symptôme : Utiliser localhost dans la config marche hors Docker, échoue dedans
Cause racine : À l’intérieur d’un conteneur, localhost désigne le conteneur lui-même.
Correctif : Utiliser le nom de service Compose (db) comme hôte, ou définir un alias réseau explicite.
Trois mini-récits du monde corporate (anonymisés, plausibles, techniquement exacts)
Mini-récit 1 : L’incident causé par une fausse hypothèse
Une entreprise SaaS de taille moyenne avait une stack Compose simple en staging qui lançait Postgres, un conteneur de migration, et une API. Le conteneur de migration dépendait de Postgres. L’API dépendait du conteneur de migration. Ça semblait une chaîne propre de responsabilités.
Pendant une répétition de release un lundi matin, l’API est montée et a immédiatement commencé à générer des erreurs. L’équipe a fait ce que font les équipes : tout redémarrer. Ça a marché la deuxième fois. Ils ont haussé les épaules et sont passés à autre chose.
Deux semaines plus tard, ils ont reconstruit les hôtes de staging. Disques propres, stockage un peu plus lent, noyau légèrement différent. Au premier démarrage après le déploiement, Postgres a mis plus de temps à rejouer le WAL. Le conteneur de migration a démarré (parce que le conteneur Postgres était démarré), a tenté de se connecter, a échoué une fois, et est sorti avec un code non nul. L’API a démarré quand même parce que la chaîne de dépendance n’encodait que l’ordre de démarrage, pas « migrations réussies ». Elle est ensuite tombée sur des tables manquantes.
L’incident n’était pas dramatique, mais bruyant : pluie d’alertes, ingénieurs confus, et des execs qui demandaient pourquoi « staging est encore down ». La cause racine était douloureusement simple : ils avaient modélisé la correction comme l’ordre de démarrage des conteneurs, pas comme des critères explicites de disponibilité et de succès.
La correction fut également simple, mais demandait de la discipline : les migrations sont devenues une étape explicite de déploiement avec pass/fail clair. L’API a gagné des retries/backoff et une deadline de démarrage. Compose a continué d’être utilisé, mais comme un runner — pas comme un moteur de fiabilité.
Mini-récit 2 : L’optimisation qui s’est retournée contre eux
Une équipe plateforme données voulait un feedback développeur plus rapide. Ils ont raccourci les intervalles et retries des healthchecks pour faire échouer rapidement les services. En théorie, une dépendance en échec remonterait vite et les devs la corrigeraient.
En pratique, ils ont créé une machine à flap. Sur les laptops, la DB était lente au cold start à cause des contraintes de Docker Desktop. Le healthcheck strict échouait tôt, Compose rapportait unhealthy, et les services dépendants ne démarraient jamais. Les devs ont commencé à « fixer » ça en augmentant localement les limites CPU ou en désactivant complètement les healthchecks.
Puis le pattern a fuité en CI. Les runners CI étaient contraints en ressources et partageaient des voisins bruyants. Les healthchecks échouaient fréquemment pendant le start_period, rendant les pipelines intermittents. Les ingénieurs ont perdu confiance dans le signal et relançaient les pipelines jusqu’à ce qu’ils passent. L’organisation s’est retrouvée avec une livraison plus lente, plus de compute gaspillé, et moins d’alertes utiles.
Ils ont reculé en admettant une vérité ennuyeuse : les healthchecks ne sont pas une course vers le bas. Ils doivent correspondre au comportement de démarrage attendu sous contention réaliste. Ils ont augmenté start_period, gardé des intervalles raisonnables, et utilisé des retries côté app pour lisser la variance. « Fail fast » est devenu « fail clearly, with context ».
Mini-récit 3 : La pratique ennuyeuse mais correcte qui a sauvé la mise
Un service adjacent aux paiements avait un environnement d’intégration basé sur Compose utilisé par plusieurs équipes. Rien de sophistiqué : API, worker, Postgres, Redis. L’équipe qui le maintenait était allergique à la débrouillardise, ce qui est un compliment.
Ils ont appliqué trois règles : chaque dépendance avait un healthcheck significatif ; chaque client avait retry/backoff avec un délai max de démarrage ; et chaque déploiement lançait un smoke test depuis l’intérieur du namespace réseau après le démarrage. Le smoke test n’était pas exhaustif — juste suffisant pour prouver le chemin critique.
Un matin, un reboot d’hôte a coïncidé avec un ralentissement du stockage. Postgres est monté mais était lent ; les healthchecks ont pris plus de temps mais ont quand même passé dans la fenêtre configurée. L’API a mis plus de temps à déclarer sa disponibilité parce que sa vérification interne attendait une requête réussie plus une vérification de version de migration. Elle n’a pas crash-loopé, donc elle n’a pas pilonné Postgres avec des tempêtes de connexions froides répétées.
Le résultat était profondément peu héroïque : le démarrage a pris plus de temps, et tout a continué de fonctionner. Les équipes ont remarqué un délai mais pas une panne. La différence n’était pas un acte héroïque ; c’était la conception pour un monde où le démarrage est variable et les dépendances peuvent être en retard.
Checklists / plan pas à pas
Un plan pas à pas pour sortir du piège des dépendances
- Cessez d’utiliser
depends_oncomme garantie de disponibilité. Gardez-le pour l’ordre de démarrage uniquement. - Ajoutez des healthchecks aux services stateful. Rendez-les significatifs (pas seulement « port ouvert »).
- Si supporté, gatez avec
condition: service_healthy. Considérez-le comme un confort, pas une garantie. - Implémentez des retries côté client avec backoff exponentiel + jitter. Incluez une deadline maximale de démarrage (ex. 2–5 minutes).
- Séparez les migrations du boot applicatif. Exécutez-les comme une étape explicite avec logging et comportement d’échec clairs.
- Concevez la readiness autour du parcours utilisateur. « Ping DB fonctionne » peut ne pas signifier « schéma prêt ».
- Instrumentez le démarrage. Logguez ce que vous attendez, combien de temps ça a pris, et pourquoi ça a échoué.
- Validez depuis l’intérieur des conteneurs. Testez DNS, TCP et une requête réelle depuis le conteneur app.
- Surveillez les ressources. Si le démarrage DB est lié à l’I/O, corrigez le stockage/la contention hôte, pas le YAML.
- Lancez un smoke test post-start. Un petit test rapide attrape les courses de démarrage avant les utilisateurs.
Checklist opérationnelle pour un fichier Compose dont vous ne regretterez pas l’écriture
- Chaque service stateful a un healthcheck avec un
start_period,timeoutetretriessensés. - Les clients utilisent les noms de service, pas
localhost, pour la connectivité intra-stack. - Les politiques de restart sont choisies intentionnellement ; les crash-loops sont traités comme incidents, pas « self healing ».
- Les migrations/initialisations sont single-runner et observables.
- Les logs incluent des timestamps et assez de contexte pour reconstruire une timeline de démarrage.
FAQ
1) Est-ce que depends_on attend que le port DB soit ouvert ?
Non. Par défaut il assure seulement que Compose démarre le conteneur dépendance avant de démarrer le conteneur dépendant. La disponibilité du port n’est pas implicite.
2) Si j’ajoute un healthcheck à Postgres, suis-je tranquille ?
Vous êtes moins en tort, pas tranquille. Un healthcheck peut aider à gatez le démarrage initial (si vous utilisez service_healthy), mais votre app a toujours besoin de retries pour les redémarrages et les erreurs transitoires.
3) Pourquoi ne pas juste utiliser un script « wait-for-it » partout ?
Parce qu’il vérifie souvent uniquement la connexion TCP, la définition la plus superficielle de « prêt ». Il a aussi tendance à devenir de la colle tribale que vous oubliez de maintenir. Préférez des retries côté app et des healthchecks significatifs ; utilisez des scripts d’attente seulement si nécessaire.
4) Ma DB est healthy mais les migrations ne sont pas terminées. Comment modéliser ça ?
Séparez les préoccupations : la santé DB signifie « la DB peut servir des requêtes ». L’achèvement des migrations est un état de déploiement. Exécutez les migrations comme une étape explicite ou un service one-shot et gatez la readiness app sur une vérification de schéma/version.
5) Puis-je compter sur condition: service_healthy dans tous les environnements ?
Non. Le support des fonctionnalités varie selon les versions de Compose et les outils. Vérifiez toujours avec docker compose config et testez dans l’environnement où vous déployez.
6) Pourquoi ça n’échoue que sur des machines fraîches ou après reboot hôte ?
Les cold starts amplifient la variabilité : caches froids, disques occupés, récupération après crash, et ordonnancement CPU plus bruité. Si votre système dépend de « ça démarre généralement vite », il échouera précisément quand tout est froid et lent.
7) Est-ce mauvais d’utiliser restart: always ?
Ce n’est pas moralement mauvais ; c’est risqué opérationnellement. Ça peut cacher de vrais échecs, créer du hammering sur les dépendances, et rendre les logs plus difficiles à interpréter. Associez les politiques de restart à un backoff, de bons logs, et des healthchecks réels.
8) Comment distinguer un problème de readiness d’un problème de performance ?
Les problèmes de readiness échouent tôt avec des « connection refused », des erreurs DNS ou des échecs d’auth. Les problèmes de performance montrent des timeouts, une latence élevée, et une saturation des ressources (docker stats montre CPU/I/O élevés). Traitez-les différemment.
9) Quelle est l’approche la plus simple et fiable pour des petites stacks ?
Healthcheckez la DB, ajoutez des retries client avec backoff, et exécutez un smoke test après le démarrage. Ce trio évite la plupart des incidents de course au démarrage sans transformer votre fichier Compose en scénario théâtral.
Prochaines étapes qui réduisent réellement les incidents
Si votre stack nécessite parfois « juste un redémarrage », vous n’avez pas un problème Compose. Vous avez un problème de contrat de disponibilité. depends_on est correct pour l’ordre. Ce n’est pas une poignée de main, pas une promesse, et pas un substitut à la résilience.
Faites ceci ensuite, dans cet ordre :
- Ajoutez des healthchecks significatifs aux dépendances stateful (DB, queues, caches).
- Faites en sorte que les clients réessaient avec backoff exponentiel, jitter, et une deadline de démarrage maximale.
- Arrêtez de mélanger les migrations dans le démarrage aléatoire d’app ; exécutez-les explicitement et observez le succès.
- Construisez une timeline pendant les incidents en utilisant des logs horodatés et
docker events. - Prouvez la connectivité depuis l’intérieur des conteneurs avant de réécrire la config.
Compose fera toujours ce qu’il a toujours fait : démarrer les conteneurs. Votre travail est de faire en sorte que « démarré » veuille dire quelque chose d’utile. C’est l’ingénierie de fiabilité : rendre les modes d’échec évidents ennuyeux.