Health check mislukt, maar de app werkt
Een health check die tijdens een deployment mislukt terwijl de app lokaal prima werkt, komt meestal door een van vijf oorzaken. Controleer binding, pad, poort, timing en dependencies in de volgorde waarmee je het probleem het snelst vindt.
Er is een specifieke vorm van vastlopen waarbij een mislukte health check tijdens een deployment elke release blokkeert, terwijl de applicatie volgens elke controle die je kunt uitvoeren volledig in orde is. Lokaal werkt alles. Ook lokaal in Docker werkt alles. De logs laten zien dat de app luistert. En het platform meldt keer op keer een fout, soms twintig keer achter elkaar, zonder dat er ooit een request in je accesslog verschijnt.
Dat laatste detail is belangrijk en beperkt de mogelijke oorzaken meteen. Als je applicatie het request nooit heeft gelogd, heeft de check je applicatie niet bereikt — dus je vindt de verklaring niet in je applicatiecode.
Hier zijn de vijf oorzaken, in de volgorde waarmee je het probleem het snelst vindt.
1. Je bent gebonden aan localhost
Dit is veruit de meest voorkomende oorzaak en verklaart precies het symptoom dat er nooit verkeer aankomt.
In een container verwijst 127.0.0.1 naar de eigen loopback van deze container. Een health check die van buiten de container komt, kan die niet bereiken. Het proces luistert wel, je logs bevestigen dat, maar de socket is onbereikbaar vanaf elke plek die ertoe doet.
// Unreachable from outside the container
app.listen(3000, '127.0.0.1')
// Correct
app.listen(3000, '0.0.0.0')
Frameworks verschillen in hun defaults en sommige hebben de standaard tussen major versions gewijzigd. Controleer waarop je framework daadwerkelijk bindt, in plaats van af te gaan op wat je denkt dat het bindt.
# Confirm from inside the running container
dockup exec "ss -ltn || netstat -ltn" my-project/my-api
Als het luisteradres 127.0.0.1:3000 is in plaats van 0.0.0.0:3000, heb je de oorzaak gevonden en doen de andere punten op deze lijst er niet meer toe.
2. De poort die het platform controleert is niet de poort waarop je serveert
Er zijn twee poorten in het spel en die worden gemakkelijk door elkaar gehaald: de poort waarop je proces in de container luistert en de poort waarnaar het platform routeert. Als je app PORT uit de omgeving leest en je ergens in een Dockerfile 3000 hebt vastgelegd, kunnen die twee stilletjes van elkaar verschillen.
Het betrouwbare patroon is om het platform de poort te laten doorgeven:
const port = process.env.PORT || 3000
app.listen(port, '0.0.0.0')
Stel de poort van de service vervolgens één keer in op het platform en beheer het nummer niet langer op twee plekken.
3. Het pad retourneert iets anders dan een success-status
Een health check-pad wordt exact gematcht en verrassend veel fouten blijken redirects te zijn. Als je app /healthz doorstuurt naar /healthz/ of HTTPS afdwingt met een 301, mislukt een checker die alleen 2xx als succesvol beschouwt elke keer, terwijl een browser de redirect volgt en je een werkende pagina toont.
Drie specifieke valkuilen:
- Redirects door een afsluitende slash.
/healthz→/healthz/is een 301. - HTTPS afdwingen. De interne check komt meestal via plain HTTP op loopback binnen. Een onvoorwaardelijke HTTPS-redirect laat de check mislukken.
- Auth-middleware. Een globale authentication guard die vóór de routing draait, retourneert ook voor het health-pad een 401.
Sluit het health-pad expliciet uit van auth en van HTTPS-enforcement. Dit is de ene route die saai hoort te zijn.
4. De check is sneller dan je cold start
Als de check een paar keer mislukt en daarna slaagt, of tijdens een deploy mislukt maar bij opnieuw proberen wel slaagt, gaat het om timing en niet om configuratie.
Het budget dat je nodig hebt is niet één poging — het is interval × retries. Een applicatie die twaalf seconden nodig heeft om verbinding te maken met de database en een cache op te warmen, heeft een totaalbudget van meer dan twaalf seconden nodig. Anders mislukt elke release en zet je uiteindelijk de gate uit, waarmee je het enige uitschakelt dat een kapotte build nog van je gebruikers weghoudt.
dockup info my-project/my-api --json | grep -A6 healthCheck
Stel de timeout in op meer dan de traagste legitieme afzonderlijke poging en stel het aantal retries zo in dat interval × retries ruim boven je traagste legitieme boot uitkomt. Meet de boot in plaats van te gokken — de logs bevatten timestamps.
5. De applicatie is daadwerkelijk nog niet klaar
Dit is het laatste geval en precies waarvoor de check bestaat: je app is gestart, kon een dependency niet bereiken en probeert het opnieuw. De app is niet gecrasht, dus er wordt niets herstart. De app kan geen requests verwerken, dus de check mislukt. Het systeem werkt precies zoals bedoeld en geeft aan dat deze release geen verkeer hoort te ontvangen.
Je onderscheidt dit van de andere vier oorzaken doordat je applicatie het request heeft gelogd en met een non-2xx heeft beantwoord. Als het request in je logs verschijnt, zijn oorzaken 1 tot en met 3 uitgesloten.
De diagnostische volgorde die tijd bespaart
# 1. Did the request reach the app at all?
dockup logs my-project/my-api --follow
# 2. What is the process actually bound to?
dockup exec "ss -ltn || netstat -ltn" my-project/my-api
# 3. Does the path answer from inside the container?
dockup exec "curl -si localhost:3000/healthz" my-project/my-api
# 4. What is the gate configured to expect?
dockup info my-project/my-api --json
Stap 3 lost de meeste van deze problemen op. Met een curl vanuit de container verwijder je alle netwerkvariabelen tegelijk: als het daar een 200 retourneert en het platform nog steeds faalt, zit het probleem in het adres of de poort, niet in de app. Als het een 301 of 401 retourneert, heb je de oorzaak gevonden zonder ook maar iets aan het platform te wijzigen.
Waarom het de moeite waard is om de gate te behouden
Na de vierde mislukte deploy is het verleidelijk om de health check uit te schakelen en de release live te zetten. Het is goed om te bedenken wat je daarmee uitschakelt.
Op Dockup zorgt de health gate ervoor dat een kapotte release bij je gebruikers wordt weggehouden. De nieuwe versie wordt gebouwd en gestart terwijl de huidige versie verkeer blijft verwerken; het verkeer wordt pas omgezet zodra de nieuwe versie antwoord geeft. Schakel de gate uit en je activeert opnieuw het scenario waarin een container die wel start maar niet kan werken, een versie vervangt die nog prima functioneerde.
Een check die vier releases achter elkaar mislukt is vervelend. Een check die onvoorwaardelijk slaagt, is een check die de deploy die ertoe doet niet zal tegenhouden.
Veelgestelde vragen
Waarom mislukt de health check terwijl de app lokaal werkt?
Bijna altijd omdat de container aan 127.0.0.1 bindt in plaats van aan 0.0.0.0. Lokaal maak je verbinding via dezelfde loopback; van buiten de container is dat adres onbereikbaar.
Moet het health-endpoint authentication vereisen? Nee. Sluit het uit van globale auth-middleware, anders krijgt de checker een 401 en mislukt de deploy terwijl de app prima werkt.
Welke timeout moet ik gebruiken? Langer dan je traagste legitieme afzonderlijke poging, met genoeg retries voor je traagste legitieme cold start. Lees de bootduur uit je logs in plaats van te gokken.
Is het veilig om de health check uit te schakelen om een release te deblokkeren? Daarmee deblokkeer je de release, maar verwijder je ook de bescherming die voorkomt dat een kapotte versie verkeer overneemt. Los de check op — meestal ligt de oorzaak bij een bind-adres of een redirect en ben je binnen enkele minuten klaar.
