La panne la plus coûteuse est celle qui a l’air saine. Les tableaux de bord sont verts, les déploiements sont « réussis »,
et pendant ce temps les clients subissent des timeouts parce que le conteneur est démarré, mais le service ne fonctionne pas.
Docker annonce fièrement running. Votre orchestrateur hausse les épaules. Vous recevez quand même une alerte.
Les healthchecks sont censés être le détecteur de mensonges. Trop souvent ils ne sont qu’un autocollant sur le front indiquant « OK »
parce que quelqu’un a exécuté curl localhost une fois et s’en est contenté. Corrigeons cela — avec des vérifications qui correspondent
aux vrais modes de défaillance, n’en créent pas de nouveaux, et vous aident réellement à prendre des décisions sous pression.
Ce que sont (et ne sont pas) les healthchecks Docker
Un healthcheck Docker est une commande que Docker exécute à l’intérieur du conteneur selon un calendrier.
La commande se termine avec 0 pour sain, une valeur non nulle pour non sain. Docker suit un état :
starting, healthy ou unhealthy.
C’est tout. Pas de magie. Pas de consensus distribué. Pas de garantie « mon appli est bonne pour les clients ».
C’est une sonde locale. Utile, mais seulement si vous la visez au bon endroit.
Ce qu’un healthcheck Docker fait bien
- Détecter les verrouillages et processus bloqués qui ont encore un PID.
- Détecter les échecs de dépendances locales (socket de base de données qui n’accepte pas, authentification du cache échouant).
- Gérer l’ordre de démarrage dans Docker Compose pour éviter une ruée vers la base de données.
- Fournir un signal aux orchestrateurs et aux humains : « ce conteneur vous ment. »
Ce qu’il ne peut pas faire (arrêtez de lui demander)
- Prouver le succès de bout en bout pour l’utilisateur. Une vérification locale ne peut pas valider DNS, routage, ACL externes ou le comportement du client.
- Remplacer les métriques. « Healthy » est un booléen. Latence, saturation et budget d’erreur ne le sont pas.
- Corriger de mauvais rollouts. Si votre check est faux, il certifiera la mauvaise chose en toute confiance.
Voici la vérité opérationnelle : un healthcheck est un contrat. Vous définissez « sain assez pour recevoir du trafic »
ou « sain assez pour continuer à tourner ». Rédigez ce contrat comme si votre pager en dépendait, parce que c’est le cas.
Faits et contexte intéressants (ce qui explique le bazar actuel)
- Docker a ajouté
HEALTHCHECKen 2015 (ère Docker 1.12), principalement pour supporter des patterns d’orchestration avant la domination de Swarm/Kubernetes. - Les healthchecks s’exécutent dans le namespace du conteneur, donc ils voient le DNS du conteneur, les sockets locaux et localhost différemment que l’hôte.
- Docker stocke l’état de santé dans les métadonnées du conteneur et l’expose via
docker inspect; ce n’est pas une ligne de log à moins que vous ne la recherchiez. - Le
depends_onoriginal de Compose n’attendait pas la disponibilité ; les versions ultérieures ont introduit des dépendances conditionnelles basées sur la santé, mais beaucoup de stacks ont encore l’ancien modèle mental. - Kubernetes a popularisé l’idée de « liveness » et « readiness » séparés, ce qui a fait réaliser qu’un seul endpoint de santé mélange souvent deux objectifs incompatibles.
- Les premiers « health endpoints » renvoyaient souvent juste « 200 OK » parce que les load balancers n’avaient besoin que d’un heartbeat ; les systèmes modernes exigent une readiness consciente des dépendances et un comportement de fast-fail.
- Les cgroups et la limitation CPU peuvent faire paraître des applis saines comme mortes ; un timeout de 1s sous pression CPU est pratiquement un tirage au sort.
- Les politiques de redémarrage préexistent souvent aux healthchecks dans de nombreuses configurations ; les opérateurs ont collé les checks aux redémarrages et ont accidentellement construit des boucles d’auto-DDoS.
Modes de défaillance que les déploiements « verts » cachent
1) Le processus est vivant ; le service est mort
Le classique : votre processus principal tourne encore, mais il est bloqué. Deadlock, GC infini, attente d’une dépendance cassée,
blocage disque, ou bloqué sur un mutex. Docker affiche « Up ». Votre reverse proxy timeoute.
Un bon healthcheck teste le comportement du service, pas l’existence d’un PID.
2) Port ouvert, appli pas prête
Les sockets TCP en écoute se lèvent tôt. Les frameworks font ça. Votre appli chauffe encore les caches, exécute des migrations,
charge des modèles, ou attend la base de données.
Un test de port est un signal plutôt liveness. La readiness exige une confirmation au niveau applicatif.
3) Défaillance partielle d’une dépendance
Votre service peut répondre sur /health, mais il ne peut pas parler à Redis à cause d’un mismatch d’auth,
ne peut pas écrire sur Postgres à cause d’un changement de permissions, ou ne peut pas atteindre une API externe à cause d’un changement d’egress.
Si vous n’incluez pas les dépendances, vous déploierez une magnifique panne saine.
4) Échec lent : ça marche, mais pas à temps
Sous charge ou voisins bruyants, la latence dépasse les timeouts clients. Votre endpoint de santé renvoie toujours 200 — finalement.
Pendant ce temps, les utilisateurs voient des erreurs.
Votre healthcheck doit avoir un budget de latence et l’appliquer avec des timeouts. Un endpoint « sain après 30 secondes » est juste de l’optimisme.
5) Le check lui‑même cause la panne
Les checks qui frappent des endpoints coûteux, exécutent des migrations, ou ouvrent de nouvelles connexions DB chaque seconde sont un excellent moyen
de faire tomber un système tout en se félicitant d’« ajouter de la fiabilité ».
Blague #1 : Un healthcheck qui DDoS sa propre base de données teste toujours la prod techniquement. Ce n’est juste pas le genre de test souhaité.
Principes de conception : des checks qui disent la vérité
Définissez ce que « sain » signifie opérationnellement
Choisissez une de ces définitions et soyez explicite :
- Prêt pour le trafic : peut servir de vraies requêtes dans une latence SLO‑like et a les dépendances requises.
- Sûr de continuer à tourner : le processus n’est pas bloqué ; il peut faire des progrès ; il peut s’arrêter proprement.
Docker ne vous donne qu’un seul état de santé. C’est ennuyeux. Vous pouvez toujours modéliser les deux en choisissant ce qui vous importe pour ce conteneur.
Pour un proxy de bord, « prêt pour le trafic » importe. Pour un worker en arrière-plan, « sûr de continuer à tourner » importe souvent plus.
Fail fast, mais pas bêtement
Un bon check échoue rapidement quand le conteneur est réellement cassé, mais il ne doit pas basculer pendant le démarrage normal
ou lors de blips transitoires de dépendances.
- Utilisez
start_periodpour éviter de punir les warmups lents. - Utilisez des timeouts pour qu’un appel bloqué ne fige pas le healthcheck indéfiniment.
- Utilisez des retries pour éviter qu’une perte de paquet devienne une tempête de redémarrages.
Privilégiez des sondes locales, peu coûteuses et déterministes
Les healthchecks s’exécutent fréquemment. Faites‑les :
- Locales : ciblez localhost, un socket UNIX, ou l’état en‑proc.
- Peu coûteuses : évitez les requêtes lourdes, évitez de créer de nouveaux pools de connexions.
- Déterministes : même entrée = même sortie ; pas d’aléa ; pas de « parfois c’est lent ».
Incluez les dépendances, mais choisissez la bonne profondeur
Si votre service ne peut pas fonctionner sans Postgres, votre check doit confirmer qu’il peut s’authentifier et exécuter une requête triviale.
S’il peut se dégrader gracieusement (servir du contenu en cache), ne marquez pas le conteneur comme unhealthy simplement parce que Redis est down.
Opérationnellement : votre healthcheck doit refléter votre comportement de défaillance attendu.
Utilisez les codes de sortie intentionnellement
Docker ne se soucie que de zéro vs non‑zéro, mais les humains veulent savoir pourquoi. Faites en sorte que votre check affiche une brève raison
sur stderr/stdout avant de quitter non‑zéro. Cette raison apparaît dans docker inspect.
Faites en sorte que les checks testent le même chemin que votre trafic
Si le trafic réel passe par Nginx vers votre appli, vérifier l’appli directement peut ignorer toute une classe de pannes :
config Nginx cassée, connexions workers épuisées, DNS upstream mauvais, problèmes TLS. Parfois vous voulez que le proxy vérifie son upstream.
Parfois vous voulez qu’un LB externe vérifie le proxy. Superposez vos checks comme vous superposez les pannes.
Citation (idée paraphrasée) — Jim Gray : « Traitez la défaillance système comme normale ; concevez comme si les composants allaient échouer à tout moment. »
Playbook de diagnostic rapide : trouver le goulot rapidement
Quand un conteneur est « sain » mais que les utilisateurs échouent, vous n’avez pas besoin de philosophie. Vous avez besoin d’une séquence.
Voici l’ordre qui fait émerger le plus souvent la contrainte réelle le plus vite.
Première étape : le signal de santé ment-il, ou le système a‑t‑il changé ?
- Vérifiez l’état de santé Docker et les derniers journaux de health pour ce conteneur.
- Confirmez que votre check teste la bonne chose (dépendance, chemin, latence).
- Comparez la config avant/après déploiement pour les changements de healthcheck, timeouts et start periods.
Deuxième étape : le service sert‑il réellement sur l’interface attendue ?
- À l’intérieur du conteneur : vérifiez les ports en écoute, le DNS et la connectivité locale.
- Depuis l’hôte : vérifiez le mapping des ports et les règles de pare‑feu.
- Depuis un conteneur pair : vérifiez la découverte de service et les politiques réseau équivalentes.
Troisième étape : sommes‑nous bloqués par le CPU, la mémoire, le disque ou une dépendance ?
- La limitation CPU et les pics de charge peuvent transformer un handler 200ms en timeout à 5s.
- Les OOM kills peuvent produire des cycles « ça marche… jusqu’à ce que ça ne marche plus ».
- La saturation disque peut geler les services I/O‑intensifs pendant que le processus reste vivant.
- La saturation d’une dépendance (max connections DB) ressemble souvent à des timeouts aléatoires applicatifs.
Quatrième étape : le healthcheck cause‑t‑il du tort ?
- Vérifiez fréquence et coût : pilonne‑t‑il la DB ou le pool de threads de l’appli ?
- Vérifiez la concurrence : les healthchecks ne doivent pas s’accumuler.
- Vérifiez les effets secondaires : les endpoints de santé doivent être en lecture seule.
L’astuce est de traiter les healthchecks comme n’importe quel autre générateur de charge en production. Parce que c’en est.
Tâches pratiques (commandes, sortie attendue, et ce que vous décidez)
Ce sont de vraies actions d’opérateur. Chaque tâche inclut : une commande, ce que la sortie signifie, et la décision que vous prenez.
Utilisez‑les pendant les incidents et pendant les périodes calmes quand vous essayez de prévenir le prochain.
Tâche 1 : Voir l’état de santé d’un coup d’œil
cr0x@server:~$ docker ps --format 'table {{.Names}}\t{{.Status}}\t{{.Image}}'
NAMES STATUS IMAGE
api-1 Up 12 minutes (healthy) myorg/api:1.9.3
db-1 Up 12 minutes (healthy) postgres:16
worker-1 Up 12 minutes (unhealthy) myorg/worker:1.9.3
Ce que cela signifie : Docker exécute les healthchecks et rapporte l’état. (unhealthy) n’est pas subtil.
Décision : Si un composant critique est unhealthy, arrêtez de traiter « Up » comme un succès. Enquêtez avant de scaler ou de router du trafic.
Tâche 2 : Inspecter le journal de santé et la raison d’échec
cr0x@server:~$ docker inspect --format '{{json .State.Health}}' worker-1 | jq
{
"Status": "unhealthy",
"FailingStreak": 5,
"Log": [
{
"Start": "2026-01-02T08:11:12.123456789Z",
"End": "2026-01-02T08:11:12.223456789Z",
"ExitCode": 1,
"Output": "redis ping failed: NOAUTH Authentication required\n"
}
]
}
Ce que cela signifie : Le check échoue de façon consistante, et il imprime une raison utile. Bénissez celui qui a écrit cette sortie.
Décision : Corrigez les identifiants/la config plutôt que de redémarrer aveuglément. Réfléchissez aussi à si un échec d’auth Redis doit marquer le worker comme unhealthy ou comme dégradé.
Tâche 3 : Exécuter manuellement la commande de healthcheck à l’intérieur du conteneur
cr0x@server:~$ docker exec -it api-1 sh -lc 'echo $0; /usr/local/bin/healthcheck.sh; echo exit=$?'
sh
ok: http=200 db=ok redis=ok latency_ms=27
exit=0
Ce que cela signifie : Vous exécutez la même sonde que Docker exécute. Elle a réussi et a renvoyé rapidement.
Décision : Si les utilisateurs échouent toujours, le problème est probablement hors de la vue locale du conteneur (réseau, proxy, LB, DNS) ou il y a un décalage entre les critères de santé et le chemin utilisateur.
Tâche 4 : Confirmer quel healthcheck Docker exécute réellement
cr0x@server:~$ docker inspect --format '{{json .Config.Healthcheck}}' api-1 | jq
{
"Test": [
"CMD-SHELL",
"/usr/local/bin/healthcheck.sh"
],
"Interval": 30000000000,
"Timeout": 2000000000,
"StartPeriod": 15000000000,
"Retries": 3
}
Ce que cela signifie : Interval 30s, timeout 2s, start period 15s, retries 3. Ces chiffres déterminent le comportement.
Décision : Si vous voyez timeout=1s sur un service JVM sous limites CPU, vous avez trouvé un incident futur. Ajustez‑le maintenant.
Tâche 5 : Surveiller les transitions de santé en direct
cr0x@server:~$ docker events --filter container=api-1 --filter event=health_status --since 10m
2026-01-02T08:03:12.000000000Z container health_status: healthy api-1
2026-01-02T08:08:42.000000000Z container health_status: unhealthy api-1
2026-01-02T08:09:12.000000000Z container health_status: healthy api-1
Ce que cela signifie : Flapping : il devient unhealthy puis se rétablit. C’est soit un vrai problème intermittent soit un check trop sensible.
Décision : Si le flapping s’aligne sur la charge, vous avez probablement une saturation de ressources. S’il est aléatoire, augmentez timeout/retries et enquêtez sur réseau/DNS.
Tâche 6 : Valider le DNS et le routage conteneur‑à‑conteneur
cr0x@server:~$ docker exec -it api-1 sh -lc 'getent hosts db && nc -zvw2 db 5432'
172.20.0.3 db
db (172.20.0.3:5432) open
Ce que cela signifie : Le DNS résout et la connexion TCP fonctionne depuis le conteneur app vers le conteneur base de données.
Décision : Si cela échoue, ne perdez pas de temps dans l’appli. Corrigez le réseau, le nom du service ou la configuration du réseau Compose.
Tâche 7 : Prouver que l’appli écoute là où vous le pensez
cr0x@server:~$ docker exec -it api-1 sh -lc 'ss -lntp | head'
State Recv-Q Send-Q Local Address:Port Peer Address:Port Process
LISTEN 0 4096 0.0.0.0:8080 0.0.0.0:* users:(("java",pid=1,fd=123))
Ce que cela signifie : Le service écoute sur 0.0.0.0:8080. S’il était lié seulement à 127.0.0.1, votre mapping de port pourrait être « up » mais injoignable depuis l’extérieur.
Décision : Si la liaison est incorrecte, corrigez l’adresse de bind de l’appli. Ne le masquez pas avec du host networking à moins d’aimer les regrets.
Tâche 8 : Tester le chemin réel de requête depuis l’hôte (port mapping)
cr0x@server:~$ curl -fsS -m 2 -D- http://127.0.0.1:18080/healthz
HTTP/1.1 200 OK
Content-Type: text/plain
Content-Length: 2
ok
Ce que cela signifie : Via le port publié, l’endpoint de santé répond en moins de 2 secondes.
Décision : Si le curl depuis l’hôte échoue mais que le curl depuis l’intérieur du conteneur fonctionne, le problème est le mapping, le pare‑feu, le proxy ou le bind incorrect de l’appli.
Tâche 9 : Identifier la limitation CPU qui fait expirer les checks
cr0x@server:~$ docker stats --no-stream api-1
CONTAINER ID NAME CPU % MEM USAGE / LIMIT MEM % NET I/O BLOCK I/O PIDS
a1b2c3d4e5f6 api-1 198.32% 1.2GiB / 1.5GiB 80.12% 1.3GB / 1.1GB 25MB / 2MB 93
Ce que cela signifie : CPU et mémoire élevés. Si vous avez aussi des timeouts serrés, vous verrez des faux négatifs sous charge.
Décision : Allouez plus de CPU/mémoire, réduisez le travail, ou élargissez le timeout health pour que « sain sous charge attendue » reste vrai.
Tâche 10 : Vérifier les OOM kills ou redémarrages qui se déguisent en « instabilité »
cr0x@server:~$ docker inspect --format 'RestartCount={{.RestartCount}} OOMKilled={{.State.OOMKilled}} ExitCode={{.State.ExitCode}}' api-1
RestartCount=2 OOMKilled=true ExitCode=137
Ce que cela signifie : Exit code 137 et OOMKilled=true : le kernel l’a tué. Votre healthcheck n’a pas échoué ; le conteneur est mort.
Décision : Corrigez les limites mémoire, les fuites ou les pics. Assurez‑vous aussi que le start_period n’est pas trop court, sinon vous accumulerez des échecs pendant la récupération.
Tâche 11 : Détecter les blocages I/O disque qui figent les processus « sains »
cr0x@server:~$ docker exec -it db-1 sh -lc 'ps -o stat,comm,pid | head'
STAT COMMAND PID
Ss postgres 1
Ds postgres 72
Ds postgres 73
Ce que cela signifie : Les processus en état D sont bloqués dans un wait I/O non interrompible. Ils ne répondront pas à vos petites requêtes gentilles.
Décision : Arrêtez d’accuser l’application. Inspectez la latence du stockage, la saturation disque hôte, les voisins bruyants ou les drivers de volume.
Tâche 12 : Confirmer la readiness d’une dépendance avec une sonde dédiée (Postgres)
cr0x@server:~$ docker exec -it db-1 sh -lc 'pg_isready -U postgres -h 127.0.0.1 -p 5432; echo exit=$?'
127.0.0.1:5432 - accepting connections
exit=0
Ce que cela signifie : Postgres accepte les connexions. C’est mieux qu’un test de port ouvert car ça parle le protocole.
Décision : Si cela échoue de façon intermittente, examinez max connections, checkpoints, disque ou la récupération. N’augmentez pas juste les retries des healthchecks en espérant.
Tâche 13 : Valider que votre endpoint de santé est assez rapide (budget de latence)
cr0x@server:~$ docker exec -it api-1 sh -lc 'time -p curl -fsS -m 1 http://127.0.0.1:8080/healthz >/dev/null'
real 0.04
user 0.00
sys 0.00
Ce que cela signifie : 40ms en local. Parfait. Si vous voyez 0.9–1.0s avec un timeout de 1s, vous vivez dangereusement.
Décision : Définissez un timeout avec marge. Les healthchecks doivent échouer pour une vraie lenteur, pas pour la gigue normale sous charge.
Tâche 14 : Attraper les mauvaises hypothèses sur « localhost » (proxy vs appli)
cr0x@server:~$ docker exec -it nginx-1 sh -lc 'curl -fsS -m 1 http://127.0.0.1:8080/healthz || echo "upstream unreachable"'
upstream unreachable
Ce que cela signifie : À l’intérieur du conteneur Nginx, localhost est Nginx — pas votre conteneur appli. C’est une des principales causes de healthchecks inutiles.
Décision : Pointez les checks vers le bon nom d’hôte/upstream, ou exécutez le check dans le conteneur approprié. « Ça marche dans mon conteneur » n’est pas une stratégie réseau.
Modèles de healthcheck pour services courants
Modèle A : Service HTTP avec readiness consciente des dépendances
Pour une API, un bon check valide généralement :
le thread serveur HTTP peut répondre rapidement, et les dépendances cœur sont joignables et authentifiées.
Gardez‑le minimal : une toute petite requête, un ping cache, un contrôle d’état interne superficiel.
Exemple : HEALTHCHECK dans Dockerfile appelant un script
cr0x@server:~$ cat Dockerfile
FROM alpine:3.20
RUN apk add --no-cache curl ca-certificates
COPY healthcheck.sh /usr/local/bin/healthcheck.sh
RUN chmod +x /usr/local/bin/healthcheck.sh
HEALTHCHECK --interval=30s --timeout=2s --start-period=20s --retries=3 CMD ["/usr/local/bin/healthcheck.sh"]
cr0x@server:~$ cat healthcheck.sh
#!/bin/sh
set -eu
t0=$(date +%s%3N)
# Fast local HTTP check (service path)
code=$(curl -fsS -m 1 -o /dev/null -w '%{http_code}' http://127.0.0.1:8080/healthz || true)
if [ "$code" != "200" ]; then
echo "http failed: code=$code"
exit 1
fi
# Optional dependency: DB shallow check via app endpoint (preferred) or direct driver probe
# Here we assume /readyz includes db connectivity check inside the app.
code2=$(curl -fsS -m 1 -o /dev/null -w '%{http_code}' http://127.0.0.1:8080/readyz || true)
if [ "$code2" != "200" ]; then
echo "ready failed: code=$code2"
exit 1
fi
t1=$(date +%s%3N)
lat=$((t1 - t0))
echo "ok: http=200 ready=200 latency_ms=$lat"
exit 0
Pourquoi ça marche : Le check impose un budget de temps et valide l’interface réelle du service.
Il évite une logique de dépendance profonde en shell quand l’appli peut mieux le faire (et réutiliser les pools existants).
Modèle B : Conteneurs de base de données : utilisez les outils natifs, pas « port open »
Si vous vérifiez Postgres, utilisez pg_isready. Pour MySQL, utilisez mysqladmin ping.
Pour Redis, utilisez redis-cli ping. Ces sondes parlent suffisamment le protocole pour être significatives.
Exemple : Postgres dans Compose
cr0x@server:~$ cat compose.yaml
services:
db:
image: postgres:16
environment:
POSTGRES_PASSWORD: example
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres -h 127.0.0.1 -p 5432"]
interval: 5s
timeout: 3s
retries: 10
start_period: 10s
Pourquoi ça marche : Ça capture « le processus est up mais n’accepte pas de connexions », y compris recovery ou misconfig.
C’est peu coûteux car ça ne nécessite pas une vraie requête.
Modèle C : Workers en arrière‑plan : checks basés sur le progrès
Les workers n’ont souvent pas d’HTTP. Votre check doit valider :
le processus peut parler à la queue, et il n’est pas bloqué.
Si vous pouvez émettre un fichier heartbeat ou un petit timestamp « dernier job traité », faites‑le.
Un worker qui peut se connecter à Redis mais n’a pas fait de progrès depuis 10 minutes n’est pas sain. Il est juste en ligne.
Modèle D : Reverse proxies : vérifiez l’upstream, pas vous‑même
Nginx « sain » alors que l’upstream est down est inutile si le proxy est la porte d’entrée du trafic.
Vous vérifiez soit la joignabilité de l’upstream, soit vous configurez le load balancer pour vérifier un endpoint qui reflète l’état upstream.
Blague #2 : Un reverse proxy qui répond 200 alors que l’upstream est en feu, c’est comme une réceptionniste disant « tout le monde est en réunion » pendant une évacuation.
Compose, orchestration, dépendances et réalité du démarrage
Docker Compose est l’endroit où les healthchecks vous sauvent ou vous rendent trop confiant.
Le piège : les gens supposent que depends_on signifie « attendre jusqu’à ce que prêt ». Historiquement, cela signifiait « démarrer dans un certain ordre ».
Ce n’est pas la même chose, et vous devinez laquelle votre base de données préfère.
Utilisez des dépendances basées sur la santé là où cela compte vraiment
Si l’API va crash‑boucler tant que la BD n’est pas up, vous pouvez conditionner le démarrage de l’API sur la santé de la BD dans Compose.
Cela réduit le bruit et évite les ruées au démarrage.
Exemple : dépendance Compose sur la santé
cr0x@server:~$ cat compose.yaml
services:
db:
image: postgres:16
environment:
POSTGRES_PASSWORD: example
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres -h 127.0.0.1 -p 5432"]
interval: 5s
timeout: 3s
retries: 12
start_period: 10s
api:
image: myorg/api:1.9.3
depends_on:
db:
condition: service_healthy
healthcheck:
test: ["CMD-SHELL", "curl -fsS -m 1 http://127.0.0.1:8080/readyz || exit 1"]
interval: 10s
timeout: 2s
retries: 3
start_period: 30s
Vérification de la réalité : Cela améliore l’ordre de démarrage, pas la fiabilité à long terme. Si la BD meurt plus tard, Compose n’orchestrera pas magiquement une bascule propre.
Mais cela empêche la « tout démarre en même temps, tout échoue en même temps » lors du boot.
Naissez pas les healthchecks en portes de déploiement incompréhensibles
Si vous faites que la readiness inclue toutes les dépendances externes, un upstream flaky peut bloquer les déploiements et rollbacks.
Ce n’est pas de la résilience ; c’est du couplage. Définissez ce qui est requis pour servir votre trafic.
Séparez warmup au démarrage et santé en régime permanent
Le démarrage est spécial. Des migrations s’exécutent. Des compilateurs JIT se réveillent. Des certificats se chargent. Les caches DNS sont froids.
C’est pourquoi start_period existe : pour éviter les redémarrages et états « unhealthy » pendant un warmup attendu.
Mais n’en abusez pas. Un start period de 10 minutes cache de réelles pannes pendant 10 minutes. Si vous en avez besoin, vous avez probablement besoin d’une meilleure conception d’initialisation.
Redémarrages, healthchecks et la spirale mortelle
Les healthchecks Docker ne redémarrent pas automatiquement les conteneurs. Quelque chose d’autre le fait : votre orchestrateur, vos scripts, vos superviseurs,
ou une automatisation « maline » qui interprète unhealthy comme « redémarrer maintenant ».
C’est là que les bonnes intentions meurent.
La spirale mortelle classique
- Une dépendance devient lente (pics de latence BD).
- Le healthcheck timeoute (trop agressif).
- L’automatisation redémarre les conteneurs.
- La tempête de redémarrages augmente la charge (caches froids, reconnexions, migrations, relectures).
- La dépendance ralenti encore plus.
Comment l’éviter
- Les healthchecks doivent être d’abord diagnostiques. Redémarrer en dernier recours, pas par réflexe.
- Utilisez du backoff dans le système qui décide de redémarrer.
- Rendez votre check peu coûteux et limité dans le temps, et ajustez les timeouts à la gigue attendue sous charge.
- Concevez un mode dégradé : si Redis est down, pouvez‑vous servir en lecture seule ? Dans ce cas, ne faites pas échouer la readiness sur Redis.
Un healthcheck qui déclenche des redémarrages est une arme chargée. Rangez‑la comme telle.
Healthchecks vs monitoring vs métriques : ne les confondez plus
Un healthcheck est une sonde locale binaire. Le monitoring est un système qui vous indique quand vous violez des objectifs.
Les métriques sont les données qui expliquent pourquoi.
Quand utiliser des healthchecks
- Pour empêcher le routage de trafic vers des conteneurs qui ne peuvent pas le servir.
- Pour éviter de démarrer des services dépendants trop tôt.
- Pour faire remonter rapidement et de façon cohérente des états manifestement cassés.
Quand ne pas utiliser des healthchecks
- Comme substitut aux percentiles de latence et taux d’erreur.
- Comme unique signal de redémarrage. C’est ainsi qu’on construit des amplificateurs d’échec élégants.
- Pour « tester le monde entier » depuis un conteneur. C’est le rôle des tests d’intégration et du monitoring synthétique.
Pattern en deux niveaux : santé interne + checks synthétiques externes
Le pattern robuste est en couches :
- Healthcheck interne : peu coûteux, rapide, suffisamment conscient des dépendances pour la correction locale.
- Check synthétique externe : frappe le vrai chemin public, exerce DNS/TLS/routage, et mesure la latence.
Cela attrape les deux grandes classes de mensonges : « le conteneur est ok mais le chemin est cassé » et « le chemin est ok mais le conteneur est mort ».
Trois mini-récits d’entreprise depuis le terrain
Mini‑récit 1 : L’incident causé par une fausse hypothèse
Une société exécutait une petite plateforme interne : un conteneur reverse proxy devant plusieurs conteneurs API.
Ils ont ajouté un healthcheck Docker au proxy qui faisait curl http://127.0.0.1/ et retournait 0 sur 200.
Le proxy répondait toujours 200, même quand les upstreams étaient down, parce que la racine servait une page statique « welcome ».
Lors d’un déploiement de routine, un service upstream n’a pas démarré à cause d’une variable d’environnement manquante.
Le proxy continuait de retourner 200 au probe du load balancer. Le trafic passait. Les utilisateurs obtenaient un sympathique 502 et un timeout.
Le monitoring indiquait « proxy healthy ». Le pipeline de déploiement affichait « tous les conteneurs en cours d’exécution ». Tout le monde fixait les graphiques, comme si l’incrédulité pouvait restaurer le service.
La mauvaise hypothèse était subtile et très humaine : « Si le proxy est up, le service est up. »
Mais pour les clients, le proxy n’est que la porte d’entrée. Si la pièce derrière est en feu, la porte ouverte n’est pas un succès.
Ils ont corrigé cela en définissant deux endpoints :
/healthz pour « le processus proxy est vivant », et /readyz pour « les upstreams critiques sont joignables et renvoient le statut attendu ».
Le load balancer vérifiait /readyz. Le healthcheck Docker du proxy vérifiait aussi /readyz,
avec des timeouts raisonnables pour éviter une défaillance en cascade quand l’upstream était lent.
Le résultat immédiat fut moins dramatique : les upstreams mal configurés ne devinrent plus des trous noirs de trafic en prod.
Le résultat à plus long terme fut culturel : les gens ont arrêté d’utiliser « conteneur en cours d’exécution » comme synonyme de « service fonctionne ».
Mini‑récit 2 : L’optimisation qui s’est retournée contre eux
Une autre organisation voulait une « détection plus rapide » des conteneurs cassés. Ils ont durci les healthchecks :
interval 2 secondes, timeout 200ms, retries 1. Ils se sont félicités d’être sérieux sur la fiabilité.
Par temps calme, tout avait l’air correct.
Puis est venu un pic normal. La limitation CPU s’est déclenchée car les conteneurs avaient des ressources limitées pour réduire les coûts.
Les pauses GC ont augmenté. La latence disque a un peu grimpé à cause de snapshots hôtes en arrière‑plan.
Les requêtes réussissaient toujours, mais parfois en 400–700ms au lieu de 80ms. Les healthchecks ont expiré.
Leur automatisation a traité « unhealthy » comme « redémarrer immédiatement ». Un redémarrage provoquait des caches froids, ce qui générait plus de trafic DB,
ce qui provoquait plus de latence, ce qui provoquait plus d’échecs de healthcheck. Bientôt, ils eurent une parade synchronisée de redémarrages.
Les systèmes n’étaient pas « down » au départ ; la politique de santé les a rendus down.
Ils se sont rétablis en revenant sur l’agressivité du healthcheck et en ajoutant du backoff aux redémarrages.
Ils ont aussi séparé les checks : un liveness rapide pour « processus répond encore », et une readiness plus tolérante
avec un timeout plus long et plusieurs retries.
La leçon n’était pas « ne jamais optimiser ». C’était « optimisez la bonne chose ».
Une détection plus rapide est inutile si votre détecteur est plus fragile que le service qu’il est censé protéger.
Mini‑récit 3 : La pratique ennuyeuse mais correcte qui a sauvé la mise
Une troisième équipe gérait des services stateful avec volumes persistants. Ils avaient une habitude que personne ne célébrait :
chaque service avait un contrat de santé documenté, et la commande de healthcheck imprimait un résumé d’état sur une seule ligne
incluant un timestamp et les états des dépendances clés.
Un matin, un sous‑ensemble de conteneurs est devenu unhealthy après une fenêtre de maintenance hôte.
Les applications étaient up, mais leurs healthchecks rapportaient db=ok et disk=slow, avec la latence en millisecondes.
Ce « disk=slow » n’était pas du génie. C’était un simple seuil : écrire un petit fichier temporaire et mesurer le temps requis.
L’on‑call n’a pas eu à deviner. Il a vérifié les stats disque hôte, a trouvé qu’un backend de volume se comportait mal,
et a drainé l’hôte affecté. Le trafic s’est déplacé. Les erreurs ont chuté. Pas de session de debug héroïque dans l’appli.
Personne n’a réécrit du code à 3h du matin, ce qui est toujours la vraie victoire.
Plus tard, ils ont ajusté le healthcheck pour que la lenteur disque ne marque pas immédiatement les conteneurs unhealthy à moins que cela persiste sur plusieurs intervalles.
La pratique ennuyeuse — sortie de santé cohérente et contrat documenté — a transformé un incident vague en un arbre de décision propre.
Voilà à quoi ressemble l’excellence opérationnelle en conditions réelles : majoritairement peu sexy, parfois salvatrice.
Erreurs courantes (symptômes → cause racine → correction)
1) Symptom : Le conteneur est « sain » mais les utilisateurs reçoivent des 502/504
Cause racine : Le healthcheck ne teste que le processus local ou une page statique, pas les dépendances upstream ou le chemin réel de requête.
Correction : Testez le même chemin que le trafic réel (proxy→upstream), ou exposez un /readyz qui valide les dépendances critiques avec des budgets temporels.
2) Symptom : Les conteneurs basculent healthy/unhealthy sous charge
Cause racine : Timeout trop bas, le healthcheck se dispute le CPU/les threads avec le trafic réel, ou pics de latence dépendance.
Correction : Augmentez le timeout, ajoutez des retries, utilisez start_period, et rendez le check moins coûteux. Vérifiez limitation CPU et saturation du pool de threads.
3) Symptom : Les déploiements bloquent car les services ne deviennent jamais sains
Cause racine : Le healthcheck exige des dépendances externes optionnelles, ou dépend de migrations de données plus longues que le start_period.
Correction : Restreignez la readiness à « peut servir le trafic en sécurité » (pas « le monde est parfait »), et déplacez les migrations longues dans un job one‑shot.
4) Symptom : La DB est martelée toutes les quelques secondes même au repos
Cause racine : Le healthcheck exécute des requêtes lourdes ou ouvre de nouvelles connexions DB à chaque intervalle ; multiplié par les replicas, cela devient une charge réelle.
Correction : Utilisez des sondes natives (pg_isready), des requêtes superficielles, ou des checks au niveau appli réutilisant des pools. Augmentez l’intervalle.
5) Symptom : Le healthcheck passe en local mais échoue seulement en production
Cause racine : Hypothèses sur localhost, DNS, certificats, headers proxy, ou auth spécifique à l’environnement.
Correction : Exécutez le check dans le même namespace réseau où il tournera (le conteneur). Validez la découverte de service depuis des pairs, pas seulement depuis votre laptop.
6) Symptom : Unhealthy déclenche des redémarrages, les redémarrages aggravent le problème
Cause racine : Automatisation de redémarrage sans backoff + check trop strict + amplification au cold‑start.
Correction : Ajoutez du backoff, élargissez les seuils, séparez les sémantiques liveness/readiness, et évitez de redémarrer sur des jitters transitoires de dépendances.
7) Symptom : Le healthcheck lui‑même provoque des pics de latence
Cause racine : Le check frappe des endpoints coûteux (graph de dépendances complet, rebuild cache, handshake auth) trop fréquemment.
Correction : Créez un endpoint dédié et peu coûteux, mettez en cache brièvement le résultat de santé dans l’appli, et assurez‑vous que le check est sans effet de bord.
8) Symptom : Le healthcheck renvoie toujours healthy, même quand l’appli est bloquée
Cause racine : Le check est juste « port open » ou « process existe », ou il appelle un handler qui n’exerce pas le sous‑système bloqué.
Correction : Incluez une opération triviale qui exige du progrès (par exemple, enqueue/dequeue noop, exécuter une petite requête DB, ou vérifier un tick de boucle d’événement).
Listes de contrôle / plan étape par étape
Étape par étape : écrire un healthcheck qui ne vous embarrassera pas
- Écrivez le contrat : « Sain signifie X. Non sain signifie Y. » Gardez‑le dans le repo à côté du Dockerfile.
- Choisissez la cible : liveness‑ish (progrès) ou readiness‑ish (sûr pour servir le trafic). Ne prétendez pas qu’un booléen fait parfaitement les deux.
- Choisissez un budget de latence : définissez un timeout qui reflète les conditions réelles plus une marge (pas votre laptop sur Wi‑Fi).
- Start period : réglez
start_periodsur la base du temps de démarrage mesuré, pas de l’espoir. - Retries : réglez des retries pour tolérer la gigue transitoire mais ne pas masquer une défaillance persistante.
- Rendez‑le peu coûteux : évitez les endpoints lourds et les tempêtes de connexions. Préférez les checks en‑proc ou les sondes natives.
- Rendez‑le observable : imprimez une raison d’échec sur une ligne et un résumé concis en cas de succès.
- Testez sous stress : exécutez les healthchecks pendant que CPU et I/O sont contraints ; voyez s’ils basculent.
- Décidez de la politique de redémarrage : qui redémarre sur unhealthy, avec quel backoff ? Documentez‑le. Implémentez‑le délibérément.
- Révisez régulièrement : les healthchecks pourrissent au fil du temps. Ajoutez‑les à votre liste de revue « correction production ».
Checklist : sanity pré‑déploiement pour stacks Compose
- Les dépendances utilisent‑elles
condition: service_healthyquand c’est approprié ? - Les healthchecks utilisent‑ils les bons hostnames (noms de service), pas
127.0.0.1entre conteneurs ? - Les intervalles sont‑ils raisonnables (pas 1–2s sur des dizaines de replicas à moins que le check soit vraiment trivial) ?
- Les timeouts et retries sont‑ils ajustés pour la charge et la gigue attendues ?
- Les checks évitent‑ils les effets secondaires et les requêtes lourdes ?
- Les checks échouent‑ils pour les modes de défaillance qui vous importent réellement ?
Checklist : réponse à incident quand les healthchecks sont impliqués
- Le healthcheck échoue‑t‑il parce que le service est cassé, ou parce que le check est trop strict ?
- L’automatisation de redémarrage amplifie‑t‑elle le problème ?
- Les logs de santé montrent‑ils une dépendance spécifique en échec (auth, DNS, timeout) ?
- Le goulot est‑il CPU, mémoire, disque ou saturation upstream ?
- Pouvez‑vous dégrader gracieusement au lieu d’échouer brutalement ?
FAQ
1) Est‑ce que chaque conteneur doit avoir un healthcheck ?
Non. Ajoutez des healthchecks là où ils motivent une décision : routage, dépendances, ou diagnostic rapide.
Pour des jobs one‑shot ou des sidecars stateless triviaux, un healthcheck peut être du bruit. Ne faites pas de cargo‑cult.
2) Quelle est la différence entre « liveness » et « readiness » dans Docker ?
Docker n’expose qu’un état de santé, mais vous pouvez choisir vos sémantiques.
« Liveness » = « le processus peut encore faire des progrès ». « Readiness » = « sûr pour recevoir du trafic ».
Pour les frontends et APIs, priorisez la readiness. Pour les workers, priorisez le progrès.
3) Pourquoi ne pas juste vérifier que le port est ouvert ?
Parce qu’un port peut être ouvert alors que l’appli est cassée : handlers verrouillés, pools de threads épuisés,
dépendances en échec, ou proxy servant une page statique. Les checks de port ne détectent que les pannes les plus paresseuses.
4) À quelle fréquence dois‑je exécuter un healthcheck ?
Commencez par 10–30 secondes pour la plupart des services. Des checks plus rapides augmentent la charge et le risque de flapping.
Si vous avez besoin d’une détection sub‑seconde, vous résolvez probablement le mauvais problème ou utilisez le mauvais outil.
5) Quels timeouts et retries dois‑je utiliser ?
Les timeouts doivent être inférieurs aux timeouts clients et refléter la latence attendue sous charge plus une marge.
Les retries doivent tolérer les problèmes transitoires (1–3) sans masquer une défaillance persistante. Mesurez démarrage et régime permanent séparément.
6) Un healthcheck doit‑il tester les dépendances comme bases et caches ?
Si le service ne peut pas fonctionner sans elles, oui — superficiellement.
Si le service peut se dégrader gracieusement, ne faites pas échouer la readiness pour des dépendances optionnelles. C’est ainsi qu’on transforme des pannes partielles en pannes totales.
7) Pourquoi mon healthcheck passe en un conteneur mais échoue dans un autre ?
Les namespaces. 127.0.0.1 dans un conteneur est ce conteneur. Le DNS de service diffère entre réseaux.
Aussi, certificats et auth peuvent être spécifiques à l’environnement. Testez toujours depuis le même chemin réseau où le check tournera.
8) Docker redémarre‑t‑il automatiquement les conteneurs unhealthy ?
Pas par défaut. Docker rapporte l’état de santé ; les redémarrages sont pilotés par des politiques de restart (à la sortie) ou une automatisation externe.
Faites très attention en reliant « unhealthy » aux redémarrages. Ajoutez du backoff et évitez les tempêtes de redémarrages.
9) Mon endpoint de santé doit‑il renvoyer des diagnostics détaillés ?
En interne, oui : une chaîne de raison courte est précieuse pendant les incidents. En externe, soyez prudent : ne divulguez pas de secrets ni de topologie.
Beaucoup d’équipes servent un /healthz minimal public et un endpoint de diagnostic interne protégé.
10) Comment empêcher les healthchecks de surcharger la base de données ?
Utilisez des sondes natives (pg_isready) ou des checks au niveau appli qui réutilisent les pools de connexions.
Augmentez l’intervalle. Évitez les requêtes qui scannent des tables. Votre healthcheck doit être moins coûteux qu’une requête réelle.
Prochaines étapes que vous pouvez livrer cette semaine
Arrêtez de laisser « vert » signifier « bon ». Les healthchecks sont des contrôles de production, pas du YAML décoratif.
Si votre check ne correspond pas aux vrais modes de défaillance, il certifiera des pannes avec assurance.
- Auditez vos 5 services principaux : que testent exactement leurs healthchecks, et est‑ce le même chemin que prennent vos utilisateurs ?
- Ajoutez un signal de readiness conscient des dépendances là où cela compte (API, proxy, gateway). Gardez‑le superficiel et limité dans le temps.
- Ajustez les timeouts et start periods en vous basant sur des mesures de démarrage et de charge, pas des suppositions.
- Rendez la sortie exploitable : raisons d’échec sur une ligne qui apparaissent dans
docker inspect. - Revoyez le comportement de redémarrage : si « unhealthy » déclenche des redémarrages, ajoutez du backoff et vérifiez que vous ne construisez pas une spirale mortelle.
L’objectif n’est pas de rendre les healthchecks stricts. C’est de les rendre honnêtes. Des checks honnêtes n’empêcheront pas toutes les pannes.
Ils empêchent la pire : celle que vos systèmes jurent ne pas vivre.