A Docker HEALTHCHECK answers a single question: did the command you defined exit with code 0? Nothing more. It doesn't measure whether the app is "healthy" in any broad sense — it measures whether that specific command, at that specific moment, didn't fail.
The official Docker documentation says it plainly: the HEALTHCHECK instruction tells Docker how to test a container to check that it's "still working." The example the docs themselves give is telling:
HEALTHCHECK --interval=5m --timeout=3s \
CMD curl -f http://localhost/ || exit 1That curl -f http://localhost/ tests one thing: that the web server answers something on the root path within 3 seconds. It doesn't test whether the database is up, whether the message queue has live workers, or whether the authentication service the app needs to log users in is responding. If curl gets any response at all — even an error page with a misconfigured 200 status — the healthcheck passes.
The three states and what triggers each one
According to the docs, a container with a healthcheck defined has a health status in addition to its normal status. It starts as starting. Every time the check passes, it moves to healthy. After a certain number of consecutive failures, it moves to unhealthy.
The command's exit code is the only thing that drives that state machine:
0: healthy — the container is "healthy" according to that command1: unhealthy — the container isn't working correctly2: reserved, do not use
That's all Docker knows. It doesn't interpret the content of the response, it doesn't validate business logic, it doesn't check transitive dependencies. If your check command is curl -f http://localhost/health and that endpoint returns 200 OK with a hardcoded JSON that never changes, you'll have a container that's forever healthy even if the app's actual logic is broken.
Shallow liveness vs. real health
Here's the distinction that matters when deciding what to put in the healthcheck: a shallow "liveness" check only confirms that the process responds — that it's not hung, that it hasn't entered an infinite loop, that the port is listening. A real health check validates that the dependencies the app needs to serve traffic actually work: database connection, access to critical external services, queue status if the app depends on them.
The docs give the exact example of the problem a basic liveness check solves: "detecting cases such as a web server that is stuck in an infinite loop and unable to handle new connections, even though the server process is still running." That's exactly what it covers, no more, no less. If the process responds but can't write to the database because the connection dropped, a healthcheck that just curls / will never find out — the server keeps answering with 200 on that path while every real request to the app fails.
A HEALTHCHECK that only verifies the process responds gives a false sense of security if it doesn't validate the app's real dependencies. The healthy status in docker ps can coexist perfectly well with an app that can't complete a single useful operation.
What happens with start_period and retries
Two HEALTHCHECK options define how much patience Docker has before declaring unhealthy: --start-period and --retries. The docs explain that the start period gives initialization time for containers that need to bootstrap — if the check fails during that window, it doesn't count toward the maximum number of retries. But if the check passes once during the start period, the container is considered started, and from then on every consecutive failure does count.
This matters in a detail the docs don't highlight but that follows from the mechanics: if the app takes a while to come up and your start_period is short, you can end up counting as a "real failure" something that was actually "still starting up." The right tuning of start_period and retries depends on the app's actual bootstrap time — there's no universal value that works for every image.
What it doesn't measure, even if the healthcheck is well written
Not even the best CMD inside HEALTHCHECK solves this:
- External dependencies you're not explicitly checking. If the command doesn't touch the database, the healthcheck knows nothing about it.
- Partial degradation. A service that responds slowly but within the timeout passes the same as one that responds instantly. The exit code has no gradients.
- Business state. If the app returns 200 but with corrupted data or an empty response where there should be content, and your check only validates the HTTP code, it passes all the same.
- Anything happening outside the container. HEALTHCHECK runs inside the container, with whatever visibility it has from there. It doesn't see the state of the host, the external network, or other containers unless your command queries them directly.
Each of these points gets solved by adding logic to the healthcheck command — not by changing how Docker interprets the result. If you need the check to reflect the service's real health, the work is in writing an endpoint or a script that actually tests the dependencies that matter, not in tweaking intervals or retries.
Original source
Looking for this approach on your team?
Explore my technical case studies or discuss a senior role, architecture and technical leadership.
Related Articles
depends_on Isn't Enough: service_healthy in Compose
depends_on without a condition only waits for the container to exist, not for the app inside it to respond. Only with condition: service_healthy in Docker Compose do you get a real guarantee of ordered startup.
Sep 09 2026 · 6′ · Tutorials · docker · devops
Docker Says "Healthy" and the Pod Keeps Sending Broken Traffic
Docker's HEALTHCHECK is information, not a routing guarantee. If Kubernetes or Swarm isn't reading it, your container can be "healthy" and still keep taking requests that are going to fail.
Aug 28 2026 · 7′ · Tutorials · docker · devops
The Complete Guide to Docker HEALTHCHECK: Dockerfile vs Compose vs Orchestrator
Why a HEALTHCHECK copied from a tutorial usually lies more than having none at all, and how to decide between Dockerfile, Compose, and the orchestrator based on what you actually need to monitor.
Aug 04 2026 · 9′ · Tutorials · docker · devops
Comments (0)
What do you think of this?
Drop your comment in 10 seconds.
We only use your login to show your name and avatar. No spam.
No comments yet. Be the first — your take matters most when we're few.