Werkende Docker Compose-healthchecks voor Postgres en apps
Lees hoe Docker Compose healthchecks werken, waarom depends_on niet op gereedheid wacht en hoe u betrouwbare readiness-checks voor Postgres en uw app schrijft.
Wat een Docker Compose-healthcheck daadwerkelijk doet
Een Docker Compose-healthcheck is één opdracht die Docker volgens een timer in de container uitvoert. Docker leest uw logs niet, controleert uw poort niet en inspecteert uw proceslijst niet. Het voert de opdracht uit, leest de afsluitcode en slaat één status op de container op: starting, healthy of unhealthy. Afsluitcode 0 betekent gezond. Elke andere afsluitcode betekent ongezond. Afsluitcode 2 is gereserveerd door Docker. Retourneer deze daarom nooit bewust.
Dat is het volledige mechanisme. Bij vrijwel elk probleem met een healthcheck is de oorzaak hetzelfde: de opdracht die u hebt geschreven beantwoordt een andere vraag dan de vraag die u wilde stellen. In deze handleiding wordt ervan uitgegaan dat u al weet hoe u een compose-bestand op een VPS schrijft. De handleiding gaat verder op het moment waarop de stack in de verkeerde volgorde wordt gestart.
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: 30sDe waarde test heeft twee bruikbare vormen. Een lijst die begint met CMD voert de opdracht rechtstreeks uit, zonder shell. Pipes, && en variabelexpansie werken dan niet. Een lijst die begint met CMD-SHELL geeft de rest als één tekenreeks door aan /bin/sh -c in de container. Dat is nodig wanneer de controle shellsyntaxis gebruikt. Een gewone tekenreeks wordt behandeld als CMD-SHELL. Een lijst die exact uit ["NONE"] bestaat, verwijdert een healthcheck die via het Dockerfile in de image is opgenomen.
De controle wordt in de container uitgevoerd. Daarom moet elk binair bestand waarnaar de opdracht verwijst in die image aanwezig zijn. Controleer dit eerst. Een minimale image zonder curl zorgt anders voor een container die permanent de status ongezond heeft, terwijl de oorzaak nooit in het applicatielog verschijnt. Test dit handmatig:
docker compose exec api curl --versionBij een ontbrekend binair bestand krijgt u OCI runtime exec failed: exec: "curl": executable file not found in $PATH: unknown. Images op basis van Alpine bevatten meestal BusyBox wget. De controle wordt dan ["CMD", "wget", "-q", "-O", "-", "http://localhost:8080/healthz"].
Hoe interval, retries en start_period worden gecombineerd
Vijf instellingen bepalen de timing. De standaardwaarden komen uit Docker Engine, niet uit Compose.
interval: de tijd tussen twee controles nadat de container de startperiode heeft doorlopen. Standaard 30s.timeout: hoelang één uitvoering van de controle mag duren voordat Docker deze beëindigt en die uitvoering als fout telt. Standaard 30s.retries: het aantal opeenvolgende fouten dat nodig is voordat de status verandert inunhealthy. Standaard 3.start_period: een respijtperiode nadat de container is gestart. Standaard 0s.start_interval: hoe vaak de controle tijdens de startperiode wordt uitgevoerd. Standaard 5s; hiervoor is Docker Engine 25.0 of nieuwer vereist.
De belangrijkste regel is: tijdens de startperiode telt een mislukte controle niet mee voor retries en blijft de container in starting. Zodra de controle voor het eerst slaagt, wordt de container healthy en eindigt de startperiode direct, ook als het grootste deel van de periode nog niet is verstreken. Als de startperiode eindigt terwijl de controle nog steeds mislukt, begint de normale aftelling. De container heeft dan retries opeenvolgende fouten nodig voordat deze als unhealthy wordt gemarkeerd.
De maximale tijd vanaf het starten van de container tot unhealthy is dus start_period plus retries vermenigvuldigd met interval plus timeout. Met de waarden in het bovenstaande bestand is dat 30 plus 5 maal 13, dus 95 seconden. Noteer dat getal voordat u een deploy-time-out instelt. Een rollout die na 60 seconden wordt afgebroken, zal deze container nooit een definitieve status zien bereiken.
De meest gemaakte fout is om retries te verhogen voor een trage start. Dat werkt één keer en veroorzaakt daarna blijvende nadelen: een service die 8 retries nodig had om te starten, accepteert in productie nu 8 opeenvolgende fouten voordat iemand dit merkt. Gebruik in plaats daarvan start_period, omdat dit alleen vóór de eerste geslaagde controle wordt toegepast.
Waarom depends_on op zichzelf niets garandeert
De korte vorm van depends_on veroorzaakt de meeste verwarring.
api:
depends_on:
- dbDit betekent slechts één ding: start de container db vóór de container api. Compose wacht totdat de container is gemaakt en gestart. Het wacht niet totdat PostgreSQL de eerste initialisatie heeft voltooid en ook niet totdat poort 5432 verbindingen accepteert. Uw app start ongeveer een seconde later, probeert verbinding te maken met een poort waarop nog niets luistert en wordt afgesloten. In het log ziet u Connection refused of FATAL: the database system is starting up wanneer de server actief is maar nog bezig is met herstel.
De lange vorm is wat men meestal nodig heeft:
api:
depends_on:
db:
condition: service_healthy
restart: true
migrate:
condition: service_completed_successfullycondition heeft drie waarden. service_started is hetzelfde als de korte vorm. service_healthy houdt de afhankelijke service tegen totdat de dependency de status healthy meldt. Dit is alleen zinvol wanneer die dependency een healthcheck definieert, in het compose-bestand of in de image. service_completed_successfully wacht totdat een container die eenmalig wordt uitgevoerd, zoals een databasemigratie, wordt afgesloten met status 0.
Naast condition staan nog twee velden. restart: true geeft Compose opdracht deze service opnieuw te starten nadat de dependency-service is bijgewerkt. required: false verandert een ontbrekende dependency van een fout in een waarschuwing.
Hier is de beperking die vaak voor problemen zorgt. Deze voorwaarden worden geëvalueerd wanneer de stack wordt gestart. Ze bepalen de startvolgorde en zijn geen regel voor toezicht. Als de database om drie uur 's nachts opnieuw wordt gestart, worden service_healthy en de rest niet opnieuw geëvalueerd. Uw app wordt ook niet opnieuw gestart om opnieuw aan deze voorwaarde te voldoen. Uw applicatiecode moet zelf opnieuw verbinding maken. docker compose up --no-deps api schakelt dit hele mechanisme bewust over en hetzelfde geldt voor het rechtstreeks starten van een container met docker start.
Schrijf een controle die gereedheid test, niet alleen of een proces bestaat
Een controle zoals pgrep nginx bewijst dat er een vermelding in de procestabel bestaat. Dit zegt niets over de vraag of de service een verzoek kan beantwoorden. Een webtoepassing kan de listening socket openhouden lang nadat de databasepool is uitgevallen. De procescontrole blijft dan gedurende de volledige storing groen.
Laat de container het werk uitvoeren waarvoor deze bestaat:
- Vraag bij een HTTP-service een echt endpoint op.
curl -fsSeindigt met een foutstatus bij elke status van 400 of hoger vanwege-f. Een 500-status van een defecte toepassing resulteert dus in een mislukte controle. - Gebruik voor PostgreSQL
pg_isready. Deze opdracht eindigt met status 0 wanneer de server verbindingen accepteert, met status 1 wanneer de server verbindingen weigert, met status 2 wanneer de server helemaal niet reageert en met status 3 wanneer de doorgegeven parameters onjuist zijn. - Gebruik voor Redis
redis-cli ping. Deze opdracht schrijftPONGnaar de uitvoer en eindigt met status 0. - De officiële MariaDB-image bevat een script met de naam
healthcheck.sh.healthcheck.sh --connect --innodb_initializedis de vorm die de beheerders ervan documenteren.
Bij pg_isready moet u rekening houden met een valkuil. Bij de eerste start met een lege gegevensdirectory voert de officiële postgres-image de initialisatie uit tegen een tijdelijke server die alleen op de Unix-socket luistert. pg_isready zonder hostargument gebruikt die socket. De opdracht kan dan "accepting connections" beantwoorden terwijl TCP-poort 5432 voor uw toepassing nog gesloten is. Richt de controle expliciet op TCP om dit probleem te voorkomen. De tijdelijke server beantwoordt daar namelijk geen verzoeken.
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: 30sDe dubbele dollartekens zijn geen typefout. Compose breidt $VAR zelf uit tijdens het inlezen van het bestand. Daardoor zou een waarde uit de hostomgeving in de controle worden opgenomen. $$ ontsnapt dit naar één $, zodat de shell in de container de variabele uitbreidt op basis van de eigen omgeving van de container.
Een postgres- en app-stack die in de juiste volgorde start
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:Start de stack en controleer hoe de statussen veranderen:
docker compose up -d
docker compose psDe kolom STATUS bevat de gezondheidsstatus tussen vierkante haken. Bij een gezond paar staat op beide rijen Up 41 seconds (healthy). Terwijl de database nog wordt geïnitialiseerd, heeft db de waarde Up 4 seconds (health: starting) en ontbreekt api in de lijst, omdat Compose deze nog niet heeft aangemaakt.
Lees het gezondheidslogboek om te zien waarom een controle is geslaagd of mislukt:
docker inspect --format '{{json .State.Health}}' "$(docker compose ps -q db)"Docker bewaart de laatste resultaten. Elk resultaat bevat een starttijd, een eindtijd, een ExitCode en de Output van de opdracht. De opgeslagen uitvoer wordt afgekapt. Een controle die een grote paginainhoud afdrukt, levert daardoor een onbruikbare logboekvermelding op. Houd controles stil.
Wat Docker doet wanneer een container ongezond wordt
Niets. Dit is het antwoord dat mensen het meest verrast.
Docker Engine op één host start een ongezonde container niet opnieuw. Het beleid restart: unless-stopped reageert wanneer het hoofdproces wordt beëindigd. Een ongezonde container is niet beëindigd. De container kan een week op unhealthy blijven staan terwijl Compose niets doet. In de Swarm-modus worden ongezonde taken vervangen. Een gewone Compose-stack op één server doet dat niet.
Daarbij blijven twee duidelijke opties over. Laat het proces beëindigen wanneer het weet dat het defect is. Dan kan het restartbeleid ingrijpen. Of controleer de status van buitenaf en genereer hiervoor een melding. Als u een Uptime Kuma-monitor richt op hetzelfde endpoint dat uw healthcheck aanroept, wordt een defecte afhankelijkheid op beide plaatsen zichtbaar. U krijgt dan een melding van de monitor in plaats van van een gebruiker. Als het netwerkverkeer de app via een Traefik reverse proxy bereikt, moet u er rekening mee houden dat de eigen beoordeling van een backend door de proxy losstaat van de Docker-healthstatus. Het ene vervangt het andere dus niet.
Foutopsporing van een controle die nooit gezond wordt
Voer exact het commando zelf uit in dezelfde container en bekijk de afsluitcode:
docker compose exec api curl -fsS http://localhost:8080/healthz; echo "exit=$?"exit=0 hier terwijl de container nog steeds de status unhealthy meldt, betekent dat uw compose test afwijkt van wat u zojuist hebt getypt. Dit komt meestal doordat CMD is gebruikt waar shell-syntaxis nodig was.
Twee fouten verklaren het grootste deel van de overige gevallen. De eerste is de verkeerde poort. De healthcheck wordt in de container uitgevoerd en moet daarom de containerpoort gebruiken, nooit de gepubliceerde hostpoort. Met ports: - "8080:3000" luistert de applicatie op 3000. Een controle op http://localhost:8080 blijft dan altijd mislukken, terwijl de site in een browser gewoon werkt. De tweede is de verkeerde host. Binnen de controle verwijst localhost naar diezelfde container. Dat is correct om de container zelf te controleren, maar niet om een buurcontainer te controleren. Daarvoor hebt u de servicenaam nodig, bijvoorbeeld db.
Tot slot is er een geval dat afzonderlijke aandacht verdient: de healthcheck slaagt, terwijl gebruikers fouten zien. Dit gebeurt wanneer het endpoint een statische 200 retourneert zonder iets daadwerkelijk te controleren. Een readiness-endpoint dat nooit de database bevraagt, kan niet vaststellen dat de database niet beschikbaar is. Laat het endpoint één eenvoudige, echte query uitvoeren.
FAQ
Waarom maakt mijn app nog steeds geen verbinding als depends_on aangeeft dat de database gezond is?
Omdat condition: service_healthy eenmaal wordt geëvalueerd wanneer de stack wordt gestart. Daarna houdt het niets meer in de gaten. Als de databasecontainer later opnieuw wordt gestart, start Compose uw applicatie niet opnieuw om opnieuw aan de voorwaarde te voldoen. Uw applicatiecode heeft daarom eigen logica voor opnieuw verbinden en opnieuw proberen nodig. De voorwaarde heeft ook geen effect wanneer u één container start met docker start of met docker compose up --no-deps.
Heb ik een healthcheck nodig als de image er al een definieert?
Meestal niet. Een bestaande healthcheck overschrijven is vaak een stap achteruit, omdat de beheerder van de image weet wat gereedheid voor die software betekent. Voeg alleen een eigen healthcheck toe wanneer de controle van de image niet geschikt is voor uw configuratie, bijvoorbeeld wanneer deze een poort controleert die u hebt gewijzigd. Schakel een healthcheck van de image uit door test: ["NONE"] of disable: true in te stellen voor de service.
Moet de healthcheck curl of wget gebruiken?
Gebruik het programma dat al in de image aanwezig is en controleer dit met docker compose exec <service> curl --version voordat u erop vertrouwt. Veel op Debian gebaseerde images bevatten geen van beide. Op Alpine gebaseerde images bevatten BusyBox wget. Voeg niet alleen een pakket aan een image toe om een healthcheck uit te voeren als de software een eigen client levert, zoals pg_isready of redis-cli.
Wordt een ongezonde container automatisch opnieuw gestart?
Niet door Docker Engine op één host. Restart policies reageren op het stoppen van het proces, niet op de health-status. Een ongezonde container blijft daarom actief en defect totdat iets anders ingrijpt. Laat het proces stoppen wanneer het de fout detecteert, of gebruik een externe monitor die een alarm geeft op basis van de status.
Hoe lang moet start_period zijn?
Lang genoeg voor de langzaamste legitieme eerste start die u hebt gemeten, plus een marge. Meet dit met docker compose up op basis van een leeg volume, omdat de eerste start van een database veel langer duurt dan elke daaropvolgende start. Een te lange startperiode vertraagt alleen het eerste oordeel van unhealthy. Een te hoog aantal retries verzwakt de controle gedurende de hele levensduur van de container. Dat is de ernstigere fout.