Docker Compose : depends_on vous a menti — disponibilité réelle sans bricolages

Cet article vous a aidé ?

Vous lancez votre stack. Le conteneur de base de données est « up ». Le conteneur API démarre. Puis il plante parce qu’il ne peut pas se connecter.
Vous ajoutez depends_on. Ça plante encore. Vous ajoutez sleep 10. Ça marche… jusqu’au lundi.

Si vous avez déjà vu une stack Compose vaciller comme une enseigne au néon mourante, voilà pourquoi : depends_on n’a jamais été une porte d’entrée pour la disponibilité.
C’est un indice d’ordre de démarrage. Le traiter comme une garantie de readiness mène à des échecs intermittents qui ne se reproduisent qu’en démo.

Ce que depends_on fait réellement (et ce qu’il ne fait pas)

Compose a deux idées distinctes que les gens confondent sans cesse :
ordre de démarrage et disponibilité (readiness).
depends_on ne traite que du premier—en quelque sorte.

La vérité simple

  • Il peut démarrer des conteneurs dans un ordre donné. C’est tout.
  • Il n’attend pas que votre service soit prêt. « Conteneur démarré » n’est pas « base de données acceptant des requêtes ».
  • Il ne valide pas la joignabilité réseau. Votre dépendance peut être « up » mais injoignable à cause du DNS, de règles de pare-feu ou d’un mauvais nom d’hôte.
  • Il ne prévient pas les conditions de course. Si votre app fait des migrations au démarrage et que la BD est encore en initialisation, vous pouvez perdre.

Certaines implémentations et versions de Compose supportent depends_on avec des conditions comme service_healthy.
C’est plus proche de ce que les gens veulent, mais même là : c’est seulement aussi bon que votre healthcheck.
Un mauvais healthcheck n’est que sleep 10 avec plus de paperasse.

Voici le changement de mentalité : la readiness est un contrat au niveau de l’application.
Docker peut exécuter des processus. Il ne sait pas quand votre BD a rejoué le WAL, votre appli a préchauffé les caches,
ou vos migrations de schéma sont terminées. Vous devez définir ces signaux.

Blague n°1 : Utiliser sleep 10 pour la readiness, c’est comme vouloir résoudre la perte de paquets en criant sur le routeur. Ça donne l’illusion d’être productif, sans l’être.

Pourquoi « conteneur démarré » est un jalon inutile

Si vous exécutez Postgres, « démarré » peut signifier qu’il exécute encore des scripts d’initialisation, crée des utilisateurs ou rejoue des journaux.
Pour Elasticsearch, « démarré » peut signifier que la JVM existe mais que le cluster est en rouge.
Pour des stockages d’objets, « démarré » peut signifier que les identifiants ne sont pas encore chargés.

Compose ne connaît pas vos sémantiques. Et même s’il les connaissait, la readiness multi-étape est courante :
DNS prêt, port TCP ouvert, handshake TLS possible, authentification opérationnelle, schéma présent, migrations terminées, workers en arrière-plan.
Choisissez l’étape dont vous dépendez réellement, puis testez-la.

Faits et historique à utiliser dans les arguments

Quand vous essayez de convaincre une équipe d’arrêter de livrer un « wait-for-it.sh » collé au démarrage de leur appli,
il aide de savoir d’où vient ce bazar.

  1. Compose ciblait à l’origine les workflows développeur, pas l’orchestration production. L’ordre de démarrage suffisait « pour le laptop ».
  2. « Healthy » n’est pas un état d’exécution Docker natif au même titre que « running » ; c’est le résultat d’un healthcheck, optionnel et défini par l’appli.
  3. Les versions classiques du fichier Compose ont changé la sémantique au fil du temps ; certaines fonctionnalités existaient en syntaxe v2 mais se sont embrouillées en v3 (surtout avec la pensée Swarm).
  4. Swarm et Kubernetes ont poussé des modèles différents : Swarm s’appuyait sur le cycle de vie des conteneurs ; Kubernetes a rendu readiness/liveness prioritaires, mais toujours définis par l’appli.
  5. Les ports peuvent être ouverts bien avant que les services soient utilisables. Beaucoup de démons se lient tôt, puis effectuent une initialisation interne.
  6. Le DNS dans les réseaux Docker est finalement cohérent lors de redémarrages rapides ; des échecs de résolution pendant des pics de démarrage existent réellement.
  7. Les politiques de redémarrage peuvent créer des essaims : une appli qui échoue vite peut saturer une BD déjà en difficulté au démarrage.
  8. Les healthchecks ont été conçus pour « est-ce vivant ? » et ont été réutilisés pour « est-ce prêt ? », ce qui n’est pas toujours la même question.

La conclusion : ce n’est pas que vous « faites mal » parce que Compose est mauvais. Vous faites mal parce que vous demandez à Compose d’être Kubernetes.
Compose peut néanmoins être rendu fiable. Il suffit d’être explicite.

Les modes de défaillance que vous diagnostiquez mal

1) Connexion refusée au démarrage, puis ça marche plus tard

C’est généralement une course. Le processus cible ne s’est pas encore lié au port, ou il s’est lié sur une interface différente.
Parfois c’est l’inverse : le port est ouvert mais le protocole n’est pas prêt (TLS non chargé, BD qui n’accepte pas l’authentification).

2) « Temporary failure in name resolution »

Le DNS embarqué de Docker est généralement solide, mais lors d’un fort churn de conteneurs vous pouvez encore obtenir des échecs transitoires de résolution.
Si votre appli considère un seul hic DNS comme fatal, vous avez un démarrage fragile.
Votre plan de readiness doit inclure des retries avec backoff pour la résolution de noms et les tentatives de connexion réseau.

3) Le healthcheck indique « healthy » mais l’appli plante encore

Le healthcheck est trop superficiel. Un check de connexion TCP n’équivaut pas à « le schéma existe ».
Un curl / retournant 200 peut signifier « le serveur web est up », pas « l’application peut parler à la BD ».
Les healthchecks doivent refléter la frontière de dépendance.

4) Tout fonctionne en local, échoue en CI

Les hôtes CI ont un comportement CPU, disque et entropie différents.
Des I/O lentes rallongent l’initialisation de la BD. Un DNS lent fait échouer la résolution précoce.
Les timeouts réglés pour votre laptop sont inutilisables dans un runner bridé.
Si votre solution est « ajouter 30 secondes de sleep », vous avez juste déplacé le flake.

5) Redémarrages en cascade

Un service dépendant échoue vite et redémarre agressivement. Chaque redémarrage déclenche des retries, migrations, réchauffage de cache.
Pendant ce temps, la BD est encore en démarrage et sous charge.
Vous obtenez une boucle de rétroaction : le service dépendant devient un outil de déni de service contre sa propre dépendance.

Patrons de readiness corrects (sans ingénierie à base de siestes)

Patron A : Utiliser des healthchecks qui testent ce dont vous avez réellement besoin

Ne testez pas qu’un port est ouvert. Testez que le système peut accomplir l’opération minimale requise par le service dépendant.
Pour une base de données, cela peut être « peut s’authentifier et exécuter une requête triviale ».
Pour un service HTTP, cela peut être « retourne 200 sur un endpoint de readiness qui vérifie les dépendances en aval ».

Exemple : le healthcheck Postgres devrait exécuter pg_isready et idéalement une requête si vous dépendez d’une base spécifique.
Pour Redis, redis-cli ping suffit. Pour Kafka, c’est plus compliqué.

Patron B : Verrouiller le démarrage sur le statut de santé (quand disponible), pas sur le démarrage du conteneur

Si votre Compose supporte depends_on avec des conditions comme service_healthy, utilisez-les.
Mais considérez cela comme un mécanisme d’application, non comme la conception principale.
La conception principale reste : le healthcheck doit représenter la readiness.

Patron C : Intégrer retry/backoff dans votre application

C’est ce que les ingénieurs rechignent à faire parce que ça ressemble à « masquer » des problèmes d’infrastructure.
Ça ne l’est pas. Les réseaux sont peu fiables. Les courses de démarrage arrivent. Les dépendances redémarrent.
Si votre appli ne peut pas retenter une connexion BD pendant 30–60 secondes avec un backoff bruité, elle n’est pas prête pour la production.

Il y a une différence entre « retry parce que le monde est sale » et « retry indéfiniment parce qu’on refuse de corriger la config ».
Fixez un plafond. Émettez des logs structurés. Échouez après un timeout raisonnable.

Patron D : Séparer le « travail d’init » du « service qui sert le trafic »

Les migrations de schéma, la création de buckets, les templates d’index et la « création d’un utilisateur admin » ne devraient pas s’exécuter dans le process principal
sauf si vous êtes prêt pour les problèmes de concurrence et d’idempotence.

Dans Compose, un bon patron est : un service « init » one-shot qui s’exécute et sort avec succès, et votre appli en dépend.
Votre conteneur d’init doit être idempotent : sûr à exécuter plusieurs fois, sûr si partiellement terminé.

Patron E : Préférer des endpoints de readiness explicites pour les services HTTP

Si votre API a besoin de BD + queue + stockage d’objets, exposez un endpoint /ready qui vérifie ces dépendances.
Ensuite votre healthcheck appelle celui-ci. Maintenant votre définition de « ready » correspond aux exigences réelles.

Patron F : Éviter les scripts fragiles « wait-for » collés à l’ENTRYPOINT

Les gens adorent coller un script qui boucle sur un check de port.
C’est facile. C’est souvent aussi faux : port ouvert ≠ prêt, et le script devient un mini-platform non maîtrisé.

Si vous devez attendre, faites un vrai check client (par ex. exécuter une requête BD). Et gardez-le minimal.
Mieux : utilisez healthchecks + gating de dépendance + retries applicatifs.

Citation (idée paraphrasée), attribuée : Werner Vogels a souvent insisté que « tout échoue, tout le temps », donc les systèmes doivent supposer l’échec et se rétablir automatiquement.

Tâches pratiques : commandes, sorties, décisions

Voici les actions que j’utilise réellement quand une stack Compose ne démarre pas proprement.
Chaque tâche inclut : commande, sortie typique, ce que ça signifie, et la décision à prendre.

Task 1: Prove what Compose thinks the config is

cr0x@server:~$ docker compose config
services:
  api:
    depends_on:
      db:
        condition: service_healthy
    environment:
      DATABASE_URL: postgres://app:***@db:5432/app
  db:
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U app -d app"]
      interval: 5s
      timeout: 3s
      retries: 20

Signification : Ceci est la configuration normalisée que Compose exécutera, après fusions et substitution d’env vars.
Si condition: service_healthy disparaît ici, votre implémentation de Compose ne la respecte pas.

Décision : Si la sortie de config n’affiche pas ce que vous attendez, arrêtez de déboguer le runtime.
Corrigez d’abord le YAML et le décalage de version/implémentation.

Task 2: Watch startup order and exit reasons in one shot

cr0x@server:~$ docker compose up --detach && docker compose ps
[+] Running 2/2
 ✔ Container stack-db-1   Started   0.7s
 ✔ Container stack-api-1  Started   0.2s
NAME          IMAGE             COMMAND                  SERVICE   STATUS
stack-db-1    postgres:16       "docker-entrypoint.s…"   db        Up 2 seconds (health: starting)
stack-api-1   myapi:latest      "/app/start"             api       Restarting (1) 2 seconds ago

Signification : La BD est « starting » côté health ; l’API redémarre déjà. Voilà votre course, visible.

Décision : Si un service dépendant redémarre tandis que les dépendances sont « starting », vous avez besoin de gating et/ou de retries.

Task 3: Inspect health status precisely

cr0x@server:~$ docker inspect --format '{{json .State.Health}}' stack-db-1
{"Status":"starting","FailingStreak":2,"Log":[{"Start":"2026-01-02T10:01:01.123Z","End":"2026-01-02T10:01:01.456Z","ExitCode":1,"Output":"/bin/sh: pg_isready: not found\n"}]}

Signification : Votre commande de healthcheck n’existe pas dans l’image. Les images Postgres ont pg_isready, mais des dérivés slim peuvent ne pas l’avoir.

Décision : Corrigez le healthcheck pour utiliser les outils disponibles, ou installez les outils clients. Un healthcheck qui échoue est pire que pas de healthcheck.

Task 4: Confirm the process is actually listening

cr0x@server:~$ docker exec -it stack-db-1 ss -lntp
State  Recv-Q Send-Q Local Address:Port Peer Address:PortProcess
LISTEN 0      244    0.0.0.0:5432      0.0.0.0:*    users:(("postgres",pid=1,fd=6))

Signification : Postgres a lié le TCP 5432. C’est nécessaire, mais pas suffisant.

Décision : Si ce n’est pas en écoute, vérifiez les logs et la config de la BD. Si c’est en écoute mais que les clients échouent, montez dans la pile : auth, DNS, TLS, schéma.

Task 5: Test name resolution from the dependent container

cr0x@server:~$ docker exec -it stack-api-1 getent hosts db
172.20.0.2   db

Signification : La résolution DNS fonctionne à ce moment.

Décision : Si la résolution échoue de façon intermittente, ajoutez des retries avec backoff et envisagez de ralentir les boucles de redémarrage.

Task 6: Test connectivity at the TCP level (fast, shallow)

cr0x@server:~$ docker exec -it stack-api-1 bash -lc 'timeout 2 bash -lc "

Signification : Un handshake TCP est possible.

Décision : Si le TCP échoue, c’est réseau/nom/pare-feu/écoute. Si le TCP fonctionne, vous avez besoin de vérifications au niveau protocole.

Task 7: Test readiness using a real client operation (Postgres)

cr0x@server:~$ docker exec -it stack-api-1 bash -lc 'psql "postgres://app:app@db:5432/app" -c "select 1;"'
 ?column?
----------
        1
(1 row)

Signification : L’authentification fonctionne, la BD existe, les requêtes fonctionnent. Voilà une readiness réelle.

Décision : Si cela échoue, arrêtez de blâmer Compose. Corrigez les identifiants, l’initialisation de la BD ou les migrations.

Task 8: Read the logs with timestamps and without scrolling madness

cr0x@server:~$ docker compose logs --timestamps --tail=80 api
2026-01-02T10:01:03.002Z api-1  ERROR db connect failed: dial tcp: lookup db: temporary failure in name resolution
2026-01-02T10:01:03.540Z api-1  ERROR exiting after 1 attempt

Signification : Ce n’est pas « la BD est lente », c’est une résolution DNS transitoire + l’appli qui sort après une seule tentative.

Décision : Ajoutez de la logique de retry. Envisagez aussi de réduire l’agressivité des redémarrages pour laisser le churn DNS/daemon se stabiliser.

Task 9: Inspect restart policy and current restart loop

cr0x@server:~$ docker inspect --format '{{.HostConfig.RestartPolicy.Name}}' stack-api-1
always

Signification : Le conteneur redémarrera indéfiniment, même s’il échoue instantanément.

Décision : Utilisez on-failure pour certains services en développement, ou ajoutez du backoff/timeout dans l’app pour éviter l’auto-DoS.

Task 10: Confirm the containers are on the same network

cr0x@server:~$ docker network inspect stack_default --format '{{json .Containers}}'
{"a1b2c3d4":{"Name":"stack-db-1","IPv4Address":"172.20.0.2/16"},"e5f6g7h8":{"Name":"stack-api-1","IPv4Address":"172.20.0.3/16"}}

Signification : Ils partagent le réseau par défaut du projet.

Décision : Si un service est sur un réseau différent, votre hostname peut ne pas résoudre ni router. Corrigez les réseaux avant de toucher aux timeouts.

Task 11: Validate that your healthcheck is actually executing

cr0x@server:~$ docker inspect --format '{{range .State.Health.Log}}{{.ExitCode}} {{.Output}}{{end}}' stack-db-1 | tail -n 3
0 /var/run/postgresql:5432 - accepting connections
0 /var/run/postgresql:5432 - accepting connections
0 /var/run/postgresql:5432 - accepting connections

Signification : Le healthcheck s’exécute et passe. C’est un prérequis si vous comptez sur service_healthy.

Décision : Si les healthchecks ne se déclenchent pas, vérifiez que l’image supporte le comportement HEALTHCHECK et que Compose est correctement configuré.

Task 12: Measure cold-start time of the dependency

cr0x@server:~$ time docker compose up -d db && docker inspect --format '{{.State.Health.Status}}' stack-db-1
healthy

real	0m18.412s
user	0m0.071s
sys	0m0.052s

Signification : La BD a mis ~18 secondes pour être healthy dans ce run.

Décision : Réglez les timeouts et fenêtres de retry des dépendants sur la réalité mesurée, pas sur des impressions. Si la CI est plus lente, mesurez là aussi.

Task 13: Verify your app’s readiness endpoint from inside the network

cr0x@server:~$ docker exec -it stack-api-1 curl -fsS http://localhost:8080/ready
{"status":"ready","db":"ok","queue":"ok"}

Signification : L’appli se déclare prête et vérifie ses propres dépendances.

Décision : Si cet endpoint ment, corrigez-le. Votre orchestration n’est fiable que dans la mesure où le signal que vous fournissez l’est.

Task 14: Catch “port is open but service is not ready” with HTTP status

cr0x@server:~$ docker exec -it stack-api-1 curl -i http://localhost:8080/
HTTP/1.1 503 Service Unavailable
Content-Type: application/json
Content-Length: 62

{"error":"warming up","details":"migrations running"}

Signification : Le serveur est vivant mais pas prêt. C’est un bon comportement.

Décision : Faites en sorte que votre healthcheck appelle /ready, pas /. Gardez / pour le comportement utilisateur si vous le souhaitez.

Task 15: Identify slow storage as the real “readiness bug”

cr0x@server:~$ docker exec -it stack-db-1 bash -lc 'dd if=/dev/zero of=/var/lib/postgresql/data/.bench bs=1M count=256 conv=fsync'
256+0 records in
256+0 records out
268435456 bytes (268 MB, 256 MiB) copied, 9.82 s, 27.3 MB/s

Signification : Si vous voyez des dizaines de Mo/s avec fsync, la « readiness BD » peut simplement être due au disque lent.
Les conteneurs ne contredisent pas la physique.

Décision : Si le stockage est lent, augmentez les timeouts de readiness et corrigez le disque sous-jacent (ou déplacez les volumes), plutôt que de parsemer des sleeps dans le code applicatif.

Playbook de diagnostic rapide

Quand la stack ne démarre pas, n’agitez pas inutilement. Exécutez ceci dans l’ordre. L’objectif est de trouver le goulot en moins de cinq minutes.

Premier : confirmez si vous avez un signal de readiness ou non

  • Exécutez docker compose config et cherchez les blocs healthcheck et toute condition de dépendance.
  • Exécutez docker compose ps et vérifiez si les dépendances sont (health: starting), (health: unhealthy), ou n’ont pas de health du tout.

S’il n’y a pas de healthcheck, votre « readiness » est de la pensée magique. Ajoutez-en un.

Second : déterminez si l’échec est réseau/DNS vs protocole/auth

  • Depuis le conteneur en échec : getent hosts <service> (DNS)
  • Puis : check TCP sur le port (connectivité)
  • Puis : opération client réelle (protocole/auth)

Cette séquence évite l’erreur classique : passer une heure à tuner la BD alors que le hostname est faux.

Troisième : stoppez les tempêtes de redémarrage avant qu’elles n’aillent masquer l’erreur réelle

  • Vérifiez la politique de redémarrage. Si ça claque, mettez temporairement le service dépendant à l’arrêt : docker compose stop api.
  • Montez la dépendance seule. Rendez-la healthy d’abord.
  • Puis démarrez l’app et observez le premier échec, pas le 50ème.

Quatrième : vérifiez le stockage et la contention CPU

  • Une BD qui reste « starting » éternellement signifie souvent disque lent, blocages fsync, ou pression mémoire.
  • Mesurez avec un test d’écriture fsync rapide ou inspectez les métriques de l’hôte si disponibles.

Blague n°2 : Compose n’a pas de flag « wait for SAN », parce qu’admettre que vous avez un SAN est déjà une forme de vérification de readiness.

Erreurs courantes : symptômes → cause racine → correction

« J’ai utilisé depends_on, pourquoi ça échoue encore ? »

Symptôme : Le service dépendant démarre puis échoue immédiatement en se connectant à la BD/queue.

Cause racine : depends_on gère l’ordre de démarrage, pas la readiness.

Correction : Ajoutez un vrai healthcheck à la dépendance ; verrouillez avec service_healthy si supporté ; ajoutez retry/backoff dans l’app.

« Le healthcheck dit healthy mais l’appli plante sur les migrations »

Symptôme : BD healthy, app échoue avec « relation does not exist » ou « database does not exist ».

Cause racine : Le healthcheck n’a validé que la connectivité, pas la disponibilité du schéma/des données.

Correction : Ajoutez un job init qui exécute les migrations idempotentes ; ou faites dépendre la readiness de la fin des migrations ; ou faites que le healthcheck teste l’existence des objets requis.

« Temporary failure in name resolution » au démarrage

Symptôme : La lookup DNS échoue une fois ; l’app sort ; redémarre ; parfois ça marche.

Cause racine : Course DNS au démarrage + l’app ne retry pas.

Correction : Retry DNS/connexion avec backoff sur une fenêtre bornée ; réduire l’agressivité des redémarrages ; éviter de planter au premier échec de lookup.

« Connection refused » alors que le service est up

Symptôme : Le conteneur cible tourne ; les clients voient ECONNREFUSED.

Cause racine : Service pas encore à l’écoute, mauvais port, mauvaise interface de binding, ou config de sécurité rejetant tôt.

Correction : Vérifiez ss -lntp dans le conteneur ; vérifiez le mapping de ports vs port interne ; confirmez l’adresse d’écoute ; utilisez un check de readiness conscient du protocole.

« Ça marche après avoir ajouté sleep 30, donc c’est bon »

Symptôme : Les flakiness disparaissent localement ; la CI flake encore ; les redeploys production sont lents.

Cause racine : Le sleep est une estimation ; le temps de démarrage varie avec I/O, CPU et chemins d’init.

Correction : Supprimez les sleeps. Remplacez par des healthchecks + gating + retries. Mesurez le warm-up des dépendances et ajustez les timeouts sur la réalité mesurée.

« Tout est healthy mais les requêtes échouent pendant 2 minutes »

Symptôme : Les healthchecks passent, mais l’app renvoie des 500 parce qu’elle réchauffe des caches ou reconstruit des index.

Cause racine : Votre définition de readiness est incorrecte ; vous testez la liveness.

Correction : Implémentez un endpoint de readiness qui vérifie les dépendances critiques et l’achèvement du warm-up interne ; healthcheckez cet endpoint.

« Le redémarrage corrige le problème »

Symptôme : Premier démarrage échoue ; deuxième marche.

Cause racine : Problème d’ordre d’initialisation caché (utilisateurs/BD/schéma créés au premier run).

Correction : Sortez l’init dans un job one-shot, ou rendez-le idempotent et répétable en sécurité. Assurez-vous que l’app attend la fin de l’init.

Trois mini-récits d’entreprise venus des tranchées

Mini-récit 1 : L’incident causé par une fausse hypothèse

Une SaaS de taille moyenne utilisait un environnement d’« intégration » interne avec Docker Compose sur une VM costaude. Ce n’était pas la production,
mais c’était l’endroit où les ingénieurs validaient les changements avant livraison. La stack incluait un conteneur Postgres et un conteneur API.
Quelqu’un a ajouté depends_on: [db] et s’en est senti responsable. Il l’était.

L’API avait un chemin de démarrage qui appliquait automatiquement les migrations. La plupart du temps, Postgres s’initialisait assez vite pour que la première tentative de connexion de l’API réussisse.
Certains jours—après des redémarrages d’hôte ou quand le cache disque de la VM était froid—Postgres mettait plus de temps à accepter les connexions.
L’API tentait une fois, échouait, et sortait. La politique de redémarrage la ramenait. La seconde tentative fonctionnait généralement.

Puis un changement est arrivé qui a ralenti le démarrage de Postgres : extensions supplémentaires installées au premier démarrage, plus de scripts d’init.
L’API a alors échoué trois ou quatre fois avant de réussir. Les ingénieurs voyaient des logs en flap, relançaient docker compose up, et passaient à autre chose.
Le jour où ça comptait, l’environnement servait de démo client. L’API ne s’est jamais stabilisée car la tempête de redémarrages provoquait des tentatives de migration répétées,
chacune verrouillant des tables et prolongeant le démarrage.

La fausse hypothèse n’était pas « depends_on fonctionne ». Elle était plus subtile : « si ça finit par se lever, c’est bon ».
C’est ainsi que des échecs intermittents de démarrage deviennent des pannes complètes sous charge ou lors de moments sensibles.
La correction a été ennuyeuse : un healthcheck Postgres, verrouillage sur service_healthy, et migration déplacée vers un job one-shot
qui s’exécutait exactement une fois par déploiement et logguait fortement en cas d’échec.

Mini-récit 2 : L’optimisation qui s’est retournée contre eux

Une autre organisation recherchait des CI plus rapides. Ils ont réduit agressivement les images : bases plus petites, moins de paquets, moins d’utilitaires « inutiles ».
Quelqu’un a retiré les outils clients Postgres d’une image applicative parce que « nous n’avons pas besoin de psql en production ».
Vrai, en grande partie. Mais ils utilisaient aussi psql dans un script de readiness au démarrage pour vérifier l’existence du schéma.

Le pipeline a commencé à échouer. Pas systématiquement—parce que le cache gardait d’anciens layers, et certaines exécutions utilisaient différents chemins de build.
Sur les runs qui échouaient, le conteneur API démarré tentait d’exécuter psql et renvoyait psql: command not found.
Le conteneur sortait, la politique de redémarrage relançait, et le job time-outait. On accusait la base de données. Elle était innocente.

L’« optimisation » a empiré : pour réduire le bruit dans les logs, quelqu’un a modifié l’entrypoint pour avaler l’erreur et retomber sur un check basé sur le port.
Maintenant le conteneur « attendait » le TCP 5432 et lançait l’app.
L’app rencontrait alors immédiatement « relation missing » parce que les migrations n’étaient pas garanties, et l’échec se déplaçait plus loin dans la séquence de boot.

Finalement, ils ont fait ce qui aurait dû être fait en premier : remplacer le script ad-hoc par un vrai healthcheck de dépendance sur Postgres,
et l’app a gagné une boucle de retry bornée pour les connexions BD.
Si un check de schéma était vraiment nécessaire, ils ont ajouté un conteneur de migration dédié qui contenait les bons outils et n’avait qu’une seule mission.
La CI est devenue plus rapide, mais surtout plus prévisible.

Mini-récit 3 : La pratique ennuyeuse mais correcte qui a sauvé la mise

Une équipe de services financiers exécutait plusieurs stacks Compose pour des environnements de test sur des hôtes partagés.
Ces environnements n’étaient pas des « jouets » : ils servaient pour des répétitions d’incident et la validation de rollback.
L’équipe avait une règle : chaque dépendance doit avoir un healthcheck qui correspond à une opération client réelle,
et chaque appli doit retry les dépendances critiques au démarrage.

Cela alourdissait un peu leurs fichiers Compose. Ça a aussi raccourci leur vie, dans le bon sens.
Ils avaient des endpoints de readiness pour les services HTTP, des healthchecks de base de données qui effectuaient l’authentification, et des services init one-shot pour les migrations.
Ils avaient aussi réglé les politiques de redémarrage délibérément : les bases ne redémarraient pas à chaque échec transitoire, et les apps n’attaquaient pas les dépendances avec des retries instantanés.

Un matin, après un cycle de patchs d’hôte, plusieurs environnements sont revenus plus lents que d’habitude.
Le stockage était temporairement dégradé après un resync RAID. Les conteneurs Postgres ont mis plus de temps à être prêts.
Les stacks ne se sont pas effondrées en tempêtes de redémarrage. Les apps ont attendu. Les healthchecks sont restés « starting » jusqu’à ce que la BD soit réellement utilisable.

L’équipe l’a remarqué, parce qu’elle monitorait l’état de santé et le temps de démarrage, pas seulement « le conteneur tourne ».
Ils ont repoussé une répétition prévue de 20 minutes au lieu de passer deux heures à se disputer avec des fantômes.
Les pratiques ennuyeuses ne font pas les héros. Elles empêchent juste que des héros soient nécessaires.

Listes de contrôle / plan étape par étape

Étape par étape : convertir une stack Compose instable en une stack fiable

  1. Définir la readiness par dépendance.
    Pour une BD : « peut s’authentifier et exécuter une requête ». Pour les services HTTP : « /ready retourne ok et les dépendances en aval sont ok ».
  2. Ajouter des healthchecks à chaque dépendance stateful.
    Évitez les checks purement basés sur le port sauf si c’est vraiment votre seul besoin.
  3. Verrouiller le démarrage des dépendants sur la santé quand c’est supporté.
    Si votre Compose supporte service_healthy, utilisez-le. Sinon, reposez-vous sur le retry applicatif et considérez le pattern init job.
  4. Ajouter des retries bornés avec backoff dans l’application.
    Incluez les échecs de lookup DNS, les timeouts de connexion et les erreurs d’auth qui peuvent survenir pendant l’initialisation.
  5. Séparer le travail d’init du travail de service.
    Migrations, création de buckets, templates d’index vont dans un service one-shot pouvant être relancé sans risque.
  6. Rendre l’init idempotent.
    Utilisez « create if not exists », migrations transactionnelles et relances sûres. Supposez qu’il s’exécutera deux fois.
  7. Ajuster les politiques de redémarrage pour éviter les tempêtes.
    Si un service échoue parce que les dépendances ne sont pas prêtes, il ne doit pas redémarrer 20 fois par minute.
  8. Instrumenter le temps de démarrage.
    Logguez « starting », « connecté à la BD », « migrations terminées », « ready ». Vous ne pouvez pas réparer ce que vous ne mesurez pas.
  9. Tester les cold starts en CI.
    Purgez les caches de temps en temps ou exécutez sur des runners propres. Mesurez le chemin lent.
  10. Arrêtez d’utiliser des sleeps comme mécanisme de contrôle.
    Remplacez-les par des vérifications représentant la vraie readiness, ou retirez-les et comptez sur les retries.

Checklist : à quoi ressemble un « bon » healthcheck

  • S’exécute rapidement (idéalement < 1s) quand le service est sain.
  • Échoue de façon fiable quand le service n’est pas utilisable pour les dépendants.
  • Utilise un protocole client réel quand c’est possible (requête SQL, requête HTTP).
  • Ne nécessite pas d’accès réseau externe ou de dépendances instables.
  • Possède des intervalles et des retries sensés basés sur le temps de démarrage mesuré.
  • Fournit une sortie d’échec claire dans docker inspect.

Checklist : ce que votre app devrait faire au démarrage

  • Retry des connexions aux dépendances pendant une fenêtre bornée (par ex. 60–180 secondes selon l’environnement).
  • Utiliser un backoff exponentiel avec jitter pour éviter des essaims de retries synchronisés.
  • Logger chaque tentative ratée avec la raison, mais ne pas spammer : agrégez ou limitez le débit si nécessaire.
  • Sortir avec une erreur claire si la dépendance n’est pas joignable après la fenêtre.
  • Exposer un endpoint de readiness qui reflète la réelle capacité à servir.

FAQ

1) Est-ce que depends_on attend parfois la readiness ?

Pas tout seul. Certaines implémentations de Compose supportent des conditions comme service_healthy, qui peuvent verrouiller sur un healthcheck.
Mais le healthcheck doit exister et représenter la vraie readiness.

2) Un check de port TCP est-il un healthcheck valide ?

Parfois. Si votre service dépendant ne requiert que « port ouvert », très bien. C’est rare.
La plupart des services nécessitent authentification, routage, schéma ou initialisation interne—donc un check au niveau protocole est plus sûr.

3) Pourquoi ne pas simplement augmenter le sleep à 60 secondes ?

Parce que le temps de démarrage est variable. Vous ralentirez les chemins rapides et échouerez toujours sur les chemins lents.
De plus, les sleeps masquent de vrais problèmes : identifiants erronés, mauvais hostnames, migrations manquantes.

4) Faut-il faire les migrations au démarrage de l’app ?

Si vous le faites, vous devez gérer proprement la concurrence, l’idempotence et les échecs.
Dans les stacks Compose, un service de migration one-shot est généralement plus propre et plus facile à observer.

5) Quelle est la différence entre liveness et readiness ici ?

Liveness : « le process est vivant et pas bloqué ». Readiness : « il peut servir correctement des requêtes maintenant ».
Les healthchecks Compose sont souvent utilisés pour la liveness ; vous pouvez les utiliser pour la readiness, mais seulement si vous les définissez ainsi.

6) Mon service est « healthy » mais toujours injoignable depuis un autre conteneur. Comment ?

Le healthcheck s’exécute à l’intérieur du conteneur. Il peut réussir même si le service n’est pas joignable sur le réseau (mauvaise adresse de bind, mauvais réseau, règles de pare-feu).
Vérifiez l’adresse d’écoute (ss -lntp) et l’appartenance réseau (docker network inspect).

7) Utiliser restart: always—bon ou mauvais ?

Ni l’un ni l’autre. C’est un outil. Pour des dépendances qui peuvent crasher, ça peut aider.
Pour des apps qui échouent vite parce que les dépendances ne sont pas prêtes, ça peut créer des tempêtes de redémarrage et masquer les causes racines.
Associez-le à une logique de retry sensée et à des logs clairs.

8) Puis-je compter sur l’ordre de docker compose up pour les bases de données et caches ?

Vous pouvez compter sur le fait que « Compose va essayer de démarrer les conteneurs dans cet ordre ».
Vous ne pouvez pas compter sur « la dépendance est utilisable quand le dépendant démarre ».
Si votre app a besoin d’une BD utilisable, vous avez besoin de checks de readiness et de retries.

9) Comment gérer plusieurs dépendances (BD + queue + stockage d’objets) ?

Définissez la readiness au niveau de la frontière applicative : créez un endpoint de readiness qui vérifie toutes les dépendances critiques.
Verrouillez le trafic sur cette readiness, et assurez-vous que chaque dépendance a son propre healthcheck quand c’est possible.

Conclusion : prochaines étapes à faire réellement

Cessez de demander à depends_on d’accomplir un travail pour lequel il n’a jamais été engagé. Utilisez-le pour l’ordre de démarrage si vous le souhaitez.
Mais pour la fiabilité, vous avez besoin de vrais signaux de readiness, et de systèmes capables de tolérer les courses de démarrage.

  1. Ajoutez des healthchecks à chaque dépendance qui compte (BD, cache, queue, gateways de stockage d’objets).
  2. Faites en sorte que les healthchecks représentent l’utilisabilité réelle, pas « port ouvert ».
  3. Si possible, verrouillez le démarrage sur service_healthy. Sinon, traitez-le comme un nice-to-have et reposez-vous sur les retries applicatifs.
  4. Déplacez les migrations et initialisations one-time dans un conteneur job dédié et idempotent.
  5. Implémentez un retry borné avec backoff dans chaque service qui contacte une dépendance au démarrage.
  6. Utilisez le playbook de diagnostic rapide quand ça casse encore, parce que ça cassera—mais moins dramatiquement.

L’objectif n’est pas la perfection. C’est des démarrages ennuyeux. Les démarrages ennuyeux vous permettent de consacrer votre attention aux problèmes produit plutôt qu’à la roulette du boot.

← Précédent
CSS Grid vs Flexbox : règles de décision et recettes de mise en page robustes en production
Suivant →
Invalidation du cache de build Docker : pourquoi les builds sont lents et comment les accélérer

Laisser un commentaire