Health check v Dockeru spustí první kontrolu až po uplynutí celého interval od startu kontejneru, ne hned. S výchozím nastavením to je 30 sekund ve stavu health: starting. S --interval=1h je to hodina a všechno, co čeká na service_healthy, čeká také celou hodinu. Řešením není kratší interval po celou dobu života kontejneru. Nastav start_period dost dlouhý na to, aby aplikace nabootovala, a na Docker Engine 25.0 nebo novějším přidej krátký start_interval, aby se kontrola při startu ptala často a po nastartování jen zřídka. Druhá věc, ve které se lidé často mýlí: kontejner označený jako unhealthy dál běží. Čistý Docker ho nerestartuje a Compose také ne.
Tento článek rozebírá časování do detailu: každou z pěti voleb, jak se skládají do „za jak dlouho bude healthy“ a „za jak dlouho bude unhealthy“, syntaxi v Dockerfile a Compose vedle sebe, psaní kontroly pro image bez curl a čtení výsledku. K tomu, co má kontrolovat samotný endpoint a proč jsou liveness a readiness dvě různé otázky, se podívej na graceful shutdown a skutečné health checky. K tomu, jak health check zapadá do celého stacku s proxy a databází, se podívej na Docker Compose pro malé stacky.
Pět časovacích voleb a jejich výchozí hodnoty#
Health check je příkaz, který Docker na časovač spouští uvnitř kontejneru. Návratový kód 0 znamená healthy, 1 znamená unhealthy a 2 je rezervovaný a neměl by se používat. V praxi Docker bere jako selhání jakýkoli nenulový kód, ale dokumentace žádá 1, takže obvyklý idiom končí kontrolu příkazem || exit 1.
| Volba | Výchozí | Co řídí |
|---|---|---|
--interval | 30s | Čas mezi kontrolami za běhu |
--timeout | 30s | Jak dlouho smí jedna kontrola trvat, než se počítá jako neúspěšná |
--retries | 3 | Počet po sobě jdoucích selhání potřebný k označení za unhealthy |
--start-period | 0s | Odklad po startu, během kterého se selhání nepočítají |
--start-interval | 5s | Čas mezi kontrolami během start period (Engine 25.0+) |
Několik detailů, které tabulka vynechává:
- Interval se počítá od konce poslední kontroly, ne od jejího začátku. Kontrola, která trvá 4 sekundy při intervalu 30 sekund, se spouští zhruba každých 34 sekund.
- Timeout je tvrdý limit jednoho běhu. Pokud příkaz po jeho vypršení pořád běží, je ten běh neúspěšný. Výchozích 30 sekund je na lokální HTTP požadavek až příliš štědrých. Pokud
/healthzneodpoví do 3 až 5 sekund, něco je špatně a delší čekání jen oddaluje zjištění. - Retries počítá po sobě jdoucí selhání. Jediný úspěch počítadlo vynuluje. Kontrola, která selže každý druhý pokus, se k
unhealthynikdy nedostane, a to je dobrý důvod, proč kontrolu psát deterministicky. - Start period neodkládá první kontrolu. Kontroly během něj normálně běží, jejich selhání se jen nepočítají. Úspěch během start period ho předčasně ukončí: kontejner je healthy a každé další selhání se počítá.
- Start interval platí jen uvnitř start period. Při
start_period: 0s, tedy ve výchozím stavu,start_intervalnedělá nic.
Proč první kontrola čeká celý interval#
Tohle je chování za většinou dotazů typu „můj kontejner uvízl ve stavu starting“. Docker naplánuje první kontrolu jeden interval po startu kontejneru. Nespustí žádnou v čase nula, aby zjistil, jestli aplikace už neběží. Dokud první kontrola neuspěje, docker ps ukazuje (health: starting).
S výchozími 30 sekundami je to neškodné. Přestane to být neškodné ve chvíli, kdy si někdo řekne: „kontrola je levná, ale nepotřebuju ji často, tak dám interval: 5m“. Každý nový kontejner pak sedí pět minut ve stavu starting. Každá služba s depends_on: condition: service_healthy čeká pět minut, než ji Compose vůbec vytvoří. A docker compose up --wait blokuje pět minut u nasazení, které bylo hotové za dvě sekundy. Při interval: 1h je kontejner ve stavu starting celou hodinu.
Před Engine 25.0 z toho nebylo čisté východisko. Dlouhý interval znamenal pomalý první verdikt a krátký interval znamenal ptát se každých pár sekund donekonečna. Lidé se dohodli na kompromisu 10 až 30 sekund a smířili se s oběma nevýhodami.
--start-interval obě věci odděluje. Během start period Docker kontroluje každý start_interval. Po prvním úspěchu, nebo když start period skončí, přepne na běžný interval. Kontejner tak může být při bootu kontrolován každé 2 sekundy a potom každých 5 minut:
HEALTHCHECK --interval=5m --timeout=3s --retries=3 \ --start-period=60s --start-interval=2s \ CMD curl -fsS http://127.0.0.1:8080/healthz || exit 1Pokud aplikace odpoví po 7 sekundách, uspěje čtvrtá nebo pátá kontrola a kontejner je healthy zhruba 8 sekund po startu, ne za 5 minut. Pokud aplikace nenaběhne vůbec, start period vyprší po 60 sekundách a od té chvíle se selhání počítají v 5minutovém intervalu.
Za jak dlouho healthy, za jak dlouho unhealthy#
Dvě čísla, která stojí za to u každé kontroly spočítat, jsou: jak rychle se dobrý kontejner prohlásí za healthy a jak pomalu se mrtvý prohlásí za unhealthy.
Čas do healthy je zhruba doba bootu zaokrouhlená nahoru na další kontrolu. Bez start interval je první kontrola v čase interval, takže kontejner nikdy nebude healthy dřív. Se start interval je to doba bootu plus nanejvýš jeden start_interval.
Čas do unhealthy u aplikace, která se zasekne poté, co byla healthy, je zhruba retries kontrol:
worst case ~ retries x (interval + timeout)defaults: 3 x (30s + 30s) = about 3 minutestuned: 3 x (10s + 3s) = about 40 secondsTimeout se počítá, protože zaseknutá aplikace spojení neodmítne. Přijme ho a nikdy neodpoví, takže každá kontrola běží až do plného timeoutu, než selže. To je druhý důvod, proč výchozí timeout zkrátit: s výchozími hodnotami může zaseknutý proces tři minuty vracet chyby, než Docker přizná, že je něco špatně.
U aplikace, která healthy nenaběhne nikdy, přidej start period. Selhání v něm se nepočítají, takže první započtené selhání přijde po start_period a unhealthy následuje o retries kontrol později.
Několik výchozích bodů, které uprav podle své aplikace:
| Typ zátěže | Interval / timeout | Start period |
|---|---|---|
| Malá webová aplikace nebo API | 30s / 3s | 30s, start interval 2s |
| PostgreSQL nebo MySQL | 10s / 5s | 30s až 60s |
| Aplikace v JVM nebo .NET | 30s / 5s | 60s až 120s |
| Databáze obnovující dump při prvním spuštění | 10s / 5s | Několik minut |
Drž retries na 3, pokud k tomu nemáš důvod. Jedno selhání je šum. Tři v řadě už jsou vzorec.
Syntaxe v Dockerfile a v Compose#
Stejná kontrola může žít na třech místech a v každém se píše trochu jinak.
V Dockerfile je to instrukce s přepínači a stává se součástí metadat image:
HEALTHCHECK --interval=30s --timeout=3s --start-period=30s --start-interval=2s \ --retries=3 CMD ["curl", "-fsS", "http://127.0.0.1:8080/healthz"]Dockerfile může mít jen jeden HEALTHCHECK. Pokud jich je víc, vyhrává poslední. Health check v základním image se dědí a HEALTHCHECK NONE ho vypne.
V Compose je to klíč healthcheck: u služby, s podtržítky tam, kde mají přepínače pomlčky:
services: app: image: example/app:1.4.2 healthcheck: test: ["CMD", "curl", "-fsS", "http://127.0.0.1:8080/healthz"] interval: 30s timeout: 3s retries: 3 start_period: 30s start_interval: 2sDoby se zapisují jako 1m30s, 10s nebo 500ms. healthcheck v Compose přepisuje ten zabudovaný v image pole po poli, takže můžeš ponechat příkaz z image a změnit jen časování. Chceš-li kontrolu z image odstranit úplně, použij disable: true (nebo test: ["NONE"]). start_interval v souboru Compose vyžaduje Docker Compose 2.20.2 nebo novější a stejně jako přepínač v Dockerfile také Engine 25.0 nebo novější, aby se projevil.
U `docker run` jsou stejné volby přepínače: --health-cmd, --health-interval, --health-timeout, --health-retries, --health-start-period, --health-start-interval a --no-healthcheck pro vypnutí zděděné kontroly.
Které místo je správné? Dej kontrolu do Dockerfile, když patří k aplikaci: image zná svůj port i cestu health endpointu a každé nasazení dostane rozumnou výchozí hodnotu. Dej ji do Compose, když závisí na nasazení, například jiný port, závislost, na kterou má kontrola čekat, nebo image třetí strany, který nemůžeš přebuildovat.
CMD vs CMD-SHELL a past se znakem dolaru#
Pole test má tři podoby a rozdíl je důležitý, jakmile příkaz obsahuje rouru, proměnnou nebo ||.
["CMD", "curl", "-fsS", "http://127.0.0.1:8080/healthz"]- exec forma. Docker spustí binárku přímo, bez shellu. Žádné roury, žádné rozbalování$VAR, žádné||. Funguje i v image, které shell nemají.["CMD-SHELL", "curl -fsS http://127.0.0.1:8080/healthz || exit 1"]- spustí řetězec ve výchozím shellu kontejneru, na Linuxu/bin/sh -c. Roury, proměnné i||fungují.test: curl -fsS http://127.0.0.1:8080/healthz || exit 1- obyčejný řetězec je totéž coCMD-SHELL.
V Dockerfile existuje stejné rozdělení: HEALTHCHECK CMD ["curl", ...] je exec forma a HEALTHCHECK CMD curl ... || exit 1 je shell forma.
Past jsou proměnné v Compose. Compose si $VAR interpoluje sám, z tvého shellu a souboru .env, dřív než je kontejner vůbec uvidí. Kontrola napsaná jako pg_isready -U $POSTGRES_USER dostane hodnotu ze stroje, na kterém běží docker compose, a ta je často prázdná. Aby proměnnou rozbalil shell kontejneru, zdvoj znak dolaru:
db: image: postgres:17 environment: POSTGRES_USER: app POSTGRES_PASSWORD_FILE: /run/secrets/db_password healthcheck: test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_USER}"] interval: 10s timeout: 5s retries: 5 start_period: 30s start_interval: 2s$$ dorazí do kontejneru jako jediné $. Prostředí kontejneru POSTGRES_USER obsahuje, protože health checky běží s prostředím kontejneru, stejně jako docker exec.
Ještě dvě věci o tom, kde kontrola běží. Běží uvnitř kontejneru, takže localhost je samotný kontejner, ne hostitel. Pro kontrolu aplikace je to správně, ale znamená to, že health check nikdy netestuje publikovaný port ani proxy před aplikací. A raději používej 127.0.0.1 než localhost: v některých image se localhost překládá nejdřív na IPv6 ::1, aplikace navázaná na 0.0.0.0 (jen IPv4) spojení odmítne a kontrola selže, i když je aplikace v pořádku.
Když image nemá curl ani wget#
Příklady výše počítají s curl a spousta image ho neobsahuje. Slim a minimální image vynechávají vše, co aplikace nepotřebuje: node:22-slim a python:3.13-slim nemají ani curl, ani wget. Image založené na Alpine mají BusyBox wget, ale ne curl. Distroless image nemají ani shell, takže jakákoli kontrola přes CMD-SHELL selže ještě před spuštěním. Kontrola, která nenajde svou binárku, selhává pokaždé a kontejner přejde do stavu unhealthy, i když je aplikace úplně v pořádku. docker inspect pak v logu kontroly ukáže curl: not found nebo chybu exec.
Existují tři cesty, jak to obejít.
- Nainstalovat nástroj. Přidat
curldo Debian slim image je jeden řádekapt-get installa pár megabajtů. Často je to v pořádku, ale kvůli sondě to přidává do image další balíček a tím i útočnou plochu. - Použít to, co už tam je. Na Alpine funguje BusyBox
wget:wget -q --spider http://127.0.0.1:8080/healthz || exit 1. Databáze mají vlastní sondy:pg_isreadyv image Postgresu,mysqladmin pingv MySQL. - Použít runtime, který už máš. Node image má Node a Python image má Python. Oba umí HTTP požadavek na jeden řádek.
HEALTHCHECK --interval=30s --timeout=3s --start-period=20s --start-interval=2s \ CMD ["node", "-e", "fetch('http://127.0.0.1:3000/healthz').then(r => process.exit(r.ok ? 0 : 1)).catch(() => process.exit(1))"]HEALTHCHECK --interval=30s --timeout=3s --start-period=20s --start-interval=2s \ CMD ["python", "-c", "import urllib.request,sys; urllib.request.urlopen('http://127.0.0.1:8000/healthz', timeout=2)"]Verze v Pythonu funguje, protože urlopen vyhodí výjimku při jakémkoli stavu 4xx nebo 5xx i při odmítnutém spojení a neodchycená výjimka skončí se stavem 1. Obě jsou v exec formě, takže fungují i na image bez shellu. Spuštění interpretu Node nebo Python stojí víc než curl, na okamžik desítky megabajtů. Při 30sekundovém intervalu je to nepodstatné, při 2sekundovém start interval na malém stroji to stojí za zamyšlení.
U distroless image je nejčistším řešením drobná binárka pro health check zabudovaná do image, nebo režim --health ve vlastní aplikaci, který provede požadavek a skončí. Přesně tohle dělá spousta služeb v Go.
Ať použiješ jakýkoli nástroj, nastav ho tak, aby selhal při HTTP chybě, nejen při odmítnutém spojení. Samotný curl skončí s 0 i při odpovědi 500. -f (--fail) je to, co z chybového stavu udělá nenulový návratový kód. Bez něj kontrola dokazuje jen to, že něco poslouchá.
depends_on a service_healthy#
Stav health má největší význam pro pořadí startu v Compose. Závislost deklarovaná s condition: service_healthy zdrží závislou službu, dokud závislost nehlásí healthy:
app: image: example/app:1.4.2 depends_on: db: condition: service_healthy restart: trueTady se ladění intervalu vyplatí. Compose čeká na první úspěšnou kontrolu databáze, takže databáze s interval: 60s a bez start interval zdrží aplikaci při každém up nejméně o minutu, ať Postgres naběhne sebe rychleji. Se start_interval: 2s aplikace startuje během pár sekund poté, co je databáze připravená.
Compose to také vzdá, když závislost přejde do stavu unhealthy: up selže se zprávou, že se závislost nepodařilo nastartovat. Obvykle byl start period při prvním spuštění příliš krátký, například když databáze inicializuje nový svazek nebo obnovuje dump, zatímco pozdější restarty byly v pořádku. Prodluž raději start period než retries, aby odklad platil jen při bootu.
restart: true (Compose 2.17.0 nebo novější) restartuje závislou službu, když Compose sám restartuje nebo aktualizuje závislost. Není to restart podle zdraví. Databáze, která sama přejde do stavu unhealthy, nespustí nic.
docker compose up --wait používá stejný signál pro nasazení: vrátí se, až všechny služby běží a jsou healthy, a skončí nenulovým kódem, pokud některá selže. Ve skriptu pro nasazení díky tomu zastavíš před přepnutím provozu, ne po něm. Nasazení bez výpadku na malém serveru na tomhle čekání staví.
Čtení health logu přes docker inspect#
docker ps ukazuje shrnutí ve sloupci stavu: Up 2 minutes (healthy), (health: starting) nebo (unhealthy). Takto najdeš všechny unhealthy kontejnery na hostu:
$ docker ps --filter health=unhealthy$ docker inspect --format '{{json .State.Health}}' app | python3 -m json.toolObjekt Health obsahuje Status, FailingStreak (zatím po sobě jdoucí selhání) a Log, seznam posledních výsledků kontrol. Každý záznam má Start, End, ExitCode a Output. Docker si pamatuje jen posledních pět výsledků a z výstupu každé kontroly ukládá jen prvních 4096 bajtů. Health endpoint, který při selhání vypíše stručný důvod (db: connection refused), je tady proto mnohem užitečnější než ten, který vyplivne stack trace.
Než cokoli změníš, přečti si Output. Běžné zprávy a jejich význam:
curl: not found,executable file not found in $PATH- nástroj v image chybí. Viz předchozí sekce.Health check exceeded timeout- příkaz neskončil včas. Buď je aplikace zaseknutá, nebo je timeout kratší než legitimně pomalá odpověď.Connection refused- na té adrese uvnitř kontejneru nic neposlouchá. Zkontroluj port a porovnej127.0.0.1slocalhost.- HTTP
503z tvého vlastního endpointu - kontrola funguje a aplikace hlásí skutečný problém. Oprav aplikaci, ne kontrolu.
Přechody můžeš sledovat i za běhu pomocí docker events --filter event=health_status, který vypíše řádek pokaždé, když kontejner přejde mezi healthy a unhealthy.
Unhealthy nic nerestartuje#
Restart policy Dockeru (no, on-failure, always, unless-stopped) reagují na ukončení hlavního procesu kontejneru a na nic jiného. Kontejner unhealthy, jehož proces pořád běží, zůstane přesně tak, jak je, a dál dostává provoz, pokud ho dostával předtím. Stav health je informace. Čistý Docker a Compose ho hlásí a jednají podle něj jen při startu, přes depends_on a --wait.
Podle něj jednají tři věci:
- Docker Swarm nahrazuje úlohy služby, které přejdou do stavu unhealthy. Pokud provozuješ Swarm, je health check skutečným spouštěčem restartu.
- Hlídač, kterého si přidáš. Malý kontejner s přístupem k Docker socketu hledá
unhealthya to, co najde, restartuje. Známým příkladem je imagewillfarrell/autoheal. Připojení Docker socketu dává tomuto kontejneru kontrolu nad hostitelem rovnocennou rootu, takže to ber jako kompromis, kterým to je. - Aplikace, která sama skončí. Pokud aplikace pozná, že je rozbitá, je ukončení s nenulovým kódem nejspolehlivější spouštěč restartu, jaký existuje, protože mu rozumí každá restart policy.
Kubernetes je výjimka, o kterou lidé zakopávají: HEALTHCHECK z Dockerfile úplně ignoruje a používá vlastní sondy liveness, readiness a startup. startupProbe tam hraje roli start period.
Pozor na to, co si od automatických restartů slibuješ. Kontrola, která selže, protože spadla databáze, bude donekonečna restartovat zdravou aplikaci, což článek o graceful shutdown nazývá zesilovačem. Cokoli se restartuje podle zdraví, by mělo sledovat jen věci, které restart opraví. A upozorňuj na změnu stavu health, jak popisuje monitoring, který ti něco řekne, místo abys ji tiše zamazával.
Na app plánech RE:NODE vypadá otázka trochu jinak. Každý server je kontejner, který provozujeme my, bez tvého Docker daemonu uvnitř, takže tu není žádný HEALTHCHECK k ladění. Místo toho crash watcher každé dvě minuty kontroluje, jestli uptime nešel zpátky nebo jestli server není offline, a restarty, o které sis řekl, se nepočítají. Tři neplánované restarty za hodinu zobrazí upozornění na stránce serveru a otevřou ticket. Šest server pozastaví. Ukončení při fatální chybě je tu tedy pořád správný návrh. Pokud chceš vlastní health mechanismus Dockeru s Compose a service_healthy, potřebuješ stroj, kde je daemon tvůj, a k tomu slouží řada VDS. Docker na VDS pokrývá nastavení.
FAQ#
Proč můj kontejner uvízl ve stavu „health: starting“?
První kontrola zatím neuspěla. Buď proběhne první kontrola až po celém interval (dlouhý interval tedy znamená dlouhé čekání), nebo dosavadní kontroly všechny selhaly uvnitř start period, kde se selhání nepočítají. Spusť na kontejneru docker inspect --format '{{json .State.Health}}'. Prázdný Log znamená, že žádná kontrola ještě neproběhla. Záznamy s nenulovými návratovými kódy znamenají, že kontrola selhává, a Output říká proč.
Jaký je dobrý interval pro Docker healthcheck?
Pro typickou webovou službu 30 sekund s timeoutem 3 sekundy a 3 retries, což zaseknutou aplikaci označí za méně než dvě minuty. Kratší hodnoty volej jen tehdy, když na výsledek rychle reaguje něco jiného, například load balancer nebo Swarm. Na Engine 25.0 nebo novějším spáruj každý interval delší než minutu se start_period a start_interval několika sekund, aby start nečekal na dlouhý interval.
Odkládá start_period první health check?
Ne. Kontroly během start period běží v normálním interval, nebo v start_interval na Engine 25.0 a novějším. Selhání uvnitř okna se prostě nezapočítávají do retries. První úspěch start period předčasně ukončí.
Restartuje Docker unhealthy kontejner?
Sám od sebe ne. Restart policy se spouštějí jen při ukončení hlavního procesu. Cesty, jak z unhealthy udělat restart, jsou služby Swarm, hlídač typu autoheal, nebo aplikace, která sama skončí. Compose používá health jen k řazení startu a pro up --wait.
Má health check volat databázi?
Obvykle ne u kontroly, podle které se cokoli restartuje. Výpadek databáze by pak označil všechny aplikační kontejnery za unhealthy najednou. Kontroluj samotný proces a závislosti hlas zvlášť přes readiness endpoint. U kontejneru s databází má kontrola samozřejmě být vlastní sonda databáze, například pg_isready nebo mysqladmin ping.
Můžu změnit health check bez přebuildování image?
Ano. Blok healthcheck: v Compose nebo přepínače --health-* u docker run hodnoty z image přepíšou a disable: true nebo --no-healthcheck kontrolu vypne. Instrukce v Dockerfile je jen výchozí hodnota.


