A deploy is not finished when the container starts
Sagar Tayde · · 6 min read
The easiest deploy status to build is the one that lies. The container process started, so the platform shows “Live”, and the customer opens their URL to a 502.
Short answer: a running container only proves that a process started and has not exited yet. A deploy is done when the service has stayed up through a crash-loop window and answered a request on the port it actually listens on. That second check, the deployment health check, is the one most platforms skip.
That gap, between a process exists and this service answers, is where most of the support load in a deploy platform comes from. Closing it is not hard. It is just work nobody does, because the optimistic version demos well.
What “running” actually proves
A container’s state tells you the supervisor started a process and the process has not exited yet. It does not tell you:
- whether the app bound a port, or bound the wrong one
- whether it finished booting, or is thirty seconds into loading a model
- whether it crashed and is in a restart loop that happens to be “up” at the instant you looked
- whether anything in front of it (the proxy, the certificate, DNS) can actually route to it
Each of those produces a healthy-looking container and a broken URL. A status that reports on the first while the customer experiences the fourth is not a status. It is a coin flip with good branding.
Running vs ready vs live
| State | What it proves | What it does not prove |
|---|---|---|
| Running | A process started and has not exited | That it listens anywhere, or will still be up in five seconds |
| Ready | The process answers a request on its port | That the public URL routes to it |
| Live | Real traffic reaches the new version through the domain | Nothing more is needed for a deploy verdict |
Kubernetes users will recognise the split: it is the difference between a container that is running and one that passes its readiness probe. Most simpler platforms collapse all three rows into one green tick.
The restart loop is the honest test
The restart loop deserves special attention, because it is the case where naive status reporting is not merely incomplete but actively backwards.
A container that crashes on boot and restarts every two seconds spends most of its life in the
running state. Poll it at a random moment and you will usually catch it running. The more broken
it is, the faster it crash-loops, and the more reliably it looks fine. Any check that samples state
once will report the worst service on the platform as healthy.
So the check cannot be a sample. It has to be a state that persists: has this service stayed up, and answered, for long enough that “it is up” is a claim about the service rather than about the moment you asked.
What a deployment health check should verify

Our rule is that a deploy counts as successful only when something outside the container has confirmed it. A complete verdict checks four things, in order:
- The build finished. Not started: finished, with the exit status read, not inferred.
- The container reached a running state and stayed there across a window long enough that a crash loop cannot hide inside it.
- The service answered on the port it actually listens on, at the health path if one is configured. This is the check that separates “a process exists” from “a service exists”.
- Routing resolves. The domain, the certificate and the proxy rule all point at the new container, not the previous one.
Steps 1 to 3 are cheap and belong inside the deploy itself. Step 4 depends on DNS and certificate issuance, which can lag the deploy by minutes, so treat it as a separate signal rather than a reason to hold the deploy open.
Why the previous version should keep serving
A verdict that waits for confirmation needs somewhere for traffic to go while it waits. Keep the old container serving until the new one passes its checks. A deploy that fails its health check then costs the customer nothing but a red line in their history, instead of an outage.
The portless exception, and why it matters
Not every service has a port. A background worker, a cron host and a static site all deploy through the same pipeline, and none of them answers HTTP on a port of its own.
The tempting shortcut is to run the port probe anyway and treat the failure as non-fatal. Do not. A check that is allowed to fail without consequence is not a check. It is a line in a log that teaches everyone to ignore it, and the day it starts failing for a real reason, it has already been trained out of everyone’s attention.
Skip the probe explicitly for service types that have no port, and resolve on the running state instead. The verdict stays honest because the check that was skipped was skipped on purpose, by type, in code you can read, not because it failed and nobody minded.
Saying “I don’t know”
The last piece is the one that takes nerve: when the platform cannot confirm a deploy, it has to say so, rather than rounding up to success.
“Deployed, but we could not confirm it is serving traffic” is an unsatisfying sentence to put in a product. It is also the truth, it is actionable, and it is far better than a green tick above a URL that returns 502. Customers forgive a platform that is honest about uncertainty. They do not forgive one that was confidently wrong, because they spent the afternoon debugging their own code on its say-so.
The general principle, past deploys: a status that cannot be wrong in a way you would notice is not telling you anything.
Frequently asked questions
Why does my app show 502 Bad Gateway when the container is running?
A 502 means the proxy reached your container but got no valid answer. The usual causes are an app
listening on a different port than the one the proxy targets, an app bound to 127.0.0.1 instead of
0.0.0.0, or an app that is still booting or stuck in a crash loop. A running container rules out
none of these.
What is the difference between a liveness check and a readiness check?
A liveness check asks whether the process is still alive and should be restarted if not. A readiness check asks whether it can serve traffic right now. A deploy verdict needs readiness: a process can be alive for minutes while it loads data, and sending it traffic in that time still fails.
How long should a deploy wait before calling a service healthy?
Long enough to cover your slowest normal boot plus a margin, so a crash loop cannot pass by luck. Most web apps answer within seconds; apps that load large models or run migrations at start may need a minute or more. Make the window configurable per service rather than global.
Do background workers need a health check?
They cannot be probed over HTTP because they have no port. Check that the process reached a running state and stayed there through the startup window. If the worker can write a heartbeat somewhere, alert on a missing heartbeat as a separate signal.
Further reading
- How health checks work on Voroa, including the startup timeout setting
- Deploy statuses explained: Building, Starting, Live and Failed
- Troubleshooting failed deploys
- Deploying a background worker, the portless case above
- Kubernetes documentation on liveness, readiness and startup probes
Updated October 2, 2026