# Docker Container Unhealthy: Health Check Failed - Diagnosis and Fixes
Your container is running, but Docker marks it as "unhealthy". Applications might still work partially, but orchestration systems like Docker Swarm or Kubernetes will eventually replace it. Understanding why health checks fail is critical for maintaining reliable deployments.
When you run docker ps, you see something like:
CONTAINER ID STATUS
abc123def456 Up 2 hours (unhealthy)This means the container's health check is failing repeatedly. Let's diagnose why.
Understanding Health Checks
A health check is a command Docker runs inside your container at regular intervals. If the command returns exit code 0, the container is healthy. Any other exit code means unhealthy.
Health checks are defined in the Dockerfile or at runtime:
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD curl -f http://localhost:8080/health || exit 1The key parameters are: - interval: Time between health checks (default 30s) - timeout: Max time to wait for a response (default 30s) - start-period: Grace period for container startup (default 0s) - retries: Consecutive failures before marking unhealthy (default 3)
Diagnosing Unhealthy Containers
Check Health Check Status
Get detailed health information:
docker inspect --format='{{json .State.Health}}' <container_name> | jqThis shows: - Current status (healthy/unhealthy/starting) - Last 5 health check results - Output from failed checks - Exact error messages
View Health Check Logs
See what happened in recent health checks:
docker inspect --format='{{range .State.Health.Log}}[{{.End}}] {{.ExitCode}}: {{.Output}}{{println}}{{end}}' <container_name>The output might look like:
[2026-04-03T16:05:00Z] 1: curl: (7) Failed to connect to localhost port 8080: Connection refused
[2026-04-03T16:05:30Z] 1: curl: (7) Failed to connect to localhost port 8080: Connection refused
[2026-04-03T16:06:00Z] 0:This tells you exactly why the check failed.
Check Container Logs
The application itself might have relevant errors:
docker logs --tail 100 <container_name>Look for errors around the time health checks started failing.
Manually Run Health Check Command
Test the health check command inside the container:
docker exec <container_name> curl -f http://localhost:8080/health
echo $?If the exit code is not 0, the health check command itself is the problem.
Common Causes and Fixes
Cause 1: Health Check Endpoint Doesn't Exist
The most common issue—the endpoint your health check tries to reach doesn't exist or isn't configured.
Symptoms:
``
curl: (7) Failed to connect to localhost port 8080: Connection refused
Fix: Verify the endpoint exists:
docker exec <container_name> curl -v http://localhost:8080/healthIf the endpoint is at a different path:
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
CMD curl -f http://localhost:8080/api/health || exit 1Cause 2: Application Not Ready During Start Period
Applications need time to initialize. If health checks start too early, they'll fail.
Symptoms: - Container marked unhealthy shortly after starting - Later becomes healthy on its own - Logs show "starting up" messages
Fix: Increase the start period:
HEALTHCHECK --interval=30s --timeout=5s --start-period=60s --retries=3 \
CMD curl -f http://localhost:8080/health || exit 1For slower applications, use 2-3 minutes:
docker run --health-start-period=180s myimage:latestCause 3: Health Check Timeout Too Short
Complex health checks might need more time than the default 30 seconds.
Symptoms:
``
Health check exceeded timeout (30s)
Fix: Increase the timeout:
HEALTHCHECK --interval=30s --timeout=10s --retries=3 \
CMD curl -f http://localhost:8080/health || exit 1Cause 4: Missing Health Check Dependencies
The health check command might need tools not installed in the container.
Symptoms:
``
/bin/sh: curl: not found
Fix: Either install the tool or use an alternative:
```dockerfile # Option 1: Install curl RUN apk add --no-cache curl
# Option 2: Use wget instead HEALTHCHECK --interval=30s --timeout=5s --retries=3 \ CMD wget --no-verbose --tries=1 --spider http://localhost:8080/health || exit 1
# Option 3: Use a simple TCP check with nc HEALTHCHECK --interval=30s --timeout=5s --retries=3 \ CMD nc -z localhost 8080 || exit 1 ```
Cause 5: Wrong Port or Protocol
Health checks sometimes use the wrong port, especially in multi-service containers.
Symptoms:
``
curl: (7) Failed to connect to localhost port 80: Connection refused
Fix: Check what ports are actually listening:
docker exec <container_name> netstat -tlnp
# or
docker exec <container_name> ss -tlnpUpdate the health check to use the correct port:
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
CMD curl -f http://localhost:3000/health || exit 1Cause 6: Application Binding to Wrong Interface
The application might bind to an external interface instead of localhost.
Symptoms: - Health check fails for localhost - Application works when accessed externally
Fix: Check where the app is listening:
docker exec <container_name> netstat -tlnp | grep <port>If it binds to 0.0.0.0 or external IP, use that:
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
CMD curl -f http://127.0.0.1:8080/health || exit 1Cause 7: Health Check Returns Wrong Status Code
The endpoint might return 200 OK but curl interprets it differently, or returns a non-200 status.
Symptoms:
``
curl: (22) The requested URL returned error: 503
Fix: Check what the endpoint actually returns:
docker exec <container_name> curl -v http://localhost:8080/healthIf the endpoint returns 503 during startup, that's intentional—the app isn't ready. Increase start period.
If the endpoint returns something other than 200 when healthy, adjust the health check:
# Accept any 2xx response
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
CMD curl -f http://localhost:8080/health || exit 1Fixing Health Checks at Runtime
You can override health checks without rebuilding the image:
docker run -d \
--health-cmd="curl -f http://localhost:3000/health || exit 1" \
--health-interval=30s \
--health-timeout=10s \
--health-retries=3 \
--health-start-period=60s \
--name myapp \
myimage:latestFor Docker Compose:
services:
myapp:
image: myimage:latest
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 60sVerification Steps
After fixing the health check:
- 1.Check status changes:
- 2.```bash
- 3.watch -n 5 'docker inspect --format="{{.State.Health.Status}}" <container_name>'
- 4.
` - 5.Monitor health check log:
- 6.```bash
- 7.docker inspect --format='{{json .State.Health.Log}}' <container_name> | jq -r '.[] | "[\(.End)] \(.ExitCode)"'
- 8.
` - 9.Verify the container recovers:
- 10.```bash
- 11.docker ps --filter "name=<container_name>"
- 12.
`
You should see the status change from "unhealthy" to "healthy" after the retries succeed.
Best Practices for Health Checks
- Keep health checks simple - They should test if the app can serve requests, not every dependency
- Use fast endpoints - A
/healthendpoint should respond in milliseconds - Don't check dependencies - If your database is down, the container should still be healthy (the app handles the error)
- Set appropriate start periods - Apps need time to initialize; don't set start period too short
- Log health check failures - Have your health endpoint log why it fails for debugging
- Include health checks in Dockerfile - Don't rely on runtime configuration
A properly configured health check keeps your containers running smoothly and helps orchestration systems make intelligent decisions about your services.