Van egy megnyugtató kis szertartás a konténeres életben: az ember beír a Dockerfile-ba egy HEALTHCHECK sort, aztán a docker ps kimenetében gyönyörködik a (healthy) feliratban. Az agy innen automatikusan továbbírja a történetet: ha egyszer az app megakad, a Docker majd észreveszi, és újraindítja. Ez a történet ott hibádzik, hogy a második fele nincs megírva. A sima Docker az unhealthy állapotra nem reagál. Semmit nem csinál. Lemértem, kipróbáltam, és összeszedtem, ki az, aki tényleg olvassa ezt a státuszt, ki az, aki cselekszik is belőle, és ki az, aki tüntetően ignorálja.

Egy státuszgép, három állapottal
A HEALTHCHECK utasítás annyit tesz, hogy a daemon időnként lefuttat egy parancsot a konténerben: alapból 30 másodpercenként (interval), 30 másodperces timeout-tal, és három egymást követő bukás (retries) után vált a státusz starting-ból vagy healthy-ből unhealthy-ba. A parancs exit kódja dönt: 0 = healthy, 1 = unhealthy, a 2-es pedig hivatalosan „reserved – don’t use”, ami elég ritka műfaj egy exit kód életében. Két kevésbé ismert opció is van: a start-period türelmi ideje alatt a bukott próba nem számít bele a retries-be, a Docker Engine 25.0 (2024. január) óta létező start-interval pedig azt mondja meg, hogy a türelmi idő alatt milyen sűrűn próbálkozzon — erre még visszatérünk, mert hasznosabb, mint amilyen ismeretlen.
Unhealthy. És akkor mi van?
A kísérlet: node:22-alpine konténer, benne egy app, ami tíz másodpercig egészséges, utána minden /health kérésre 500-at ad. Kétmásodperces interval, három retry, és — hogy izgalmasabb legyen — --restart always. Egy perccel az indítás után:
$ docker inspect hc-demo --format 'Status={{.State.Status}} RestartCount={{.RestartCount}} Health={{.State.Health.Status}} FailingStreak={{.State.Health.FailingStreak}}'
Status=running RestartCount=0 Health=unhealthy FailingStreak=30
Harminc egymás utáni bukott próba, always restart policy, és a konténer rezzenéstelenül fut tovább. A --restart ugyanis kizárólag arra reagál, ha a főprocessz kilép — a health státusz nem tartozik rá. Amit az unhealthy állapotért cserébe kapsz: egy zárójeles felirat a docker ps-ben, egy health_status esemény a docker events streamben, az utolsó öt próba naplója a docker inspect kimenetében (a próbaparancs kimenetének első 4096 bájtjával), és egy szűrő: docker ps --filter health=unhealthy. Ez monitoring. Nem gyógyítás.
És ez nem egy elfelejtett feature: a moby/moby#28400 issue („trigger restart from unhealthy status”) 2016 novembere óta van nyitva. Idén novemberben lesz tíz éve. A közösségi kerülőmegoldás az autoheal konténer, ami a Docker socketet figyelve újraindítgatja az unhealthy társait — működik, csak épp egy plusz konténernek adsz teljes daemon-hozzáférést azért a funkcióért, amit a platformtól várnál.
Aki mégis cselekszik: Compose és Swarm
A státusznak ott van értelme, ahol valaki olvassa is. A Compose a depends_on + condition: service_healthy feltétellel pontosan erre épít: a függő szolgáltatás el sem indul, amíg a függősége healthy nem lesz. Lemértem: egy nyolc másodperc alatt „bemelegedő” api mögött a web konténer 8,6 másodperccel később indult — pontosan az első sikeres próba pillanatában, mert a start_interval: 1s miatt másodpercenként kérdezett rá. Ez a civilizált alternatívája a sleep 10-nek és a wait-for-it.sh-nak:
services:
api:
image: sajat-api:latest
healthcheck:
test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:3000/health || exit 1"]
interval: 5s
retries: 3
start_period: 30s
start_interval: 1s
web:
image: sajat-web:latest
depends_on:
api:
condition: service_healthy
restart: true
A restart: true opcióval a lánc explicit újraindításnál is működik: a docker compose restart api után a Compose megvárta az api healthy állapotát, majd újraindította a webet is. Viszont — és ez a rész szokott kimaradni — a Compose nem daemon: a felállás után senki nem figyeli tovább a státuszt. Ha az api futás közben válik unhealthy-vá, a Compose pont annyit tesz, mint a sima Docker: semmit.
Az egyetlen hely, ahol a Docker maga tényleg cselekszik, a Swarm mód. Ugyanazt a tíz másodperc után elromló szervert service-ként elindítva a task másodpercek alatt repül:
$ docker service ps hc-svc --no-trunc
hc-svc.1 Running 9 seconds ago
hc-svc.1 Failed 17 seconds ago "task: non-zero exit (137): dockerexec: unhealthy container"
hc-svc.1 Failed 46 seconds ago "task: non-zero exit (137): dockerexec: unhealthy container"
SIGKILL (137-es exit kód), „unhealthy container”, és már jön is a friss példány — ami tíz másodperc múlva szintén elromlik, úgyhogy a Swarm türelmesen cseréli tovább, a végtelenségig. Ugyanaz a daemon, ugyanaz a healthcheck: docker run alatt dísz, Swarm alatt kivégzőosztag. Ebből az is következik, hogy Swarmban egy rosszul megírt healthcheck nem kozmetikai hiba, hanem restart-loop.
A Kubernetes pedig meg sem hallgatja
A Kubernetes a Dockerfile-os HEALTHCHECK-et nem rosszul használja, és nem is felülbírálja: soha le sem futtatja. A kubelet a saját liveness, readiness és startup próbáit kezeli, amiket a pod manifestben írsz le. Az OCI image-specifikációban a Healthcheck mező hivatalosan is csak „reserved […] to maintain compatibility” — egy Docker-örökség, amit a szabvány udvariasan eltűr, de definiált jelentése nincs, és a containerd nem is kezd vele semmit. A vicc az, hogy a dockershim-korszakban pont fordítva volt baj: a Docker daemon lefuttatta a HEALTHCHECK-et, a kubelet meg a saját próbáit, vagyis az app duplán kapta a terhelést — erről szól a kubernetes#50703 issue. Ma egyszerűbb a képlet: a Kubernetesbe szánt image HEALTHCHECK utasítása halott kód. Nem árt senkinek, csak hamis biztonságérzetet ad annak, aki azt hiszi, ezzel le van tudva a health check.
A hazug healthcheck: exit 127
A másik klasszikus hiba nem az, hogy a healthcheck nem csinál semmit, hanem az, hogy nem igazat mond. Beírod a mindenhonnan másolt curl -f http://localhost:3000/health sort egy node:22-alpine alapú image-be, az app hibátlanul válaszol, a konténer mégis unhealthy lesz:
exit: 127 out: "/bin/sh: curl: not found"
Az alpine-os Node-image-ekben ugyanis nincs curl. (BusyBox wget van, az működik.) Distroless image-ben viszont se shell, se wget, se curl — ott csak arra számíthatsz, ami biztosan ott van: magára a runtime-ra. A Node 18 óta beépített fetch-csel a legrobusztusabb megoldás egy exec formos egysoros, amihez shell sem kell:
HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
--start-period=30s --start-interval=2s \
CMD ["node", "-e", "fetch('http://127.0.0.1:3000/health').then(r=>process.exit(r.ok?0:1),()=>process.exit(1))"]
A start-period + start-interval páros pedig azt a dilemmát oldja fel, hogy induláskor sűrűn akarsz próbálkozni (hogy a Compose ne várjon feleslegesen), üzem közben viszont felesleges pár másodpercenként plusz processzt indítgatni a konténerben. Így néz ki a próbanapló egy hat másodperc alatt beinduló appnál, a fenti beállításokkal:
09:05:53.108 exit: 1 ← indulás után 2 mp-enként próbálkozik
09:05:55.158 exit: 1
09:05:57.200 exit: 0 ← első siker: a konténer healthy
09:06:27.249 exit: 0 ← innentől 30 mp-enként
09:06:57.299 exit: 0
Az első sikerig kétmásodperces tempó, utána pontosan harminc. A váltás automatikus: az első sikeres próba lezárja a start-periódust, akkor is, ha annak amúgy még lenne hátra ideje.
Mikor éri meg, mikor nem
Compose-alapú dev környezetben és CI-ban a healthcheck + service_healthy szerintem kötelező darab: olcsó, determinisztikus, és kigyomlálja a sleepeket meg a flaky integrációs teszteket. Swarmban ez maga az önjavító mechanizmus — ott tényleg számít, hogy jól van-e megírva. Kubernetesbe szánt image-be viszont felesleges HEALTHCHECK-et írni: senki nem futtatja le, a valódi próbák a manifestben élnek. A sima docker run + restart policy felállásnál pedig tudd, mit veszel: státuszjelzőt a monitoringodnak, nem újraindítást. Ha az utóbbi kell, az vagy Swarm, vagy egy systemd unit, vagy az autoheal — de a HEALTHCHECK magától soha.
A tanulság szárazabb, mint szeretném: a HEALTHCHECK soha nem ígért öngyógyítást, a happy endet mi képzeltük hozzá. A dokumentáció pontosan azt írja, amit a rendszer csinál — csak épp az ember általában akkor olvassa el azt a bekezdést, amikor hajnali kettőkor rácsodálkozik egy hetek óta unhealthy státuszban vidáman futó konténerre.
Források
- Dockerfile reference: HEALTHCHECK (Docker Docs)
- Control startup and shutdown order in Compose (Docker Docs)
- moby/moby#28400: trigger restart from unhealthy status (GitHub, 2016 óta nyitva)
- OCI Image Format Specification: config.md (a
Healthcheckmező mint „reserved”) - Liveness, Readiness and Startup Probes (Kubernetes docs)
- kubernetes#50703: Disable Health Checks from Docker daemon (GitHub)
- willfarrell/docker-autoheal (GitHub)