SSD Nodes Learn Hosting plans →
Gidsen Matt ConnorDoor Matt Connor · Bijgewerkt 2026-08-07

Docker Compose healthcheck correct configureren

Ontdek hoe Docker Compose healthchecks evalueert en waarom depends_on niet volstaat. Leer hoe u betrouwbare readiness checks schrijft voor Postgres en uw eigen applicaties.

Wat een Docker Compose healthcheck daadwerkelijk doet

Een Docker Compose healthcheck is een commando dat Docker periodiek binnen de container uitvoert. Docker leest uw logs niet, controleert uw poort niet en inspecteert uw proceslijst niet. Het voert het commando uit, leest de exitcode en slaat één enkele status op voor de container: starting, healthy of unhealthy. Exitcode 0 betekent healthy. Elke andere exitcode betekent unhealthy, en exitcode 2 is gereserveerd door Docker; retourneer deze dus nooit opzettelijk.

Dat is het volledige mechanisme. Vrijwel elk probleem met een healthcheck is hetzelfde probleem: het commando dat u heeft geschreven beantwoordt een andere vraag dan u bedoelde. Deze handleiding gaat ervan uit dat u al weet hoe u een compose-bestand op een VPS schrijft, en gaat verder vanaf het punt waar de stack in de verkeerde volgorde start.

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: 30s

De waarde test kent twee nuttige vormen. Een lijst die begint met CMD voert het commando direct uit, zonder shell; pipes, && en variabele-expansie werken hierdoor niet. Een lijst die begint met CMD-SHELL geeft de rest als één string door aan /bin/sh -c binnen de container, wat u nodig heeft wanneer de check shell-syntaxis vereist. Een gewone string wordt behandeld als CMD-SHELL. Een lijst met exact ["NONE"] verwijdert een healthcheck die in de image was ingebakken via het Dockerfile.

De check draait binnen de container, dus elk binair bestand dat wordt aangeroepen moet in die image aanwezig zijn. Controleer dit als eerste, want een slim-image zonder curl resulteert in een container die permanent unhealthy is om een reden die nooit in de applicatielog verschijnt. Test dit handmatig:

docker compose exec api curl --version

Een ontbrekend binair bestand antwoordt met OCI runtime exec failed: exec: "curl": executable file not found in $PATH: unknown. Images gebaseerd op Alpine bevatten meestal BusyBox wget, waardoor de check verandert in ["CMD", "wget", "-q", "-O", "-", "http://localhost:8080/healthz"].

Hoe interval, retries en start_period samenwerken

Vijf instellingen bepalen de timing. De standaardwaarden hiervoor zijn afkomstig van Docker Engine, niet van Compose.

  • interval: de tijd tussen twee controles zodra de container de opstartperiode is gepasseerd. Standaard 30s.
  • timeout: hoe lang één uitvoering van de controle mag duren voordat Docker deze beëindigt en als mislukking telt. Standaard 30s.
  • retries: hoeveel opeenvolgende mislukkingen nodig zijn voordat de status verandert in unhealthy. Standaard 3.
  • start_period: een respijtperiode nadat de container is gestart. Standaard 0s.
  • start_interval: hoe vaak de controle wordt uitgevoerd tijdens de opstartperiode. Standaard 5s; vereist Docker Engine 25.0 of nieuwer.

De belangrijkste regel: tijdens de opstartperiode telt een mislukte controle niet mee voor retries en blijft de container in de status starting. Zodra de controle voor het eerst slaagt, krijgt de container de status healthy en eindigt de opstartperiode direct, zelfs als de tijd nog niet volledig is verstreken. Als de opstartperiode verstrijkt terwijl de controle nog steeds mislukt, begint de normale telling en heeft de container retries opeenvolgende mislukkingen 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 uit het bovenstaande bestand is dat 30 plus 5 keer 13, oftewel 95 seconden. Noteer dit getal voordat u een deploy timeout instelt, want een uitrol die na 60 seconden stopt, zal nooit zien dat deze container een definitieve status bereikt.

De veelgemaakte fout is het verhogen van retries om een trage start op te vangen. Dat werkt één keer, maar heeft daarna blijvende gevolgen: een service die 8 pogingen nodig had om op te starten, tolereert voortaan 8 opeenvolgende mislukkingen in productie voordat er actie wordt ondernomen. Gebruik in plaats daarvan start_period, omdat dit alleen van toepassing is vóór het eerste succes.

Waarom depends_on op zichzelf niets garandeert

De korte vorm van depends_on is de bron van de meeste verwarring.

  api:
    depends_on:
      - db

Dit betekent slechts één ding: start de db-container voordat de api-container wordt gestart. Compose wacht tot de container is aangemaakt en gestart. Het wacht niet tot PostgreSQL de eerste initialisatie heeft voltooid en het wacht niet tot poort 5432 verbindingen accepteert. Uw applicatie start ongeveer een seconde later, probeert verbinding te maken met een poort waarop nog niets luistert, en sluit af. In de log ziet u Connection refused, of FATAL: the database system is starting up wanneer de server wel draait maar nog bezig is met herstellen.

De lange vorm is wat gebruikers in de praktijk nodig hebben:

  api:
    depends_on:
      db:
        condition: service_healthy
        restart: true
      migrate:
        condition: service_completed_successfully

condition kent drie waarden. service_started is gelijk aan de korte vorm. service_healthy houdt de afhankelijke service tegen totdat de dependency de status healthy rapporteert; dit is alleen zinvol wanneer die dependency een healthcheck definieert, hetzij in het compose-bestand of in de image zelf. service_completed_successfully wacht tot een eenmalige container (one-shot), zoals een database-migratie, is afgesloten met status 0.

Naast condition bevinden zich twee extra velden. restart: true instrueert Compose om deze service opnieuw op te starten nadat de dependency-service is bijgewerkt. required: false verlaagt een ontbrekende dependency van een foutmelding naar een waarschuwing.

Nu de beperking waar veel gebruikers tegenaan lopen. Deze condities worden geëvalueerd wanneer de stack wordt opgestart. Het betreft hier de opstartvolgorde, geen supervisieregel. Als de database om drie uur 's nachts herstart, wordt service_healthy niet opnieuw geëvalueerd en wordt uw applicatie niet opnieuw opgestart om hieraan te voldoen. Uw applicatiecode moet nog steeds zelfstandig opnieuw verbinding maken. docker compose up --no-deps api omzeilt dit mechanisme volledig door het ontwerp, en hetzelfde geldt voor het direct starten van een container met docker start.

Schrijf een controle die de gereedheid test, in plaats van enkel het bestaan van een proces

Een controle zoals pgrep nginx bewijst enkel dat er een item in de procestabel bestaat. Het bewijst niets over het vermogen van de service om verzoeken te beantwoorden. Een webapplicatie kan zijn listening socket openhouden lang nadat de databasepool is uitgevallen, waardoor de procescontrole groen blijft gedurende de gehele storing.

Vraag de container om het werk te doen waarvoor deze is ontworpen:

  • Voor een HTTP-service, vraag een daadwerkelijk endpoint op. curl -fsS sluit af met een non-zero status bij elke statuscode van 400 of hoger vanwege -f; een 500-fout van een defecte applicatie resulteert dus in een mislukte controle.
  • Gebruik voor PostgreSQL pg_isready, dat afsluit met 0 wanneer de server verbindingen accepteert, met 1 wanneer deze verbindingen weigert, met 2 wanneer er helemaal geen antwoord komt, en met 3 wanneer de opgegeven parameters onjuist zijn.
  • Gebruik voor Redis redis-cli ping, dat PONG print en afsluit met 0.
  • Voor MariaDB levert de officiële image een healthcheck.sh-script mee, en healthcheck.sh --connect --innodb_initialized is de vorm die door de beheerders wordt gedocumenteerd.

pg_isready kent één valkuil die het vermelden waard is. Bij de allereerste start met een lege datamap voert de officiële postgres-image de initialisatie uit tegen een tijdelijke server die alleen op de Unix-socket luistert. pg_isready zonder host-argument gebruikt die socket, waardoor deze "accepteert verbindingen" kan antwoorden terwijl TCP-poort 5432 nog gesloten is voor uw applicatie. Wijs de controle expliciet naar TCP en het probleem is verholpen, omdat de tijdelijke server daar niet op antwoordt.

    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

De dubbele dollartekens zijn geen typefout. Compose breidt $VAR zelf uit tijdens het inlezen van het bestand, wat een waarde van uw host-omgeving in de controle zou bakken. $$ ontsnapt dit naar een enkel $, zodat de shell in de container het uitbreidt tegen de eigen omgeving van de container.

Een postgres- en applicatiestack 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 observeer hoe de statussen veranderen:

docker compose up -d
docker compose ps

De kolom STATUS toont de gezondheidsstatus tussen haakjes. Een gezond paar geeft Up 41 seconds (healthy) aan op beide regels. Terwijl de database nog initialiseert, geeft db de waarde Up 4 seconds (health: starting) weer en ontbreekt api in de lijst, omdat Compose deze nog niet heeft aangemaakt.

Lees het gezondheidslogboek om te achterhalen waarom een controle is geslaagd of mislukt:

docker inspect --format '{{json .State.Health}}' "$(docker compose ps -q db)"

Docker bewaart de laatste resultaten, elk met een starttijd, eindtijd, een ExitCode en de Output van het commando. De opgeslagen uitvoer is ingekort; een controle die een grote paginabody afdrukt, levert dus een onbruikbaar logbericht op. Houd controles beknopt.

Wat Docker doet wanneer een container de status unhealthy krijgt

Niets. Dit is het antwoord dat de meeste mensen verrast.

Docker Engine op een enkele host herstart een unhealthy container niet. Het restart: unless-stopped-beleid reageert op het beëindigen van het hoofdproces, en een unhealthy container is niet beëindigd. Deze kan een week lang op unhealthy blijven staan terwijl Compose de container ongemoeid laat. Swarm mode vervangt unhealthy taken wel, maar een standaard Compose-stack op één server doet dit niet.

Er blijven twee eerlijke opties over. Zorg dat het proces zichzelf beëindigt wanneer het detecteert dat het defect is, zodat het restart-beleid actie kan ondernemen. Of bewaak de status van buitenaf en stel een melding in. Door een Uptime Kuma monitor te richten op hetzelfde eindpunt dat uw healthcheck aanroept, wordt een defecte afhankelijkheid op beide plekken zichtbaar en wordt u door de monitor gewaarschuwd in plaats van door een gebruiker. Als verkeer de app bereikt via een Traefik reverse proxy, onthoud dan dat de eigen weergave van de proxy van een backend losstaat van de Docker-healthstatus; de een dekt de ander dus niet af.

Fouten opsporen in een check die nooit de status 'healthy' bereikt

Voer het exacte commando zelf uit in dezelfde container en controleer de exit-code:

docker compose exec api curl -fsS http://localhost:8080/healthz; echo "exit=$?"

exit=0 hier terwijl de container nog steeds als unhealthy wordt gerapporteerd, betekent dat uw compose test afwijkt van wat u zojuist heeft getypt; dit komt meestal doordat CMD werd gebruikt waar shell-syntaxis vereist was.

Twee fouten verklaren het merendeel van de overige gevallen. De eerste is de verkeerde poort. De healthcheck draait binnen de container, dus deze moet de containerpoort gebruiken, nooit de gepubliceerde hostpoort. Bij ports: - "8080:3000" luistert de applicatie op 3000, en een check op http://localhost:8080 zal altijd falen, terwijl de site in een browser prima werkt. De tweede is de verkeerde host. Binnen de check is localhost diezelfde container; dit is correct om zichzelf te controleren, maar onjuist voor het controleren van een buur-service, waar u de servicenaam nodig heeft, bijvoorbeeld db.

Eén laatste geval verdient aandacht: de healthcheck slaagt terwijl gebruikers fouten zien. Dit gebeurt wanneer het endpoint een statische 200 teruggeeft zonder daadwerkelijk iets te controleren. Een readiness-endpoint dat nooit de database bevraagt, kan u niet vertellen dat de database onbereikbaar is. Zorg ervoor dat het een eenvoudige, echte query uitvoert.

FAQ

Waarom kan mijn applicatie nog steeds geen verbinding maken terwijl depends_on aangeeft dat de database gezond is?

Omdat condition: service_healthy slechts eenmaal wordt geëvalueerd, namelijk wanneer de stack start. Het houdt daarna geen toezicht meer. Als de databasecontainer later herstart, herstart Compose uw applicatie niet om opnieuw aan de voorwaarde te voldoen. Uw applicatiecode moet daarom beschikken over eigen logica voor herverbinding en retries. De voorwaarde doet ook niets wanneer u een enkele 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. Het overschrijven ervan is vaak een stap achteruit, omdat de beheerder van de image weet wat 'gereedheid' betekent voor die specifieke software. Voeg alleen een eigen check toe als de check van de image niet geschikt is voor uw configuratie, bijvoorbeeld wanneer deze een poort controleert die u heeft verplaatst. Om een healthcheck van een image uit te schakelen, stelt u test: ["NONE"] of disable: true in op de service.

Moet de healthcheck curl of wget gebruiken?

Gebruik degene die al in de image aanwezig is en bevestig 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 geen pakket toe aan een image enkel om een healthcheck uit te voeren als de software zijn eigen client meelevert, zoals pg_isready of redis-cli.

Wordt een ongezonde container automatisch herstart?

Niet door de Docker Engine op een enkele host. Restart-policies reageren op het beëindigen van het proces, niet op de gezondheidsstatus. Een ongezonde container blijft dus draaien en defect totdat er actie wordt ondernomen. Zorg er ofwel voor dat het proces stopt wanneer het een fout detecteert, of gebruik een externe monitor die waarschuwt bij een ongezonde status.

Hoe lang moet start_period zijn?

Lang genoeg voor de traagste legitieme eerste start die u heeft gemeten, plus een marge. Meet de tijd met docker compose up bij een lege volume-map, aangezien de eerste start van een database veel trager is dan alle volgende starts. Een start_period die te lang is, vertraagt alleen het eerste unhealthy-oordeel. Te veel retries verzwakken de controle gedurende de gehele levensduur van de container, wat een ernstiger probleem vormt.

#docker-compose#healthcheck#depends-on#docker#reliability