Docker : ordre de démarrage vs disponibilité — l’approche qui empêche les faux démarrages

Cet article vous a aidé ?

Le pire type d’incident est celui qui semble s’être résolu tout seul. Votre pile « se lève », les tableaux de bord passent au vert,
puis cinq minutes plus tard l’application commence à renvoyer des 500 parce que la base de données n’était pas réellement prête — juste techniquement « démarrée ».
Pendant ce temps, votre orchestrateur a fait exactement ce que vous lui avez demandé. C’est ça qui fait mal.

Docker et Docker Compose sont des outils peu subtils : ils peuvent démarrer des conteneurs dans un ordre donné, mais ils ne peuvent pas
deviner quand une dépendance est sûre à utiliser à moins que vous ne leur précisiez ce que signifie « prêt ». Le remède consiste à séparer ordre de démarrage
et disponibilité, à intégrer des vérifications basées sur des preuves et à rendre les « faux démarrages » impossibles ou au moins audibles.

Ordre de démarrage vs disponibilité : la différence qui compte

Ordre de démarrage répond à : « Le processus a-t-il été lancé ? » C’est tout. Docker sait faire ça : le conteneur A démarre,
puis le conteneur B démarre. Propre et net. Mais aussi dangereusement incomplet.

Disponibilité répond à : « Le service est-il utilisable par ses clients maintenant ? » Cela inclut : sockets à l’écoute,
migrations terminées, caches chauds, clés TLS chargées, élection du leader effectuée, permissions correctes et dépendances accessibles.
La disponibilité est un contrat entre un service et le monde. Docker n’infère pas ce contrat. Vous devez l’implémenter.

Les faux démarrages arrivent quand on prend l’ordre de démarrage pour un substitut de la disponibilité. C’est comme déclarer un restaurant ouvert parce
que les lumières sont allumées alors que le chef négocie encore avec une boîte de frites surgelées.

Une pile fiable fait deux choses :

  • Elle démarre les composants dans un ordre sensé quand cela aide.
  • Elle conditionne les composants dépendants à une disponibilité prouvée, pas à l’optimisme.

Vous entendrez parfois « ajoutez juste des sleeps ». Ce n’est pas de la disponibilité ; c’est de la superstition horodatée.
Votre vous du futur va vous détester, et votre canal d’incident vous détestera plus tôt.

Pourquoi les faux démarrages arrivent (et pourquoi ils sont si courants)

« Démarré » est un état peu coûteux. Un processus peut démarrer tout en restant inutile. Dans les systèmes modernes, « utile » dépend souvent de :
accessibilité réseau, état du système de fichiers, identifiants, migrations de schéma, et services en amont. Chacun de ces éléments peut être
retardé par rapport au démarrage du processus de quelques secondes ou minutes.

Modes d’échec classiques qui produisent des faux démarrages

  • Le socket n’écoute pas encore. L’application boot, puis se lie à un port plus tard. Les clients tentent immédiatement et échouent.
  • Le port écoute, mais le service n’est pas prêt. HTTP renvoie 503 parce que des migrations ou un warm-up de cache s’exécutent.
  • La base accepte le TCP mais pas les requêtes. PostgreSQL accepte les connexions pendant qu’il rejoue des WAL ou effectue une récupération.
  • Le DNS n’est pas stabilisé. Le nom du conteneur existe, mais les caches/sidecars de résolution ne sont pas prêts.
  • La dépendance est prête, mais avec le mauvais schéma. L’application démarre avant les migrations, puis échoue avec « relation does not exist ».
  • Volume non monté ou permissions incorrectes. Le service démarre, écrit nulle part, puis s’effondre sous ses propres mensonges.
  • Limites de débit et herds tonitruants. Dix réplicas « démarrent », s’abattent tous sur une dépendance, et vous découvrez ce que « retry storm » signifie.

Voici la vérité inconfortable : beaucoup d’apps sont écrites en supposant qu’un humain les redémarrera si quelque chose se passe mal au démarrage.
Les conteneurs enlèvent l’humain et le remplacent par de l’automatisation qui va volontiers réessayer la même erreur indéfiniment.

Blague #1 : si vous pensez que depends_on signifie works_on, Docker a un pont à vous vendre — et il est probablement en maintenance.

Faits & contexte historique (court et concret)

  1. La fonction « link » initiale de Docker (avant la maturité de Compose) essayait de relier les dépendances en injectant des variables d’environnement ; ça n’a pas résolu la disponibilité.
  2. Le format de fichier Compose v2 a popularisé depends_on pour l’ordre de démarrage ; de nombreuses équipes l’ont mal interprété comme un verrouillage de disponibilité.
  3. Compose v3 a recentré l’attention sur Swarm et a retiré certains sémantiques de démarrage conditionnel ; les gens ont continué à supposer le comportement antérieur.
  4. Kubernetes a introduit les readiness probes comme concept de première classe parce que « container running » n’a jamais suffi pour le routage du service.
  5. systemd a longtemps eu l’ordonnancement de dépendances, et même lui distingue « démarré » de « prêt » via des mécanismes de notification.
  6. PostgreSQL peut accepter des connexions TCP avant d’être complètement disponible pour la charge (recovery, replay de crash, checkpoints).
  7. Les variantes MySQL peuvent écouter tôt mais refuser l’authentification ou verrouiller des tables internes pendant l’initialisation, créant un piège de faux démarrage.
  8. Les healthchecks ont été ajoutés à Docker pour dépasser le seul signal « processus existe » ; ils restent sous-utilisés ou mal utilisés.

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

depends_on : ordre, pas disponibilité

Dans Compose, depends_on contrôle l’ordre de démarrage/arrêt. Par défaut, il n’attend pas que la dépendance soit
prête. Il s’assure que Docker a tenté de démarrer le conteneur. C’est tout.

Compose peut utiliser des healthchecks pour verrouiller le démarrage dans certains modes, mais la réalité opérationnelle est bordélique :
les gens utilisent différentes versions de Compose, différents moteurs Docker, et des attentes façonnées par de vieux billets de blog.
Si vous voulez de la fiabilité, vous intégrez la logique de disponibilité dans votre pile d’une façon explicite et testable.

Healthcheck : utile, mais il faut le concevoir

Un healthcheck est un test périodique exécuté par l’engine. S’il échoue, Docker marque le conteneur comme unhealthy. C’est un signal.
Ce que vous en faites — redémarrages, verrouillage, alerting — est un choix séparé.

Si votre healthcheck est « curl localhost:8080 », mais que le service renvoie 200 alors qu’il échoue encore sur toutes les requêtes externes,
vous avez construit un menteur. Les menteurs réussissent les tests et déçoivent les clients.

Politiques de restart : pas de la disponibilité, juste de la persistance

Les politiques de redémarrage servent à récupérer après des plantages. Elles ne sont pas une stratégie de dépendance. Si votre app
sort parce que la BD n’était pas prête, les restart policies transformeront un court warm-up en boucle de redémarrage. Cette boucle peut aussi amplifier la charge sur la BD.

Le seul signal de disponibilité qui compte : « Le client peut-il réussir ? »

La disponibilité doit être définie du point de vue du client. Si un service dépend d’une base de données et d’une queue, « prêt » signifie
qu’il peut se connecter, s’authentifier, exécuter une requête simple, et publier/consommer un petit message — ou faire ce que représente le « travail viable minimal »
pour votre système.

Une citation pour rester honnête, paraphrasant une idée de John Allspaw : idée paraphrasée : la fiabilité vient de l’apprentissage et des boucles de feedback, pas de faire comme si les pannes n’arrivaient pas.

Schémas de disponibilité qui fonctionnent en production

Schéma 1 : Mettre de vrais healthchecks sur les dépendances

Si vous exécutez PostgreSQL, Redis ou une API HTTP dans un conteneur, donnez-lui un healthcheck qui reflète l’utilisabilité réelle.
Pour Postgres, cela peut être pg_isready plus une vraie requête si vous avez besoin de validation de schéma. Pour HTTP, ciblez le endpoint
qui vérifie les dépendances, pas une route statique « OK ».

Schéma 2 : Verrouiller explicitement les services dépendants sur la disponibilité

Il existe trois approches de verrouillage courantes :

  • Verrouillage Compose via le statut de santé (lorsque supporté dans votre environnement) : depends_on + conditions de santé.
  • Scripts d’attente dans l’entrypoint à l’intérieur du conteneur dépendant : attendre TCP + validation au niveau application ; puis démarrer.
  • Retries natifs de l’application avec backoff et jitter : la meilleure réponse à long terme, car elle fonctionne partout, pas seulement sous Docker.

L’approche la plus durable est : l’app réessaie correctement et l’orchestrateur a des healthchecks. Ceinture et bretelles.
C’est de l’opérationnel. On s’habille pour le temps qu’on a, pas pour celui qu’on mérite.

Schéma 3 : Faire des migrations un job de première classe

Les migrations de schéma ne sont pas un effet secondaire. Traitez-les comme une étape séparée et explicite : un conteneur/job one-shot qui exécute
les migrations et sort avec succès. Alors et seulement alors démarrez les conteneurs applicatifs. Cela évite la « course des cinq apps pour migrer le schéma ».

Schéma 4 : Utiliser un endpoint de readiness qui vérifie les dépendances

Pour les services HTTP, exposez :

  • /healthz (liveness) : « le processus est-il vivant ? »
  • /readyz (readiness) : « puis-je servir du vrai trafic ? » (BD joignable, queue joignable, configs critiques chargées)

Même dans Docker Compose, cela aide parce que votre healthcheck peut cibler /readyz plutôt que deviner.

Schéma 5 : Limitez vos retries, puis échouez bruyamment

Les retries infinis peuvent cacher de vraies pannes. Des retries limités avec des logs clairs vous donnent un délai de démarrage contrôlé quand les choses sont lentes, mais échouent si le monde est réellement cassé.

Schéma 6 : Ajoutez du jitter et du backoff

Si 20 conteneurs démarrent en même temps et frappent tous la BD toutes les 100 ms, vous vous créez un DDoS auto-infligé. Backoff et jitter transforment les ruées en filets.

Blague #2 : « Ajoutez juste un sleep de 30 secondes » est la façon dont vous vous retrouvez avec une panne de 31 secondes et un postmortem de 3 heures.

Tâches pratiques : commandes, sorties, décisions (12+)

Voici les vérifications que j’exécute réellement quand une pile « démarrée » se comporte comme si elle était allergique aux lundis.
Chaque tâche inclut : commande, ce que signifie la sortie, et la décision que vous prenez.

Task 1: See container state and health at a glance

cr0x@server:~$ docker ps --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}'
NAMES              STATUS                          PORTS
app                Up 18 seconds (health: starting) 0.0.0.0:8080->8080/tcp
db                 Up 22 seconds (healthy)          5432/tcp
redis              Up 21 seconds (healthy)          6379/tcp

Signification : app est en cours d’exécution mais pas prête ; DB et Redis sont saines.

Décision : Ne routage pas encore le trafic. Si la santé de l’app reste « starting » trop longtemps, inspectez son healthcheck et les logs de démarrage.

Task 2: Inspect why a healthcheck is failing

cr0x@server:~$ docker inspect --format '{{json .State.Health}}' app
{"Status":"unhealthy","FailingStreak":3,"Log":[{"Start":"2026-01-03T10:10:01.123Z","End":"2026-01-03T10:10:01.456Z","ExitCode":1,"Output":"curl: (7) Failed to connect to localhost port 8080: Connection refused\n"}]}

Signification : L’app n’écoute pas encore, ou elle écoute sur un port/interface différent.

Décision : Vérifiez les logs de l’app et son adresse/port d’écoute. Si elle se lie à 127.0.0.1 à l’intérieur du conteneur, c’est correct pour le healthcheck mais pas pour les clients externes sauf si c’est publié correctement.

Task 3: Read startup logs with timestamps

cr0x@server:~$ docker logs --since 10m --timestamps app | tail -n 30
2026-01-03T10:09:44.001234567Z boot: loading config
2026-01-03T10:09:44.889012345Z db: connection failed: dial tcp db:5432: connect: connection refused
2026-01-03T10:09:45.889045678Z db: connection failed: dial tcp db:5432: connect: connection refused
2026-01-03T10:09:46.889078901Z boot: giving up after 2 retries

Signification : L’app a essayé deux fois et a quitté trop tôt. La BD n’était pas prête à ce moment-là.

Décision : Augmentez les retries/backoff ou verrouillez le démarrage jusqu’à ce que la disponibilité de la BD soit vérifiée. Évaluez aussi si « quitter si BD manquante » est le comportement souhaité.

Task 4: Confirm DB container logs indicate actual readiness, not just startup

cr0x@server:~$ docker logs --since 10m --timestamps db | tail -n 30
2026-01-03T10:09:30.100000000Z PostgreSQL init process complete; ready for start up.
2026-01-03T10:09:31.200000000Z database system is ready to accept connections

Signification : La BD a déclaré sa disponibilité à 10:09:31 ; l’app a commencé à échouer à 10:09:44. Ce décalage suggère soit un mauvais hostname, un problème réseau, soit un redémarrage de la BD.

Décision : Vérifiez le réseau des conteneurs et les redémarrages de la BD ; vérifiez que l’app utilise le bon nom de service et port.

Task 5: Verify container restart loops

cr0x@server:~$ docker ps -a --format 'table {{.Names}}\t{{.Status}}\t{{.RunningFor}}'
NAMES   STATUS                     RUNNING FOR
app     Restarting (1) 3 seconds   2 minutes
db      Up 2 minutes (healthy)     2 minutes

Signification : L’app plante et redémarre en boucle.

Décision : Arrêtez d’essayer de « corriger » en redémarrant. Inspectez le code de sortie et l’erreur ; corrigez ensuite la disponibilité ou la configuration.

Task 6: Get the app’s exit code and last failure reason

cr0x@server:~$ docker inspect --format 'ExitCode={{.State.ExitCode}} Error={{.State.Error}} FinishedAt={{.State.FinishedAt}}' app
ExitCode=1 Error= FinishedAt=2026-01-03T10:10:02.002002002Z

Signification : Sortie non nulle sans erreur au niveau engine ; le processus a choisi de sortir.

Décision : Traitez-le comme un problème d’application (gestion des dépendances, validation de configuration), pas comme un problème Docker.

Task 7: Confirm service discovery (DNS) inside the network

cr0x@server:~$ docker exec -it app getent hosts db
172.20.0.3      db

Signification : Le DNS pour db résout correctement à l’intérieur du conteneur.

Décision : Passez aux vérifications de connectivité et d’auth ; le DNS n’est pas votre goulot aujourd’hui.

Task 8: Test TCP connectivity to a dependency from the dependent container

cr0x@server:~$ docker exec -it app bash -lc 'nc -vz db 5432'
nc: connect to db (172.20.0.3) port 5432 (tcp) failed: Connection refused

Signification : La BD est joignable par IP mais n’accepte pas le TCP. Soit la BD n’écoute pas, soit elle est sur un autre port/interface, soit elle redémarre.

Décision : Inspectez les sockets d’écoute de la BD et l’historique des redémarrages ; vérifiez aussi si la BD est liée uniquement à localhost.

Task 9: Confirm the DB is listening on the expected port inside its container

cr0x@server:~$ docker exec -it db bash -lc 'ss -lntp | grep 5432 || true'
LISTEN 0      244          0.0.0.0:5432      0.0.0.0:*    users:(("postgres",pid=1,fd=7))

Signification : PostgreSQL écoute sur toutes les interfaces.

Décision : Si les clients voient toujours « connection refused », suspectez le timing (redémarrage de la BD) ou des règles réseau/iptables.

Task 10: Verify healthcheck command and timing parameters

cr0x@server:~$ docker inspect --format '{{json .Config.Healthcheck}}' db
{"Test":["CMD-SHELL","pg_isready -U postgres -h 127.0.0.1 -p 5432"],"Interval":3000000000,"Timeout":1000000000,"Retries":3,"StartPeriod":0}

Signification : Interval 3s, timeout 1s, retries 3, et StartPeriod est 0. Sur des disques lents ou une récupération après crash, cela peut marquer la BD comme unhealthy prématurément.

Décision : Ajoutez une période de démarrage (par ex. 30–60s) et augmentez le timeout. Les healthchecks doivent détecter une vraie panne, pas un warm-up normal.

Task 11: Detect slow storage causing delayed readiness

cr0x@server:~$ docker exec -it db bash -lc 'dd if=/var/lib/postgresql/data/pg_wal/000000010000000000000001 of=/dev/null bs=4M count=16 status=none; echo $?'
0

Signification : La lecture basique a réussi. Cela ne prouve pas les performances, mais écarte des erreurs I/O évidentes.

Décision : Si le démarrage reste lent, vérifiez la saturation I/O au niveau hôte et la latence du système de fichiers ; les délais de disponibilité se retracent souvent jusqu’au stockage.

Task 12: Check host resource pressure (CPU, memory, I/O) that prolongs warm-up

cr0x@server:~$ docker stats --no-stream
CONTAINER ID   NAME    CPU %     MEM USAGE / LIMIT     MEM %     NET I/O       BLOCK I/O
a1b2c3d4e5f6   app     180.12%   512MiB / 1GiB         50.00%    1.2MB / 800KB  12MB / 2MB
b2c3d4e5f6g7   db      95.33%    1.8GiB / 2GiB         90.00%    900KB / 1.1MB  2.3GB / 1.9GB

Signification : La BD est proche de la limite mémoire et consomme beaucoup de I/O bloc. C’est une recette pour une disponibilité lente et des échecs intermittents.

Décision : Augmentez la mémoire, réduisez shared buffers, déplacez les volumes vers un stockage plus rapide, ou réduisez le parallélisme au démarrage. Corrigez le goulot avant de « tuner » des healthchecks pour mentir.

Task 13: See Compose dependency graph and resolved configuration

cr0x@server:~$ docker compose config
services:
  app:
    depends_on:
      db:
        condition: service_healthy
    environment:
      DATABASE_URL: postgres://postgres:postgres@db:5432/app
  db:
    healthcheck:
      test:
      - CMD-SHELL
      - pg_isready -U postgres -h 127.0.0.1 -p 5432

Signification : Le verrouillage prévu est présent dans la sortie de config (bien), en supposant que votre implémentation de Compose le respecte.

Décision : Si le comportement contredit la config, vérifiez la version de Compose et le moteur ; ensuite envisagez de déplacer le verrouillage dans l’entrypoint ou les retries applicatifs pour la portabilité.

Task 14: Confirm Compose and Docker versions (behavior depends on this)

cr0x@server:~$ docker version --format 'Client={{.Client.Version}} Server={{.Server.Version}}'
Client=27.2.0 Server=27.2.0

Signification : Vous savez sur quel runtime vous êtes réellement. Cela compte quand quelqu’un jure « ça marchait avant ».

Décision : Alignez les versions entre environnements ou incorporez la disponibilité dans l’application pour que votre pile ne dépende pas des particularités des outils.

Task 15: Time the path to readiness

cr0x@server:~$ time docker compose up -d
[+] Running 3/3
 ✔ Container db     Started
 ✔ Container redis  Started
 ✔ Container app    Started

real    0m2.114s
user    0m0.082s
sys     0m0.061s

Signification : Compose rapporte « Started » rapidement. Ça ne signifie pas prêt.

Décision : Mesurez la disponibilité séparément (statut de santé, endpoints ready, vérifications synthétiques). Ne prenez pas la sortie de l’orchestration pour vérité.

Task 16: Run a synthetic “can I do real work” check

cr0x@server:~$ docker exec -it app bash -lc 'curl -fsS http://127.0.0.1:8080/readyz && echo READY'
READY

Signification : L’app revendique sa disponibilité pour le vrai trafic (à condition que votre /readyz soit honnête).

Décision : Ce n’est qu’à ce moment qu’il est raisonnable de mettre le service derrière un load balancer, d’ouvrir des règles de pare-feu, ou de déclarer le déploiement terminé.

Récapitulatif pour un diagnostic rapide

Quand « les conteneurs sont up » mais que le système n’est pas utilisable, la rapidité compte. L’astuce est d’éviter de deviner quelle dépendance ment.
Voici l’ordre qui trouve le goulot rapidement, avec un minimum de remue-ménage.

Premier : identifiez quel composant n’est pas prêt (pas lequel est « down »)

  • Exécutez docker ps et cherchez (health: starting) ou (unhealthy).
  • S’il n’y a pas de healthchecks, c’est votre premier problème. Mais vous diagnosez quand même avec les logs et des vérifications synthétiques.

Deuxième : corrélez les horodatages entre les logs

  • Récupérez les 5–10 dernières minutes de logs avec horodatage pour l’app et les dépendances.
  • Cherchez : connection refused, timeouts, échecs d’auth, erreurs de migration, erreurs disque.
  • Décidez si c’est un problème de timing (dépendance en warm-up) ou une mauvaise configuration (mauvais hostname/identifiants).

Troisième : testez depuis l’espace de noms réseau du client

  • Depuis l’intérieur du conteneur dépendant, testez DNS, puis TCP, puis le protocole au niveau application.
  • Ne testez pas depuis l’hôte en supposant que c’est équivalent. Chemin réseau différent, vérité différente.

Quatrième : vérifiez la pression sur les ressources et la latence du stockage

  • Utilisez docker stats pour repérer la saturation CPU/mémoire/disque.
  • Si la BD est lente à être prête, suspectez d’abord l’I/O. Les apps sont impatientes ; les disques sont éternels.

Cinquième : décidez quelle couche doit corriger

  • Correction applicative : retries avec backoff/jitter ; endpoint de readiness ; meilleure gestion d’erreur.
  • Correction Compose : healthchecks ; verrouillage ; ordonnancement des jobs de migration.
  • Correction plateforme : performances du stockage ; limites de ressources ; éviter les voisins bruyants.

Trois mini-récits du monde corporate

Incident causé par une fausse hypothèse : « Démarré » voulait dire « prêt »

Une entreprise de taille moyenne exécutait une pile Docker Compose pour un portail de facturation interne : application web, service API, PostgreSQL, et un worker en arrière-plan.
Les déploiements étaient « simples » : pull des images, docker compose up -d, terminé. Pendant des mois tout semblait bien.
Puis un reboot d’hôte de routine s’est transformé en incident d’une demi-journée.

Après le reboot, Compose a démarré les conteneurs dans l’ordre attendu. Le conteneur API est revenu, a tenté d’exécuter une migration au démarrage,
et a échoué immédiatement parce que Postgres rejouait encore des WAL. L’API est sortie avec un code non nul. La politique de restart l’a relancée.
Elle a échoué à nouveau. Et encore. La migration n’a jamais tourné, l’API n’est jamais restée assez longtemps pour accepter des requêtes, et le worker
a assommé la queue de messages avec des retries non limités.

Le dashboard affichait « containers running » parce que le conteneur de la base était up et que les autres redémarraient constamment.
L’ingénieur de garde a d’abord ciblé le load balancer parce que les utilisateurs voyaient des 502. Détournement classique.
Ce n’est qu’après avoir comparé les horodatages des logs que tout s’est éclairé : l’API dépendait d’un état BD qui n’était pas garanti.

La correction fut ennuyeuse mais décisive : les migrations ont été déplacées dans un conteneur one-shot qui tournait après que la BD soit healthy, et l’API
a reçu un backoff exponentiel sur la connexion BD. Ils ont aussi ajouté un endpoint de readiness qui échouait tant que les migrations n’étaient pas terminées.
Le reboot suivant fut sans événement. L’équipe de facturation n’a pas envoyé de fleurs, mais elle n’a pas non plus envoyé d’e-mails rageurs, ce qui est l’équivalent SRE.

Optimisation qui s’est retournée contre eux : des healthchecks « tuned » pour mentir

Une autre organisation avait un environnement dev Compose qui ressemblait à la prod : microservices, BD centrale, moteur de recherche.
Les développeurs se plaignaient que la mise en route prenait trop de temps, surtout sur les laptops. Quelqu’un a « optimisé » en rendant les healthchecks
très agressifs : intervalles d’une seconde, timeouts d’une seconde, et pas de start period. L’UI affichait « healthy » plus vite les bons jours.

Un jour moyen — comme quand le moteur de recherche avait besoin de plus de temps pour initialiser les index — le conteneur était marqué unhealthy trop tôt.
Un script wrapper interprétait unhealthy comme « cassé », le tuait et le redémarrait. Cela a créé une boucle où le service n’obtenait jamais assez de temps ininterrompu pour finir son initialisation.
L’optimisation n’a pas réduit le temps de démarrage ; elle a empêché le démarrage.

L’équipe a ensuite couru après des fantômes : DNS, ports, paramètres Java heap. Tout sauf l’évidence : leur propre politique de healthcheck saboteait le système.
Ils avaient construit un test de disponibilité qui punissait l’initialisation normale.

La résolution finale : ajout de start periods, augmentation des timeouts, et changement du healthcheck du moteur de recherche de « port ouvert » à « état du cluster yellow/green »
(un signal que l’initialisation a franchi un seuil significatif). Le démarrage a pris un peu plus de temps. Il a aussi démarré à chaque fois. Voilà ce que « plus rapide » signifie en production : moins de retries, moins de boucles, moins de mensonges.

Pratique ennuyeuse mais correcte qui a sauvé la mise : une porte synthétique « ready » en CI

Une entreprise régulée exécutait des tests d’intégration nocturnes contre un environnement Compose. Ils avaient une habitude apparemment douloureuse :
chaque pipeline comportait une étape explicite « wait for readiness » via un petit script qui sondait les endpoints de readiness des services et exécutait une requête minimale BD.
Ce n’est que lorsque cela passait que les tests commençaient. Les ingénieurs râlaient parfois à propos de la minute supplémentaire.

Puis une mise à jour d’image de la BD est arrivée. La nouvelle image effectuait une étape d’initialisation supplémentaire lorsqu’elle détectait une condition particulière sur le système de fichiers.
Sur certains runners, cette étape prenait suffisamment de temps pour que les conteneurs applicatifs démarrent et échouent immédiatement leur connexion BD initiale.
Sans verrouillage, les tests auraient démarré pendant le clignotement et auraient produit des échecs aléatoires.

Au lieu de cela, le pipeline a simplement attendu. Quand la disponibilité n’est pas arrivée dans le délai configuré, il a échoué clairement avec :
« DB not ready after N seconds. » Pas de tests flakys. Pas d’artefacts à moitié cassés. L’équipe a rollbacké l’image et créé un ticket interne pour figer les versions jusqu’à ce qu’ils comprennent le nouveau comportement.

La pratique ennuyeuse a payé : l’échec fut déterministe, localisé et facile à diagnostiquer. C’est le rêve.
C’est aussi pourquoi je répète : traitez la disponibilité comme un signal de première classe, pas comme une impression.

Erreurs courantes : symptôme → cause racine → correction

1) « Connection refused » au boot, puis ça marche après un redémarrage manuel

Symptôme : l’app échoue immédiatement avec connection refused à la BD/Redis, puis fonctionne si vous redémarrez plus tard le conteneur app.

Cause racine : la dépendance a été démarrée mais n’écoutait pas encore ; l’app n’a pas de retries ou n’a pas une fenêtre de retry suffisante.

Correction : ajoutez un backoff exponentiel dans l’app ; ajoutez une porte de readiness (healthcheck + gating ou entrypoint wait) pour les dépendances.

2) Conteneur app est « Up », mais chaque requête échoue

Symptôme : l’état du conteneur est running ; le healthcheck est vert ; les utilisateurs voient des 500.

Cause racine : le healthcheck ne teste que la liveness (port ouvert) et pas la readiness (succès des dépendances).

Correction : implémentez /readyz qui vérifie les dépendances critiques ; pointez le healthcheck du conteneur dessus.

3) « Unhealthy » pendant le warm-up normal, provoquant des boucles de redémarrage

Symptôme : le service ne se stabilise jamais ; les logs montrent les étapes de démarrage qui se répètent.

Cause racine : start period du healthcheck trop court ou absent ; script wrapper redémarre sur unhealthy.

Correction : configurez start_period et un timeout raisonnable ; redémarrez seulement en cas de crash, pas en cas d’unhealthy précoce, sauf si c’est vraiment voulu.

4) Échecs aléatoires qui disparaissent quand vous sérialisez le démarrage

Symptôme : démarrer les services un par un fonctionne ; les démarrages en parallèle échouent de façon intermittente.

Cause racine : ruée sur une dépendance partagée (BD, service d’auth, fournisseur de secrets) plus des retries agressifs.

Correction : ajoutez jitter/backoff ; étalez les démarrages ; augmentez la capacité de la dépendance ; exécutez les migrations séparément.

5) « Authentication failed » au boot, puis OK plus tard

Symptôme : échecs d’auth transitoires pour la BD ou des clés API juste après le démarrage.

Cause racine : sidecar/agent d’injection de secrets pas prêt ; secrets basés sur fichiers pas encore écrits ; token IAM pas encore disponible.

Correction : la disponibilité doit inclure « identifiants présents et valides » ; verrouillez le démarrage sur cette condition, pas seulement sur le démarrage du processus.

6) L’app dit prête, mais les migrations sont encore en cours

Symptôme : le endpoint de readiness retourne OK alors que des changements de schéma sont en cours ; les clients obtiennent des erreurs SQL.

Cause racine : l’app ne traite pas les migrations comme dépendance de readiness, ou les migrations s’exécutent en parallèle sur des réplicas.

Correction : déplacez les migrations dans un job dédié ; la readiness de l’app doit échouer tant que la version de schéma n’est pas compatible.

7) « Works on my machine », échoue sur un hôte plus lent

Symptôme : les laptops dev vont bien ; les runners CI ou petites VMs échouent au démarrage.

Cause racine : hypothèses de timing intégrées au démarrage ; pas de backoff ; healthchecks trop stricts ; stockage plus lent.

Correction : augmentez les fenêtres de tolérance ; mesurez le temps réel vers la disponibilité ; corrigez la dépendance la plus lente plutôt que de la masquer.

8) Les conteneurs montrent healthy, mais le chemin réseau pour les vrais clients est cassé

Symptôme : healthchecks internes verts ; clients externes timeoutent.

Cause racine : le healthcheck ne teste que localhost ; le service se bind sur la mauvaise interface ; publication de port/ingress mal configurée.

Correction : testez la readiness via le même chemin que les clients utilisent, ou incluez une vérification synthétique secondaire depuis l’extérieur du réseau des conteneurs.

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

Plan étape par étape : corriger les faux démarrages dans une pile Compose existante

  1. Inventaire des dépendances. Pour chaque service, notez ce dont il a réellement besoin pour servir du trafic :
    connectivité BD, connectivité queue, cache, secrets, système de fichiers inscriptible, version de schéma.
  2. Ajoutez un endpoint de readiness (ou équivalent) à chaque service applicatif. S’il n’est pas HTTP, implémentez une petite vérification CLI
    qui exécute une opération minimale réelle (par ex. une requête BD).
  3. Définissez des healthchecks Docker qui reflètent la disponibilité. Ne faites pas un curl sur un endpoint de façade ; ciblez celui qui vérifie les dépendances.
  4. Réglez les timings des healthchecks de façon réaliste. Ajoutez start_period pour les warm-ups connus, utilisez des timeouts correspondant à votre démarrage le plus lent normal.
  5. Choisissez une stratégie de verrouillage. Si votre environnement supporte le verrouillage Compose sur health, utilisez-le. Sinon, verrouillez dans l’entrypoint ou la logique applicative.
  6. Implémentez des retries avec backoff et jitter dans les apps. C’est non négociable pour des systèmes qui doivent survivre aux redémarrages et aux déploiements.
  7. Séparez les migrations en job dédié. Exécutez-le une fois, avec un verrou, et échouez bruyamment s’il ne peut pas se terminer.
  8. Ajoutez une vérification synthétique « stack ready ». Quelque chose qui vérifie le parcours utilisateur : login, récupération de données, écriture d’un enregistrement.
  9. Mesurez le time-to-ready. Capturez des horodatages dans les logs et suivez le median/p95 du temps de démarrage ; ajustez les healthchecks sur la base des données.
  10. Testez le pire jour. Redémarrez l’hôte, limitez le CPU, simulez un disque lent. Si votre modèle de disponibilité survit à ça, il survivra au mardi.

Checklist : à quoi ressemble le « bon »

  • Chaque service a un signal de disponibilité signifiant.
  • Chaque dépendance a un healthcheck qui reflète l’utilisabilité.
  • Aucun service ne quitte au premier échec de connexion ; les retries sont limités et loggés.
  • Les migrations s’exécutent une fois, explicitement, et non comme effet secondaire du « start » d’une app.
  • Les healthchecks tolèrent le warm-up normal ; ils détectent de vrais deadlocks et mauvaises configurations.
  • La pile dispose d’au moins une vérification synthétique end-to-end utilisée en CI et/ou lors des déploiements.

Checklist : quoi éviter (parce que ça revient toujours vous mordre)

  • Des sleeps codés en dur comme « gestion des dépendances ».
  • Des healthchecks qui ne vérifient que « port ouvert ».
  • Les boucles de restart comme stratégie de récupération par défaut.
  • Plusieurs réplicas qui courent pour les migrations au démarrage.
  • Des timeouts réglés sur votre laptop le plus rapide plutôt que sur votre environnement normal le plus lent.

FAQ

1) depends_on n’est-il pas suffisant pour la plupart des stacks ?

Non. Il traite de la séquence de démarrage des processus, pas de l’utilisabilité du service. Vous aurez toujours des courses sur des disques lents, après des crashs,
ou quand des dépendances font une récupération interne.

2) Faut-il verrouiller le démarrage dans Compose ou dans l’app ?

Dans l’app est plus portable et plus correct. Le verrouillage Compose est une couche utile, mais votre application tournera ailleurs que dans Compose :
différents hôtes, CI, Kubernetes, systemd, peut-être du bare metal. Réessayer les dépendances est une responsabilité applicative.

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

Liveness signifie « le processus est vivant ». Readiness signifie « le service peut faire son travail pour les clients ». Ce ne sont pas interchangeables.
Si vous les confondez, vous tuerez des services sains mais occupés ou enverrez du trafic vers des services cassés mais en cours d’exécution.

4) Si mon service réessaye indéfiniment, n’est-ce pas fiable ?

C’est résilient mais pas forcément fiable. Les retries infinis peuvent masquer des pannes et créer une charge soutenue sur les dépendances.
Utilisez backoff, jitter et un maximum d’attente avec des rapports d’erreur clairs.

5) Qu’est-ce qu’un « faux démarrage » dans ce contexte ?

Un faux démarrage est quand l’orchestrateur rapporte les services démarrés (ou même healthy) mais que le système ne peut pas réellement servir du trafic correct.
Il se résout souvent « tout seul », ce qui le rend facile à ignorer jusqu’à ce qu’il vous brûle en production.

6) Comment écrire un bon healthcheck pour une base de données ?

Préférez un outil de readiness natif à la base (comme pg_isready) et, si nécessaire, une requête minimale qui vérifie l’authentification
et la bonne base. Attention : une requête peut être coûteuse si elle s’exécute trop fréquemment.

7) Pourquoi mes conteneurs démarrent bien en dev mais échouent en CI ?

Les runners CI sont souvent plus lents, plus partagés et plus variables. Les hypothèses de timing s’effondrent d’abord là-bas. Ajoutez des start periods,
utilisez des endpoints de readiness, et mesurez le time-to-ready dans les deux environnements.

8) Une vérification TCP (nc) suffit-elle pour la disponibilité ?

C’est un premier pas utile, pas une fin. L’ouverture d’une socket TCP signifie qu’il y a un socket ; cela ne garantit pas que l’auth fonctionne, que le schéma est correct,
ou que le service ne renvoie pas d’erreurs.

9) Les healthchecks peuvent-ils nuire aux performances ?

Oui. Un healthcheck fréquent et lourd (comme une requête SQL lente) peut devenir une charge auto-infligée. Gardez les vérifications légères, réduisez la fréquence,
et utilisez des start periods plutôt que des sondages agressifs.

10) Quelle est l’amélioration la plus simple avec le meilleur retour sur investissement ?

Ajoutez un vrai endpoint de readiness à l’app et pointez le healthcheck dessus. Puis implémentez des retries avec backoff pour la BD et les queues.
Ces deux changements éliminent la plupart des instabilités au démarrage.

Conclusion : prochaines étapes réalisables cette semaine

Traitez « démarré » comme un événement mécanique, pas comme une condition de succès. Si vous voulez moins d’incidents, arrêtez de demander à Docker d’inférer la disponibilité
et commencez à lui fournir des signaux qui correspondent à l’utilisabilité réelle.

Prochaines étapes pratiques :

  1. Ajoutez des endpoints de readiness (ou des vérifications d’opérations réelles minimales) à vos services applicatifs.
  2. Améliorez les healthchecks pour tester la disponibilité, pas seulement « port ouvert », et ajustez les start periods pour refléter la réalité.
  3. Implémentez des retries applicatifs avec backoff et jitter pour chaque dépendance externe.
  4. Séparez les migrations de schéma en une étape dédiée one-shot avec des sémantiques claires de succès/échec.
  5. Adoptez le récapitulatif de diagnostic rapide et en faites un réflexe : santé, logs, vérifications in-container, puis pression sur les ressources.

Les faux démarrages ne sont pas une question de malchance. C’est une lacune de conception. Fermez-la, et votre ère du « ça marche si je le redémarre » peut enfin se terminer.

← Précédent
SPF/DKIM passent mais en spam : signaux cachés à corriger
Suivant →
Debian 13 : récupérer l’accès root en mode rescue — faites-le sans empirer la situation

Laisser un commentaire