Docker Compose healthcheck na talagang gumagana
Alamin kung paano sinusuri ang healthcheck, bakit walang kapaki-pakinabang na hinihintay ang depends_on lang, at paano i-check ang Postgres at app readiness.
Ano talaga ang ginagawa ng Docker Compose healthcheck
Ang Docker Compose healthcheck ay isang command na pana-panahong pinapatakbo ng Docker sa loob ng container. Hindi binabasa ng Docker ang logs mo, mino-monitor ang port, o sinusuri ang process list. Pinapatakbo nito ang command, binabasa ang exit code, at nagtatala ng isang state sa container: starting, healthy, o unhealthy. Ang exit code 0 ay nangangahulugang healthy. Anumang ibang exit code ay nangangahulugang unhealthy, at nakalaan ang exit code 2 para sa Docker, kaya huwag itong sadyang i-return.
Iyan ang buong mekanismo. Halos lahat ng problema sa healthcheck ay iisa ang pinagmumulan: ibang tanong ang sinasagot ng isinulat mong command kaysa sa tanong na nais mong itanong. Ipinapalagay ng gabay na ito na alam mo na kung paano magsulat ng compose file sa isang VPS, at nagpapatuloy ito mula sa puntong nagsisimula ang stack sa maling pagkakasunod-sunod.
services:
api:
image: ghcr.io/example/api:1.4.0
healthcheck:
test: ["CMD", "curl", "-fsS", "http://localhost:8080/healthz"]
interval: 10s
timeout: 3s
retries: 5
start_period: 30sAng value na test ay may dalawang kapaki-pakinabang na anyo. Ang listahang nagsisimula sa CMD ay direktang nagpapatakbo ng command, nang walang shell, kaya hindi gumagana ang pipes, &&, at variable expansion. Ipinapasa ng listahang nagsisimula sa CMD-SHELL ang natitira bilang isang string sa /bin/sh -c sa loob ng container. Ito ang dapat gamitin kapag nangangailangan ng shell syntax ang check. Ang plain string ay itinuturing na CMD-SHELL. Tinatanggal ng listahang eksaktong may ["NONE"] ang healthcheck na isinama ng image sa pamamagitan ng Dockerfile.
Tumatakbo ang check sa loob ng container, kaya kailangang umiiral sa image ang bawat binary na tinutukoy nito. I-verify muna ito, dahil ang slim image na walang curl ay nagreresulta sa container na permanenteng unhealthy dahil sa problemang hindi kailanman lumilitaw sa application log. Manu-manong i-test:
docker compose exec api curl --versionAng nawawalang binary ay nagbabalik ng OCI runtime exec failed: exec: "curl": executable file not found in $PATH: unknown. Karaniwang kasama sa Alpine-based images ang BusyBox wget, kaya nagiging ["CMD", "wget", "-q", "-O", "-", "http://localhost:8080/healthz"] ang check.
Paano pinagsasama ang interval, retries, at start_period
Limang setting ang kumokontrol sa timing. Ang mga default nito ay galing sa Docker Engine, hindi sa Compose.
interval: pagitan ng dalawang check kapag lumampas na ang container sa start period nito. Default: 30s.timeout: pinakamahabang oras na maaaring tumakbo ang isang check bago ito ihinto ng Docker at bilangin ang run na iyon bilang failure. Default: 30s.retries: bilang ng magkakasunod na failure na kailangan bago magbago ang state saunhealthy. Default: 3.start_period: grace window matapos magsimula ang container. Default: 0s.start_interval: gaano kadalas tumatakbo ang check sa panahon ng start period. Default: 5s, at kailangan nito ng Docker Engine 25.0 o mas bago.
Ito ang mahalagang rule: sa panahon ng start period, ang failing check ay hindi ibinibilang sa retries, at nananatili ang container sa starting. Sa unang pagkakataong magtagumpay ang check, nagiging healthy ang container at agad na nagtatapos ang start period, kahit malaking bahagi ng oras nito ang hindi nagamit. Kung matapos ang start period habang failing pa rin ang check, magsisimula ang normal na countdown, at kailangan ng container ng retries magkakasunod na failure bago ito mamarkahang unhealthy.
Samakatuwid, ang pinakamahabang oras mula sa pagsisimula ng container hanggang sa unhealthy ay start_period plus retries na minultiply sa interval, plus timeout. Gamit ang mga value sa file sa itaas, iyon ay 30 plus 5 times 13, o 95 seconds. Isulat ang numerong ito bago magtakda ng deploy timeout, dahil hindi kailanman makikitang umabot sa final state ang container kung susuko ang rollout makalipas ang 60 seconds.
Ang karaniwang pagkakamali rito ay taasan ang retries para matugunan ang mabagal na startup. Gumagana ito sa isang pagkakataon, pero nakasasama habang tumatagal: ang service na nangailangan ng 8 retries para mag-boot ay magtitiis na ngayon ng 8 magkakasunod na failure sa production bago ito mapansin. Gamitin sa halip ang start_period, dahil nalalapat lamang ito bago ang unang tagumpay.
Bakit walang garantiya ang depends_on kapag ito lamang ang ginagamit
Ang short form ng depends_on ang pangunahing pinagmumulan ng kalituhan.
api:
depends_on:
- dbIsang bagay lamang ang ibig sabihin nito: simulan ang db container bago ang api container. Hinihintay ng Compose na magawa at masimulan ang container. Hindi nito hinihintay na matapos ng PostgreSQL ang first-time initialization nito, at hindi rin nito hinihintay na tumanggap ng connection ang port 5432. Magsisimula ang app mo makalipas ang humigit-kumulang isang segundo, kokonekta ito sa port na wala pang nakikinig, at lalabas. Sa log, makikita mo ang Connection refused, o ang FATAL: the database system is starting up kapag naka-up na ang server pero nagre-recover pa.
Ang long form ang karaniwang talagang kailangan ng mga user:
api:
depends_on:
db:
condition: service_healthy
restart: true
migrate:
condition: service_completed_successfullyMay tatlong value ang condition. Ang service_started ay katumbas ng short form. Pinipigilan ng service_healthy na magsimula ang dependent service hanggang i-report ng dependency na healthy ito. May saysay lamang ito kapag may dine-define na healthcheck ang dependency, sa compose file man o sa image nito. Hinihintay ng service_completed_successfully ang isang one-shot container, gaya ng database migration, na mag-exit na may status 0.
May dalawang karagdagang field sa tabi ng condition. Sinasabi ng restart: true sa Compose na i-restart ang service na ito matapos nitong i-update ang dependency service. Ginagawang warning sa halip na error ng required: false ang nawawalang dependency.
Narito ang limitasyong madalas nakalilito. Sinusuri ang mga condition na ito kapag umaakyat ang stack. Para lamang ito sa start ordering, hindi ito supervision rule. Kung mag-restart ang database nang 3 ng madaling-araw, walang muling magsusuri sa service_healthy at walang magre-restart sa app mo para muling matugunan ito. Kailangang ang application code mo mismo ang muling kumonekta. Sadyang nilalampasan ng docker compose up --no-deps api ang buong mekanismong ito. Gayundin ang direktang pagsisimula ng container gamit ang docker start.
Sumulat ng check na sumusukat sa readiness, hindi sa pagkakaroon lang ng process
Ang check na tulad ng pgrep nginx ay nagpapatunay lang na may entry ang process table. Wala itong pinatutunayan tungkol sa kakayahan ng service na sumagot sa request. Maaaring manatiling bukas ang listening socket ng isang web application kahit matagal nang namatay ang database pool nito, at mananatiling green ang process check sa buong outage.
Ipatrabaho sa container ang mismong tungkulin nito:
- Para sa HTTP service, mag-request ng aktuwal na endpoint. Nag-e-exit ang
curl -fsSnang non zero sa anumang status na 400 o mas mataas dahil sa-f, kaya failed check ang 500 mula sa sirang app. - Para sa PostgreSQL, gamitin ang
pg_isready. Nag-e-exit ito sa 0 kapag tumatanggap ng connections ang server, sa 1 kapag nire-reject nito ang mga ito, sa 2 kapag hindi ito tumutugon, at sa 3 kapag mali ang mga parameter na ipinasa mo. - Para sa Redis, gamitin ang
redis-cli ping. Pini-print nito angPONGat nag-e-exit sa 0. - Para sa MariaDB, may kasamang
healthcheck.shscript ang official image, at anghealthcheck.sh --connect --innodb_initializedang form na idinodokumento ng mga maintainer nito.
May isang trap na dapat malaman sa pg_isready. Sa pinakaunang start nito na walang laman ang data directory, isinasagawa ng official postgres image ang initialization laban sa temporary server na nakikinig lamang sa Unix socket. Ginagamit ng pg_isready na walang host argument ang socket na iyon, kaya maaari itong sumagot ng “tumatanggap ng connections” kahit sarado pa ang TCP port 5432 para sa application mo. Tiyaking TCP ang tahasang tinutukoy ng check at mawawala ang problema, dahil hindi tumutugon doon ang temporary server.
healthcheck:
test: ["CMD-SHELL", "pg_isready -h 127.0.0.1 -p 5432 -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
interval: 5s
timeout: 5s
retries: 10
start_period: 30sHindi typo ang magkaparis na dollar sign. Ine-expand mismo ng Compose ang $VAR habang binabasa nito ang file, kaya maaaring mailagay sa check ang value mula sa environment ng host mo. Inie-escape ito ng $$ bilang isang $, kaya ine-expand ito ng shell sa loob ng container batay sa sariling environment ng container.
Isang postgres at app stack na nagsisimula sa tamang pagkakasunod-sunod
services:
db:
image: postgres:17.5
environment:
POSTGRES_USER: appuser
POSTGRES_PASSWORD: ${DB_PASSWORD:?set DB_PASSWORD in .env}
POSTGRES_DB: appdb
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -h 127.0.0.1 -p 5432 -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
interval: 5s
timeout: 5s
retries: 10
start_period: 30s
restart: unless-stopped
api:
image: ghcr.io/example/api:1.4.0
environment:
DATABASE_URL: postgres://appuser:${DB_PASSWORD}@db:5432/appdb
depends_on:
db:
condition: service_healthy
healthcheck:
test: ["CMD", "curl", "-fsS", "http://localhost:8080/healthz"]
interval: 10s
timeout: 3s
retries: 5
start_period: 30s
ports:
- "127.0.0.1:8080:8080"
restart: unless-stopped
volumes:
pgdata:Simulan ito at obserbahan ang pagbabago ng mga state:
docker compose up -d
docker compose psMakikita sa column na STATUS ang health state na nasa loob ng mga bracket. Kapag healthy ang dalawang container, mababasa ang Up 41 seconds (healthy) sa parehong row. Habang nag-i-initialize pa ang database, db ang mababasa sa Up 4 seconds (health: starting) at wala sa listahan ang api dahil hindi pa ito nagagawa ng Compose.
Para makita kung bakit nag-pass o nag-fail ang isang check, basahin ang health log:
docker inspect --format '{{json .State.Health}}' "$(docker compose ps -q db)"Pinapanatili ng Docker ang mga huling resulta. Bawat isa ay may start time, end time, ExitCode, at Output ng command. Truncated ang naka-store na output. Kaya magiging walang silbi ang log entry kapag nag-print ang isang check ng malaking page body. Panatilihing tahimik ang mga check.
Ano ang ginagawa ng Docker kapag naging unhealthy ang isang container
Wala. Ito ang sagot na kadalasang ikinagugulat ng mga tao.
Hindi nire-restart ng Docker Engine sa iisang host ang isang unhealthy na container. Tumutugon ang restart: unless-stopped policy kapag nag-exit ang main process, ngunit hindi nag-exit ang unhealthy na container. Maaari itong manatili sa unhealthy nang isang linggo habang hindi ito ginagalaw ng Compose. Pinapalitan ng Swarm mode ang mga unhealthy task, ngunit hindi ito ginagawa ng isang plain Compose stack sa iisang server.
Dalawa ang praktikal na opsyon. Ipa-exit ang process kapag alam nitong may sira ito, para may aksyunan ang restart policy. O subaybayan ang state mula sa labas at mag-set up ng alert dito. Kapag itinuro mo ang isang Uptime Kuma monitor sa parehong endpoint na tinatawagan ng healthcheck, makikita ang sirang dependency sa parehong lugar, at makatatanggap ka ng alert mula sa monitor sa halip na mula sa user. Kung dumadaan ang traffic sa app sa pamamagitan ng isang Traefik reverse proxy, tandaan na hiwalay ang pagtingin ng proxy sa backend mula sa Docker health state, kaya hindi nito napapalitan ang isa't isa.
Pag-debug ng check na hindi kailanman nagiging healthy
Patakbuhin mismo ang eksaktong command sa parehong container, at tingnan ang exit code:
docker compose exec api curl -fsS http://localhost:8080/healthz; echo "exit=$?"Ang exit=0 dito habang unhealthy pa rin ang container ay nangangahulugang iba ang compose test mo sa kaka-type mo lang. Karaniwan itong nangyayari kapag ginamit ang CMD kung saan kailangan ang shell syntax.
Dalawang pagkakamali ang sanhi ng karamihan sa iba pang problema. Una, maling port. Tumatakbo ang healthcheck sa loob ng container, kaya dapat nitong gamitin ang container port, hindi kailanman ang published host port. Sa ports: - "8080:3000", nakikinig ang application sa port 3000. Kaya palaging nagfa-fail ang check laban sa http://localhost:8080 kahit maayos na gumagana ang site sa browser. Ikalawa, maling host. Sa loob ng check, ang localhost ay ang parehong container. Tama ito kapag sarili nito ang chine-check, pero mali kapag ibang container ang chine-check. Sa ganitong kaso, gamitin ang service name, gaya ng db.
May isa pang mahalagang kaso: pumapasa ang healthcheck habang errors naman ang nakikita ng mga user. Nangyayari ito kapag nagbabalik ang endpoint ng static na 200 nang walang aktuwal na chine-check. Hindi malalaman ng readiness endpoint na hindi na available ang database kung hindi ito kumokonekta sa database. Patakbuhin nito ang isang murang aktuwal na query.
FAQ
Bakit hindi pa rin makakonekta ang app ko kahit sinasabing healthy ang database sa depends_on?
Dahil isang beses lang sinusuri ang condition: service_healthy, kapag nagsisimula ang stack. Hindi na nito mina-monitor ang anuman pagkatapos nito. Kung mag-restart ang database container sa kalaunan, hindi nire-restart ng Compose ang application para muling matugunan ang condition. Kaya kailangang may sarili itong reconnect at retry logic sa application code. Wala ring epekto ang condition kapag nag-start ka ng isang container lang gamit ang docker start o docker compose up --no-deps.
Kailangan ko ba ng healthcheck kung mayroon nang nakadefine sa image?
Karaniwan, hindi na. Madalas ay hakbang paatras ang pag-override nito dahil alam ng image maintainer kung ano ang ibig sabihin ng readiness para sa software na iyon. Magdagdag ka lang ng sarili mong healthcheck kapag mali para sa setup mo ang check ng image, halimbawa, kapag chine-check nito ang port na inilipat mo. Para i-disable ang image healthcheck, itakda ang test: ["NONE"] o disable: true sa service.
Dapat bang gumamit ang healthcheck ng curl o wget?
Gamitin kung alinman sa mga ito ang mayroon na sa image, at kumpirmahin ito gamit ang docker compose exec <service> curl --version bago ka umasa rito. Maraming Debian-based image ang walang alinman sa dalawa. May BusyBox wget ang mga Alpine-based image. Huwag magdagdag ng package sa image para lang magpatakbo ng healthcheck kung may sarili nang client ang software, gaya ng pg_isready o redis-cli.
Awtomatiko bang nire-restart ang unhealthy container?
Hindi ng Docker Engine sa isang single host. Tumutugon ang restart policies sa pag-exit ng process, hindi sa health state. Kaya mananatiling tumatakbo at sira ang unhealthy container hanggang sa may ibang kumilos dito. Maaari mong palabasin ang process kapag natukoy nito ang failure, o magpatakbo ng external monitor na nag-a-alert kapag nagbago ang state.
Gaano dapat katagal ang start_period?
Dapat sapat ito para sa pinakamabagal na lehitimong unang start na nasukat mo, kasama ang margin. Sukatin ito gamit ang docker compose up laban sa empty volume, dahil mas mabagal nang malaki ang unang start ng database kaysa sa lahat ng kasunod na start. Kung masyadong mahaba ang start period, nade-delay lang nito ang unang unhealthy verdict. Kung masyadong mataas ang retries, humihina ang check sa buong buhay ng container. Ito ang mas masamang failure.