Docker Health Check: States, Commands, and Fixing Unhealthy
August 22, 2026


What Is a Docker Health Check?
A Docker health check is a command Docker runs on a schedule, inside a running container, to determine whether the application is actually working — not just whether the process is alive. A container can have PID 1 running fine while the app behind it hangs, deadlocks, or stops responding. Without a health check, Docker only knows the process exists; it has no idea whether that process is doing its job.
You define this behavior with the HEALTHCHECK instruction in a Dockerfile, or a healthcheck block in a Compose file. Either way, you're telling Docker: run this command (often curl or wget against a local endpoint, or a small script) at a fixed interval, and use its exit code to decide if the container is functioning. Exit code 0 means healthy; anything else counts as a failure. This lets Docker — and anything orchestrating it, like Compose or a scheduler — distinguish "running" from "actually serving traffic correctly," and react accordingly.
The Three Health States: starting, healthy, unhealthy
Once a HEALTHCHECK is defined, Docker tracks the container through a small state machine with three states.
starting is the grace period right after the container boots, since the app likely needs time to load config, connect to a database, or warm a cache. That window is controlled by start_period; checks that fail during it don't count toward marking the container unhealthy.
healthy means the most recent checks have passed according to the configured retries threshold. This is a live status, not a one-time badge — Docker keeps testing on the defined interval for the container's entire lifetime.
unhealthy means the check has failed consecutively enough times to exceed retries. This state isn't permanent: if a later check succeeds, Docker updates the status back to healthy automatically. There's no separate "recovered" state — the state machine just reflects the most recent evaluation window. For a deeper breakdown of exactly how these transitions are triggered, this explanation of the starting/healthy/unhealthy lifecycle is worth a look.
How to Check a Container's Health Status
You don't need to dig through logs to see current health — it's surfaced in a few standard places.
docker psshows a(healthy),(unhealthy), or(health: starting)tag next to the container's status — the fastest way to scan a host and spot problems.docker inspect --format '{{json .State.Health}}'gives the full picture: current status, failing streak count, and a log of recent check outputs with exit codes and timestamps. This is where you diagnose why something is failing, not just that it's failing.docker compose psshows the same health column for every service in a Compose stack, useful for figuring out which service is blocking the rest of a multi-container app from starting. This guide on unhealthy Docker status walks through reading thatinspectoutput in more detail.
Why Containers Get Stuck Unhealthy (and How to Fix It)
Most unhealthy statuses trace back to a small set of causes. Work through these before assuming something is deeply broken:
- Missing binary in a minimal image. Slim or distroless base images often don't ship
curlorwget, so the check command itself fails — not the app. Install a minimal HTTP client or switch to a shell-based/TCP check. - Wrong port or endpoint. The check hits a path or port the app isn't actually listening on, especially after a refactor. Verify the check target matches the real listener.
- Timeout shorter than actual check duration. If the endpoint occasionally responds slower than the configured timeout, Docker marks it a failure even though the app would have responded. Loosen the timeout to match realistic latency.
start_periodtoo short for slow-booting apps. JVM apps, database-backed services, or anything doing migrations on boot can take longer to become ready than the grace period allows, so early checks count against it.- Resource throttling or OOM kills. A container hitting its memory limit can be killed and restarted repeatedly, showing exit code 137 and cycling through unhealthy states — a symptom of a resourcing problem, not the check logic.
- Upstream dependency failures. If the health check calls an endpoint that itself depends on a database or downstream service, the container can look unhealthy for a failure that's actually elsewhere in the stack.
For copy-paste-ready HEALTHCHECK configurations across different stacks, see these Dockerfile health check recipes.
Docker Health Checks vs. Compose depends_on vs. Kubernetes Probes
It's easy to conflate Docker's own health check mechanism with Kubernetes probes, but they're separate systems solving overlapping problems differently. A Docker HEALTHCHECK is container-level and self-contained — Docker itself runs the command and tracks the state. Kubernetes, by contrast, has three distinct probe types: liveness (restart the pod if it fails), readiness (pull the pod out of service rotation if it fails, without restarting), and startup (delay the other two until the app is confirmed booted). Kubernetes probes don't read Docker's HEALTHCHECK state at all — they're configured independently in the pod spec and evaluated by the kubelet.
Where Docker's health status does get consumed directly is Compose. Setting depends_on with condition: service_healthy tells Compose to hold off starting a dependent service until the upstream container reports healthy, not merely running — critical for things like an app server waiting on a database to finish initializing. The official Compose services reference documents this condition and the rest of the depends_on behavior in full.
The Blind Spot: Health Checks Don't Know If Your Job Actually Ran
Here's the gap that trips up a lot of teams: a "healthy" container status only confirms that the process or HTTP endpoint responded successfully at the moment of the last check. It says nothing about whether a scheduled task, cron job, or batch process running inside that same container actually executed — let alone whether it finished or produced correct output. A container running a nightly export job can report healthy for days while that export silently fails every night, because the health check is watching the web server, not the cron task.
This is a structural limitation, not a configuration mistake — Docker health checks were built to answer "is this process responsive," a different question from "did this specific unit of scheduled work complete correctly." If your infrastructure depends on cron jobs, scheduled HTTP calls, or periodic batch tasks, you need visibility into execution history, failures, and recovery — separate from container-level health. For the full picture of where scheduling breaks down across distributed services, see this guide to distributed job scheduling, and for the deeper syntax reference alongside this exact gap, this health check syntax guide covers both.
That's precisely the layer Cronevra adds: it monitors whether your scheduled jobs actually ran and succeeded, and alerts you the moment one goes missing or fails — independent of whatever your container's health status says. If you're already relying on Docker health checks and want the missing piece, check Cronevra's pricing to see how it fits into your stack.
Frequently Asked Questions
What does it mean when Docker says a container is unhealthy?
It means the configured HEALTHCHECK command has failed consecutively enough times to exceed the retries threshold. The container process itself is typically still running — unhealthy reflects a failing check result, not a crashed process.
Does a Docker health check restart the container automatically?
No. A health check alone only updates the reported status; actual restart behavior comes from a separate restart policy or an orchestrator acting on that status, such as Compose or a monitoring script.
What's the difference between a Docker health check and a Kubernetes readiness probe?
A Docker health check is evaluated by the Docker engine itself and only updates the container's status label. A Kubernetes readiness probe is evaluated by the kubelet and actively removes the pod from service traffic when it fails — a distinct mechanism Docker's own health check doesn't perform.
How long does a container stay in the 'starting' health state?
It stays in "starting" for the duration set by start_period, a grace window during which failed checks don't count against the unhealthy threshold. Once that window ends, subsequent failures are counted normally.
Can I check a container's health status without stopping it?
Yes — docker ps shows a quick health tag, and docker inspect --format '{{json .State.Health}}' returns the full status and check history, both without affecting the running container.
Does Docker health check work for cron jobs running inside a container?
Not directly — a standard health check verifies a process or endpoint is responsive, not that an internal scheduled task executed or completed successfully. Monitoring cron job execution requires a separate layer, like Cronevra, that tracks whether each scheduled run actually happened and alerts on missed or failed executions.