This error indicates that Docker's HEALTHCHECK command could not verify your container's service is working correctly. The container is marked as 'unhealthy' and dependent services may fail to start. Fix this by debugging the health check command, adjusting timing parameters, or ensuring your application responds properly.
The "health check failed" error in Docker occurs when a container's HEALTHCHECK instruction cannot successfully verify that the service inside the container is operational. Docker runs the health check command periodically and if it fails consecutively (typically 3 times by default), the container is marked as "unhealthy." This error commonly appears in several scenarios: 1. When running `docker ps` and seeing "(unhealthy)" next to your container 2. When Docker Compose services fail to start due to `depends_on` with `condition: service_healthy` 3. In Docker Swarm or Kubernetes when containers are being replaced due to failed health checks 4. In CI/CD pipelines when health check timeouts cause deployment failures The health check itself is a command that Docker executes inside your container. If the command exits with code 0, the container is healthy. If it exits with code 1 (or any non-zero code), the container is unhealthy. The actual error could be anything from a network timeout to a missing tool to an application that hasn't started yet.
First, examine what Docker recorded from the failed health checks:
# View container status
docker ps -a
# Get detailed health check information including failure logs
docker inspect --format='{{json .State.Health}}' <container_name> | jq
# Alternative: view just the last 5 health check logs
docker inspect <container_name> | jq '.State.Health.Log[-5:]'This shows you:
- Status: "unhealthy", "healthy", or "starting"
- FailingStreak: Number of consecutive failures
- Log: Array of recent health check results with ExitCode and Output
Example output showing a failing health check:
{
"Status": "unhealthy",
"FailingStreak": 5,
"Log": [
{
"Start": "2024-01-15T10:00:00.000Z",
"End": "2024-01-15T10:00:05.000Z",
"ExitCode": 1,
"Output": "curl: (7) Failed to connect to localhost port 8080: Connection refused"
}
]
}The "Output" field often reveals exactly what went wrong.
Run the health check command yourself to see what's happening:
# Get a shell inside the container
docker exec -it <container_name> sh
# Run your health check command manually
curl -f http://localhost:8080/health
echo "Exit code: $?"
# Check if the service is listening on the expected port
netstat -tlnp || ss -tlnpIf you can't get an interactive shell:
# Run command directly
docker exec <container_name> curl -f http://localhost:8080/health
# Check what ports are listening
docker exec <container_name> netstat -tlnp 2>/dev/null || docker exec <container_name> ss -tlnpCommon issues you might discover:
- "curl: command not found" - curl isn't installed in the image
- "Connection refused" - service not running or wrong port
- "Connection timed out" - service too slow or blocked
- "404 Not Found" - health endpoint doesn't exist
- No ports listening - application failed to start
If your application needs time to initialize (loading data, warming caches, etc.), the health check may fail before it's ready. Add or increase the start_period:
In Dockerfile:
HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
CMD curl -f http://localhost:8080/health || exit 1In docker-compose.yml:
services:
myapp:
image: myapp:latest
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
interval: 30s
timeout: 10s
start_period: 60s
retries: 3Parameter guidelines:
- start_period: Grace period during which failures don't count (set to your app's typical startup time + buffer)
- interval: Time between checks (10-30s is typical)
- timeout: Maximum time for a single check (5-10s)
- retries: Number of consecutive failures before unhealthy (3-5)
For Java applications, databases, or apps loading large datasets, you may need start_period of 2-5 minutes.
Docker Compose health check syntax is a common source of errors. Here are the correct formats:
CMD form (recommended for simple commands):
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/health"]CMD-SHELL form (for commands with shell features like || or pipes):
healthcheck:
test: ["CMD-SHELL", "curl -f http://localhost:8080/health || exit 1"]String form (implicitly uses shell):
healthcheck:
test: curl -f http://localhost:8080/health || exit 1Common syntax mistakes:
# WRONG - arguments not separated
test: ["CMD", "/healthcheck.sh ready"]
# CORRECT - each argument is separate
test: ["CMD", "/healthcheck.sh", "ready"]
# WRONG - using || in CMD form (no shell to interpret it)
test: ["CMD", "curl", "-f", "http://localhost:8080/health", "||", "exit", "1"]
# CORRECT - use CMD-SHELL for shell operators
test: ["CMD-SHELL", "curl -f http://localhost:8080/health || exit 1"]Why `|| exit 1` matters: curl and other tools may return various exit codes (e.g., 7 for connection refused, 22 for HTTP errors). Docker only recognizes 0 (healthy) and 1 (unhealthy), so normalize exit codes with || exit 1.
Many minimal Docker images don't include curl or wget. Check what's available and install what you need:
Check available tools:
docker exec <container_name> which curl wget ncInstall curl in your Dockerfile:
# Debian/Ubuntu
RUN apt-get update && apt-get install -y --no-install-recommends curl \
&& rm -rf /var/lib/apt/lists/*
# Alpine
RUN apk add --no-cache curl
# RHEL/CentOS
RUN yum install -y curl && yum clean allAlternatives if you can't modify the image:
Use wget (often pre-installed):
HEALTHCHECK CMD wget --spider -q http://localhost:8080/health || exit 1Use netcat for TCP checks:
HEALTHCHECK CMD nc -z localhost 8080 || exit 1Use a built-in health endpoint (many apps have this):
# For apps with built-in health commands
HEALTHCHECK CMD /app/healthcheckUse /dev/tcp in bash:
HEALTHCHECK CMD bash -c 'echo > /dev/tcp/localhost/8080' || exit 1Health checks run inside the container, so the service must be accessible from localhost:
# Check what the app is listening on
docker exec <container_name> netstat -tlnp
# or
docker exec <container_name> ss -tlnpCommon binding issues:
1. App binds to 127.0.0.1 only - This is correct for health checks but may cause issues if you also need external access
2. App binds to a specific IP - Update health check to use that IP:
HEALTHCHECK CMD curl -f http://10.0.0.5:8080/health || exit 13. App uses a different port internally - Match the health check port:
healthcheck:
# Use the internal port, not the mapped port
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
ports:
- "8080:3000" # External:Internal4. App isn't running at all - Check container logs:
docker logs <container_name>For reliable health checks, implement a dedicated health endpoint in your application:
Node.js/Express:
app.get('/health', (req, res) => {
// Quick check - just confirm the server is responding
res.status(200).json({ status: 'ok' });
});
// Or with dependency checks
app.get('/health', async (req, res) => {
try {
await db.query('SELECT 1');
res.status(200).json({ status: 'ok', db: 'connected' });
} catch (err) {
res.status(503).json({ status: 'error', db: 'disconnected' });
}
});Python/Flask:
@app.route('/health')
def health():
return {'status': 'ok'}, 200Common database health checks:
# PostgreSQL
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $POSTGRES_USER -d $POSTGRES_DB"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
# MySQL
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-p$MYSQL_ROOT_PASSWORD"]
interval: 10s
timeout: 5s
retries: 5
# Redis
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 3
# MongoDB
healthcheck:
test: ["CMD", "mongosh", "--eval", "db.adminCommand('ping')"]
interval: 10s
timeout: 5s
retries: 3When using depends_on with health checks, ensure the dependency chain is correct:
services:
db:
image: postgres:15
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
app:
image: myapp:latest
depends_on:
db:
condition: service_healthy
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 60sIf services are stuck waiting for health:
1. Check the dependency's health status:
docker compose ps
docker inspect --format='{{json .State.Health}}' <service_name> | jq2. Start services without waiting:
# Temporarily disable health check waiting
docker compose up --no-deps app3. Increase timeouts if the dependency is slow to start:
depends_on:
db:
condition: service_healthy
restart: true # Restart if dependency becomes unhealthyDocker does NOT automatically restart unhealthy containers: Unlike Kubernetes, standalone Docker containers stay running even when unhealthy. The health status is informational only. To auto-restart unhealthy containers:
1. Use Docker Swarm - Swarm mode automatically replaces unhealthy containers:
docker service create --replicas 3 --health-cmd "curl -f http://localhost/health" myapp2. Use willfarrell/autoheal:
docker run -d --name autoheal --restart=always \
-e AUTOHEAL_CONTAINER_LABEL=all \
-v /var/run/docker.sock:/var/run/docker.sock \
willfarrell/autoheal3. Manual restart loop:
docker ps -q -f health=unhealthy | xargs -r docker restartHealth checks and resource constraints: If your container has memory limits and OOMs during health checks, the health check itself may be causing the problem. Use lightweight health endpoints that don't allocate much memory.
Alpine Linux gotchas: Alpine uses busybox which has limited shell features. Avoid bashisms in health check commands:
# May fail on Alpine
HEALTHCHECK CMD [[ -f /tmp/healthy ]]
# Works on Alpine
HEALTHCHECK CMD test -f /tmp/healthyDebugging timing issues: Add verbose output to narrow down timing problems:
HEALTHCHECK --interval=5s --timeout=3s --start-period=30s --retries=3 \
CMD curl -v http://localhost:8080/health 2>&1 | head -20 || exit 1Multiple HEALTHCHECK instructions: Only the last HEALTHCHECK in a Dockerfile takes effect. If you have multi-stage builds, add HEALTHCHECK in the final stage.
Graceful shutdown considerations: When a container is stopping, health checks may fail. This is normal - Docker sends SIGTERM and waits for the container to stop. Don't worry about health check failures during shutdown.
image operating system "linux" cannot be used on this platform
How to fix 'image operating system linux cannot be used on this platform' in Docker
manifest unknown: manifest unknown
How to fix 'manifest unknown' in Docker
cannot open '/etc/passwd': Permission denied
How to fix 'cannot open: Permission denied' in Docker
Error response from daemon: failed to create the ipvlan port
How to fix 'failed to create the ipvlan port' in Docker
toomanyrequests: Rate exceeded for anonymous users
How to fix 'Rate exceeded for anonymous users' in Docker Hub