Deploy geslaagd, maar de site ligt plat
Je dashboard geeft aan dat de applicatie actief is, terwijl je gebruikers een foutmelding zien. Ontdek waarom een geslaagde deploy en de gezondheid van je applicatie verschillende signalen zijn, en hoe je ervoor zorgt dat een groene deploy betekent dat de app daadwerkelijk antwoord geeft.
Er is een specifiek soort slechte ochtend die begint met een groen vinkje. De deploy is geslaagd, maar de site ligt plat, het dashboard geeft aan dat de applicatie actief is en iemand stuurt je een screenshot van een 502.
Dit is geen zeldzame edge case. Het is het voorspelbare resultaat van een platform dat het ene rapporteert en iets anders meet. Het is belangrijk om precies te begrijpen waarom, want de oplossing is niet "beter controleren" — je moet veranderen wat het woord actief mag betekenen.
Drie verschillende vragen, één statuslampje
Wanneer een platform zegt dat een service actief is, kan het een van deze vragen beantwoorden:
- Is de container gestart? Het proces bestaat en is niet beëindigd.
- Is de poort geopend? Er luistert iets op de plek die het platform verwacht.
- Antwoordt de applicatie correct? Een request krijgt een response die betekent dat de app klaar is om te werken.
Dit zijn enorm verschillende garanties, en de meeste incidenten van dit type ontstaan doordat een dashboard vraag 1 beantwoordt terwijl jij aannam dat het om vraag 3 ging.
Een Node-proces dat opstart, geen verbinding met de database kan maken en in een retry-loop blijft zitten, voldoet voor altijd aan vraag 1. Het is niet gecrasht. Het zal nooit een request afhandelen. De container is in elke betekenis die de orchestrator belangrijk vindt "actief".
De kloof waar de storing ontstaat
Het gevaarlijke venster zit tussen "de nieuwe versie is gestart" en "de nieuwe versie kan werken". In dat venster heeft een naïef platform het verkeer al omgeleid, omdat starten het enige was dat het mat.
Wat dit erger maakt dan een gewone crash, is het rollback-verhaal. Een crash-loop maakt lawaai: de container stopt, start opnieuw, stopt weer en het platform merkt het uiteindelijk op. Een proces dat opstart en vervolgens blijft hangen, is stil. Er wordt niets opnieuw gestart, er gaat geen alarm af en de vorige werkende versie is meestal al verwijderd.
Dat laatste is de echte schade. De oude versie werkte prima. Die is verwijderd omdat een nieuwe versie was gestart, en starten werd aangezien voor werken.
Wat een echte health gate doet
De oplossing is structureel, niet procedureel. Het verkeer mag pas worden omgeleid nadat de nieuwe versie een request heeft beantwoord.
Op Dockup verloopt een release als volgt: de nieuwe versie wordt geïsoleerd gebouwd, naast de versie gestart die momenteel verkeer afhandelt en vervolgens wordt er een vraag gesteld. Pas wanneer de nieuwe versie antwoordt, wijst het domein ernaar. Als de versie nooit antwoordt, stopt de release daar en blijft de vorige versie verkeer afhandelen — niemand buiten je dashboard merkt ooit dat er een deploy is geprobeerd.
Daarom is een mislukte deploy op Dockup geen storing. De oude container is nooit verwijderd op basis van de aanname dat de nieuwe versie wel goed zou werken.
# The health gate is per-service configuration, not a platform default you inherit
dockup info my-project/my-api --json
Het healthCheck-blok in die output vormt het volledige contract: welk pad wordt aangeroepen, hoe lang er op een antwoord wordt gewacht, hoe vaak er wordt geprobeerd en hoe lang er tussen pogingen zit.
Configureer de check om vraag 3 te beantwoorden
Een health-endpoint dat zonder voorwaarden 200 retourneert, is erger dan helemaal geen endpoint, omdat het een echte gate verandert in een stempelautomaat. Het doel van de check is om te falen wanneer de applicatie haar werk niet kan doen.
Een bruikbaar readiness-endpoint controleert de zaken waar de app niet zonder kan werken:
// Not this — it proves only that the process is alive
app.get('/healthz', (req, res) => res.send('ok'))
// This — it proves the app can actually serve a request
app.get('/healthz', async (req, res) => {
try {
await db.query('select 1') // the dependency that is usually the problem
if (!cacheReady) throw new Error('cache warming')
res.status(200).json({ ok: true })
} catch (err) {
res.status(503).json({ ok: false, reason: err.message })
}
})
Twee regels zorgen ervoor dat dit in de praktijk werkt:
Controleer afhankelijkheden waar je niet zonder kunt werken, en niets anders. Als je app graceful kan degraderen wanneer de search index niet beschikbaar is, laat readiness dan niet afhangen van die search index — anders blokkeer je deploys vanwege iets wat geen storing is.
Houd het goedkoop. Het endpoint wordt tijdens elke release herhaaldelijk aangeroepen. Een readiness-check die een dure query uitvoert, veroorzaakt onnodige belasting.
Geef het genoeg tijd, maar geen onbeperkte tijd
Twee instellingen bepalen of de gate helpt of juist problemen veroorzaakt:
- Timeout per poging moet langer zijn dan je langzaamste legitieme cold start. Een app die in acht seconden verbinding maakt met een database en een cache opwarmt, zal elke keer falen op een check van drie seconden. Vervolgens "los" je het op door de gate uit te schakelen — en ben je weer terug bij af.
- Retries moeten de totale opstarttijd afdekken, niet één poging. Interval × retries is het daadwerkelijke budget.
Op Dockup zijn dit healthCheckInterval, healthCheckTimeout en healthCheckRetries. Ze worden per service ingesteld, omdat een Rails-monoliet en een Go-sidecar niet volgens hetzelfde schema starten.
Als de site al platligt
Als je dit tijdens een incident leest, is dit de volgorde waarmee je het snelst resultaat boekt:
- Controleer of de app rechtstreeks antwoordt, buiten het domein om. Als de app op de poort antwoordt maar niet via het domein, is dit een routingprobleem en geen applicatieprobleem. Stop dan met het debuggen van je code.
- Lees de runtime-logs, niet de build-logs. De build is geslaagd — dat is het uitgangspunt. Je wilt weten wat het proces deed nadat het was gestart.
- Voer eerst een rollback uit. Diagnose is goedkoper wanneer niemand toekijkt.
dockup logs my-project/my-api --follow # what the running process is saying
dockup deployments my-project/my-api # what was live before this
dockup rollback <deployment-id> my-project/my-api # put that back
Op Dockup is een rollback een omschakeling in plaats van een nieuwe build, omdat de vorige versie nog op schijf staat. Dat is om 3 uur 's nachts belangrijk: het snelste herstel is het herstel waarvoor niets hoeft te worden gecompileerd.
De vraag die je aan een platform moet stellen
Wanneer je kiest waar je production wilt draaien, is dit een goede test om bewust uit te voeren: deploy een applicatie die succesvol start en daarna geen verbinding met de database kan maken. Kijk wat het dashboard aangeeft.
Als het actief zegt, weet je nu precies wat dat woord tijdens je volgende incident waard zal zijn.
Veelgestelde vragen
Waarom geeft mijn dashboard aan dat de applicatie actief is terwijl de site platligt? Omdat "actief" meestal betekent dat het containerproces bestaat, niet dat de applicatie een request kan afhandelen. Een proces dat blijft proberen verbinding te maken met een database, voldoet voor onbepaalde tijd aan die definitie.
Moet een health check de database aanspreken? Ja, als je applicatie zonder de database geen requests kan afhandelen. Controleer de afhankelijkheden die je echt nodig hebt en laat de afhankelijkheden weg waarop je kunt degraderen.
Wat is het verschil tussen liveness en readiness? Liveness vraagt of het proces opnieuw moet worden gestart. Readiness vraagt of het proces verkeer mag ontvangen. De gate die dit probleem voorkomt, is readiness, en die moet worden uitgevoerd voordat het verkeer wordt omgeleid.
Hoe voorkom ik dat een slechte deploy de site überhaupt platlegt? Leid het verkeer pas om nadat de nieuwe versie een echt request heeft beantwoord en houd de vorige versie beschikbaar totdat de omschakeling is bevestigd. Dan is een mislukte release een release die nooit heeft plaatsgevonden, in plaats van een storing.
