# 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:

bash
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:

dockerfile
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
  CMD curl -f http://localhost:8080/health || exit 1

The 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:

bash
docker inspect --format='{{json .State.Health}}' <container_name> | jq

This 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:

bash
docker inspect --format='{{range .State.Health.Log}}[{{.End}}] {{.ExitCode}}: {{.Output}}{{println}}{{end}}' <container_name>

The output might look like:

bash
[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:

bash
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:

bash
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:

bash
docker exec <container_name> curl -v http://localhost:8080/health

If the endpoint is at a different path:

dockerfile
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
  CMD curl -f http://localhost:8080/api/health || exit 1

Cause 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:

dockerfile
HEALTHCHECK --interval=30s --timeout=5s --start-period=60s --retries=3 \
  CMD curl -f http://localhost:8080/health || exit 1

For slower applications, use 2-3 minutes:

bash
docker run --health-start-period=180s myimage:latest

Cause 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:

dockerfile
HEALTHCHECK --interval=30s --timeout=10s --retries=3 \
  CMD curl -f http://localhost:8080/health || exit 1

Cause 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:

bash
docker exec <container_name> netstat -tlnp
# or
docker exec <container_name> ss -tlnp

Update the health check to use the correct port:

dockerfile
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
  CMD curl -f http://localhost:3000/health || exit 1

Cause 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:

bash
docker exec <container_name> netstat -tlnp | grep <port>

If it binds to 0.0.0.0 or external IP, use that:

dockerfile
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
  CMD curl -f http://127.0.0.1:8080/health || exit 1

Cause 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:

bash
docker exec <container_name> curl -v http://localhost:8080/health

If 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:

dockerfile
# Accept any 2xx response
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
  CMD curl -f http://localhost:8080/health || exit 1

Fixing Health Checks at Runtime

You can override health checks without rebuilding the image:

bash
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:latest

For Docker Compose:

yaml
services:
  myapp:
    image: myimage:latest
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 60s

Verification Steps

After fixing the health check:

  1. 1.Check status changes:
  2. 2.```bash
  3. 3.watch -n 5 'docker inspect --format="{{.State.Health.Status}}" <container_name>'
  4. 4.`
  5. 5.Monitor health check log:
  6. 6.```bash
  7. 7.docker inspect --format='{{json .State.Health.Log}}' <container_name> | jq -r '.[] | "[\(.End)] \(.ExitCode)"'
  8. 8.`
  9. 9.Verify the container recovers:
  10. 10.```bash
  11. 11.docker ps --filter "name=<container_name>"
  12. 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 /health endpoint 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.