If your container shows Status: unhealthy
, Docker's health check is failing. The container is still running, but something inside, usually your app, isn’t responding as expected.
This doesn’t always mean a crash. It just means Docker can’t verify the app is working. Here’s how to debug the issue and restore the container to a healthy state.
How Docker Health Status Works
Docker runs health checks separately from the container's lifecycle. Even if a container is running, Docker can still mark it as unhealthy
if the health check command fails.
A health check runs inside the container at set intervals. It typically hits an endpoint or runs a command to check if your app is alive and responding.
There are three possible health states:
starting
: The container is still in its startup period.healthy
: The last few health checks passed.unhealthy
: Multiple health checks failed.
The container's health status depends on the exit code from the command:
0
: Healthy1
: Unhealthy- Anything else: Inconclusive, Docker leaves the status unchanged.
Inspect Health Check Logs
1. Check the Health Logs
Start by inspecting the container’s health details. This helps you see exactly what Docker’s health check is doing and why it’s failing.
Use this command:
docker inspect --format "{{json .State.Health }}" container_name | jq
It returns structured output with:
- Current health status (
starting
,healthy
,unhealthy
) - Failing streak count
- A log of recent health checks (with timestamps, exit codes, and output)
If jq
isn’t installed, drop it for raw JSON:
docker inspect --format "{{json .State.Health }}" container_name
Look for ExitCode: 0
(success) or ExitCode: 1
(failure). Any output from the failed checks can help narrow down the issue.
Debug Health Check Commands
Test your health check command directly inside the container to isolate the issue:
docker exec -it container_name curl -f http://localhost:8080/health
echo $?
This approach lets you see exactly what your health check encounters. You might discover that the health endpoint returns unexpected status codes, takes too long to respond, or that required tools like curl
aren't available in your container.
For more complex health checks, examine the command that Docker runs. Look at your Dockerfile or docker-compose.yml
to understand the exact test being performed:
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost/api/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
Setup Health Checks in Docker and Docker Compose
Health checks help Docker understand if your containerized app is working, not just running. Here’s how to add them to your setup and make sure they reflect real application health.
Add a Basic Health Check in Your Dockerfile
Use the HEALTHCHECK
instruction to define how Docker monitors your container.
For a simple HTTP service:
HEALTHCHECK CMD curl --fail http://localhost:8080 || exit 1
This tells Docker to run that command inside the container. If curl
gets a successful response, Docker marks the container as healthy. If not, it sets the status to unhealthy
.
You can customize how often Docker runs the check and how sensitive it is:
HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
CMD curl --fail http://localhost:8080/health || exit 1
interval
: Time between checks (default: 30s)timeout
: Max time Docker waits for the check to finish (default: 30s)start-period
: Grace period after startup when failures are ignoredretries
: How many failures trigger anunhealthy
state
docker stop
and kill
and how each impacts container shutdown.Use Custom Health Check Scripts
If your app needs more than a single HTTP check, write a script:
COPY healthcheck.sh /usr/local/bin/
RUN chmod +x /usr/local/bin/healthcheck.sh
HEALTHCHECK CMD /usr/local/bin/healthcheck.sh
Example healthcheck.sh
:
#!/bin/bash
# Check Postgres
pg_isready -h localhost -p 5432 -U myuser || exit 1
# Check API
curl -f http://localhost:8080/api/health || exit 1
# Check Redis
curl -f http://redis:6379/ping || exit 1
exit 0
Keep the script fast and lightweight. Avoid expensive operations or long-running queries,health checks shouldn’t add overhead.
Write Checks That Reflect Real Application Health
A health check should confirm the app can serve requests, not just that the process exists.
Good examples:
- HTTP services:
curl -f http://localhost:8080/api/health
- Databases:
pg_isready -h localhost -p 5432 -U myuser
- Message brokers:
nc -z rabbitmq 5672
Avoid checks that only confirm the process is alive (e.g., checking PID files or running ps
). Your container can look “healthy” even if the app is broken.
Tune Your Health Check Parameters
Health checks are great, until they get too noisy or too slow. Most of that comes down to timing. Set the wrong values, and you’ll either miss real issues or end up restarting healthy containers.
Here’s how to get each setting right.
interval
: How Often to Run the Check
This controls how often Docker runs the health check.
- Set it too low, and your container spends half its time checking itself.
- Set it too high, and you won’t catch failures quickly.
Tip: 10–30 seconds is a good starting point. Go shorter for critical services that need fast detection.
timeout
: How Long to Wait for a Response
If your app takes a while to respond—especially under load—short timeouts can cause false alarms.
Tip: Match this to your app’s real response time. If most endpoints return in under a second, 2–5 seconds should be plenty.
retries
: How Many Fails Before Giving Up
Some apps stall occasionally—say, during GC or burst load. One failure doesn’t always mean something’s wrong.
Tip:
- Use higher retries (3–5) for apps that sometimes hiccup.
- Lower retries (1–2) are better if you want to fail fast and restart quickly.
start_period
: Grace Time After Startup
Some apps need a bit to get going—connecting to databases, loading config, etc. Without a grace period, the health check might fail before the app is even ready.
Tip: If your app takes 30 seconds to boot, set a start_period
of 30–60 seconds. It’ll save you from false starts and restart loops.
Health Checks in docker-compose
Docker Compose lets you define health checks alongside service dependencies. Here’s an example:
version: '3.8'
services:
web:
build: .
depends_on:
db:
condition: service_healthy
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
db:
image: postgres:13
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 5s
retries: 5
Use docker-compose ps
to see the health status of each container in your stack.
What to Do When a Container Turns Unhealthy
By default, Docker won’t restart a container just because it’s marked unhealthy
. That’s intentional, health checks are diagnostic, not reactive.
If you're running standalone containers, you’ll need to wire up your own restart logic. One common option is the autoheal container, which watches for unhealthy containers and restarts them.
Another approach: combine your health check with a manual process exit. That way, Docker’s built-in restart policies can kick in.
HEALTHCHECK --interval=5m --timeout=2m --start-period=45s \
CMD curl -f --retry 3 --max-time 5 http://localhost:8080/health || kill -s 15 1
This example uses curl
to check an endpoint. If it fails, the container’s main process (PID 1) is terminated, triggering a restart, assuming you’ve set a --restart
policy.
In orchestrated setups (like Docker Swarm or Kubernetesthe ), this is usually handled for you. The scheduler watches health states and automatically replaces failing containers as part of service management.
Quick Wins
Short on time? Here's how to get an unhealthy container back on track quickly.
1. Check What Failed
Inspect health logs:
docker inspect --format "{{json .State.Health }}" container_name | jq
Look for recent Output
, ExitCode
, and error messages.
2. Test the Health Check Inside the Container
Match the container’s environment:
docker exec -it container_name sh -c '<your health check command>'
This catches issues with missing ports, permissions, or dependencies.
3. Fix the Most Common Causes
- App crash: Logs show connection refused or stack traces
- Missing dependency: DB or API call fails inside the container
- Slow startup or load: Health check times out repeatedly
- Wrong health check config: Mismatched port or URL path
4. Adjust the Health Check Settings
Tweak the timing so Docker doesn’t panic too early:
HEALTHCHECK --interval=30s --timeout=10s --start-period=45s --retries=3 \
CMD curl -f http://localhost:8080/health || exit 1
5. Let Docker Restart It
To enable restarts:
- Use autoheal
- Or exit the process on failure to trigger the restart policy:
CMD curl -f http://localhost:8080/health || kill -s 15 1
Common Causes of Unhealthy Containers
The container's up, but health checks are failing. Here's what to look for and what the symptoms often tell you.
Crashing Applications
The most straightforward case: the app inside the container has crashed. Health checks that hit a port or check a process will fail because nothing is listening anymore.
What you might see:
In the health log:
Output: curl: (7) Failed to connect to localhost port 8080: Connection refused
What to check:
- Run
docker logs <container>
to check for stack traces or crash messages. - Look for OOM kills (check
dmesg
if running locally, or container metrics if using a platform). - If it’s a language runtime crash (e.g., Node.js, Python), you’ll usually find it in the logs.
Broken Dependencies
Sometimes the app is fine, but a dependency isn't. Maybe your database isn’t reachable. Maybe an API is timing out. From Docker’s point of view, the container looks alive. But your app can’t do its job.
What you might see:
Output: psql: could not connect to server: Connection refused
What to check:
- Is the dependent service running?
- Can you connect manually using
docker exec
+curl
orpsql
? - If the issue happens during boot, maybe the health check runs too early—add retries or a delay.
Resource Exhaustion
The app hasn't crashed, but it’s hanging. Not enough memory, high CPU, or a full disk can make the health check time out.
What you might see:
- Empty
Output
with a long delay - Health check keeps failing with timeouts
What to check:
- Use
docker stats
to monitor memory and CPU usage in real-time. - Run
df -h
orfree -m
inside the container to check the disk and memory. - If the container is hitting limits, increase them or reduce the app load.
Misconfigured Health Checks
This one’s easy to miss. The app is working, but the health check is pointing to the wrong place.
What you might see:
Output: curl: (7) Failed to connect to localhost port 8080: Connection refused
when the app is running on port 3000.
What to check:
- Does the health check use the right port and path?
- Is the app listening on
localhost
,0.0.0.0
, or something else? - If it needs env vars to start up, make sure they’re available during the health check.

Monitor Health Events in Real Time
Docker lets you track health status changes as they happen:
docker events --filter event=health_status
This is useful when you're debugging flapping containers or trying to correlate startup issues with unhealthy states. But it's local, ephemeral, and doesn’t scale beyond your terminal.
If you're running multiple containers in production, you need better visibility.
At Last9, we make it easy to monitor container health over time, alongside other application signals. Here's how we help:
- Track container health as a time series: Instead of combing through
docker events
Last9 stores health status transitions so you can graph unhealthy periods and correlate with CPU, memory, or downstream failures. - Set alerts on flapping health states: Detect containers that frequently toggle between healthy and unhealthy, and notify only when it matters—no noisy alerts.
- Correlate with traces and logs: Use our OpenTelemetry-native ingestion to link health check failures with slow spans, failed DB calls, or error logs, without needing to jump between tools.
- Visualize readiness across environments: Whether you're running containers locally, in staging, or across multiple clusters, Last9 shows you which services are consistently healthy (and which ones aren't).
Get started with us today or book sometime with us!
FAQs
Q: Why is my container marked as unhealthy when it's still running?
A: Docker treats the container’s State
(running/stopped) separately from its health check results. If your health check fails repeatedly, Docker marks it unhealthy
, even while the container stays up. Use docker inspect
on .State.Health
to see what’s failing.
Q: How do I see which health check command is breaking?
A: Check the full configuration with:
docker inspect container_name
Look for the Healthcheck
section to see the exact CMD
used. To test it manually, run it inside the container:
docker exec -it container_name sh -c '<healthcheck command>'
Q: Can I make Docker restart an unhealthy container automatically?
A: Not by default. You can either use tools like autoheal or modify your health check to force the container to exit, letting Docker's --restart
policy take over. For example:
HEALTHCHECK … CMD curl -f … || kill -s 15 1
If you're in Docker Swarm or Kubernetes, the scheduler handles unhealthy containers automatically.
Q: What’s the difference between a health check timing out and failing?
A:
- A timeout means the command didn’t complete within the defined timeout window.
- A failure means it finished but returned a non-zero exit code (usually
1
). Docker marks the check as failed in either case.
Q: How often should I run health checks?
A: It depends. A 30‑second interval
is a good default. If rapid failure detection is important, go lower—but be cautious about adding too much load. Adjust based on how quickly or slowly your application usually responds.
Q: My health check passes manually, but fails in Docker—why?
A: The context is different. Docker runs the command inside the container, not from your host. That means different network settings, file paths, and environment variables. Always test with:
docker exec container_name sh -c '<healthcheck command>'
Q: Should every container have a health check?
A: Focus on containers where serving requests or data matters—like web apps, databases, message queues, etc. Not every short-lived or job container needs a health check. But for infrastructure services, yes, it’s well worth it.
Q: How do I stop an unhealthy container automatically?
A: Docker won't shut it down on its own. You can either:
- Add logic to the health check itself to kill PID 1 on failure
- Run a small host-side script to restart all containers with
health=unhealthy
For example:
docker ps -q -f health=unhealthy | xargs -r docker restart
This avoids giving containers full Docker socket access