This error occurs when Docker's health check command takes longer to execute than the configured timeout duration, causing Docker to consider the check as failed. Common fixes include increasing the timeout value, optimizing the health check command, and addressing underlying performance issues.
The "Health check timed out" error in Docker indicates that the HEALTHCHECK command defined in your Dockerfile or docker-compose.yml did not complete within the allowed time. By default, Docker gives a health check 30 seconds to complete, and if the command hasn't returned an exit code by then, Docker treats it as a failure. This is different from the health check command returning an error - in a timeout situation, the command is still running when Docker decides to mark it as failed. After the configured number of consecutive timeouts (retries), Docker marks the container as "unhealthy." Timeouts commonly occur when: 1. The application inside the container is slow to respond due to high load or resource constraints 2. The health check command itself is slow (e.g., complex database queries) 3. Network latency between the health check and the target service 4. The container is severely resource-constrained (low memory, CPU throttling) 5. The default timeout (30s) is too short for your specific application
First, check the current health status and identify if timeouts are occurring:
# View container health status
docker ps
# Get detailed health check information
docker inspect --format='{{json .State.Health}}' <container_name> | jq
# View the last 5 health check results
docker inspect <container_name> | jq '.[0].State.Health.Log[-5:]'Look for health check entries where the output is empty or truncated, which indicates a timeout. You can also check the time difference between "Start" and "End" timestamps:
{
"Status": "unhealthy",
"FailingStreak": 3,
"Log": [
{
"Start": "2024-01-15T10:00:00.000Z",
"End": "2024-01-15T10:00:30.000Z",
"ExitCode": -1,
"Output": ""
}
]
}An ExitCode of -1 with empty output typically indicates a timeout.
The most direct fix is to increase the health check timeout to accommodate slower responses:
In Dockerfile:
HEALTHCHECK --interval=30s --timeout=60s --start-period=60s --retries=3 \
CMD curl -f http://localhost:8080/health || exit 1In docker-compose.yml:
services:
api:
image: my-api:latest
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
interval: 30s
timeout: 60s # Increased from default 30s
start_period: 60s
retries: 3Key parameters explained:
- timeout: Maximum time Docker waits for the health check to complete (default: 30s)
- interval: Time between health checks (default: 30s)
- start_period: Grace period during startup where failures don't count (default: 0s)
- retries: Consecutive failures before marking unhealthy (default: 3)
Recommended timeout values:
- Simple HTTP ping: 5-10 seconds
- Database connectivity check: 10-30 seconds
- Complex application check: 30-60 seconds
- Heavy initialization apps: 60-120 seconds
A slow health check command can cause timeouts. Optimize it for speed:
Use connection timeout flags:
# Add connect timeout to curl (5 second connection timeout)
HEALTHCHECK --timeout=30s CMD curl -f --connect-timeout 5 http://localhost:8080/health || exit 1
# Add max-time for total request timeout
HEALTHCHECK --timeout=30s CMD curl -f --connect-timeout 5 --max-time 10 http://localhost:8080/health || exit 1Use lightweight alternatives:
# wget is often faster than curl
HEALTHCHECK CMD wget --no-verbose --tries=1 --spider http://localhost:8080/health || exit 1
# TCP port check (fastest, no HTTP)
HEALTHCHECK CMD nc -z localhost 8080 || exit 1
# For bash environments
HEALTHCHECK CMD bash -c 'echo > /dev/tcp/localhost/8080' || exit 1Avoid slow health check patterns:
# BAD: Complex database query
HEALTHCHECK CMD psql -c "SELECT count(*) FROM large_table" || exit 1
# GOOD: Simple connection check
HEALTHCHECK CMD pg_isready -U postgres || exit 1
# BAD: Full application endpoint with heavy processing
HEALTHCHECK CMD curl http://localhost:8080/api/complex-status || exit 1
# GOOD: Dedicated lightweight health endpoint
HEALTHCHECK CMD curl -f http://localhost:8080/health || exit 1Measure how long your health check actually takes to complete:
# Time the health check command manually
docker exec <container_name> time curl -f http://localhost:8080/health
# Or use bash timing
docker exec <container_name> sh -c 'start=$(date +%s); curl -f http://localhost:8080/health; echo "Duration: $(($(date +%s) - start)) seconds"'Test under various conditions:
# During container startup
docker run -d --name test myimage && \
for i in {1..10}; do \
docker exec test curl -f http://localhost:8080/health 2>&1; \
sleep 5; \
done
# Under load
docker exec <container_name> sh -c 'while true; do curl -f http://localhost:8080/health; done'If the health check takes 25+ seconds under normal conditions, increase your timeout to at least 2x that value.
Resource-constrained containers may timeout during health checks:
# Check container resource usage
docker stats <container_name>
# Check container resource limits
docker inspect <container_name> --format='{{json .HostConfig.Memory}} {{json .HostConfig.CpuShares}}'If memory-constrained, increase limits:
services:
api:
image: my-api:latest
deploy:
resources:
limits:
memory: 512M
cpus: '0.5'
reservations:
memory: 256M
cpus: '0.25'Or using docker run:
docker run -d --memory=512m --cpus=0.5 myimageCheck for OOM issues:
docker inspect <container_name> | jq '.[0].State.OOMKilled'If your container is being OOM killed or is heavily swapping, health checks will be slow or timeout.
Design your health endpoint to respond quickly without heavy processing:
Node.js/Express example:
// Fast health endpoint - no DB calls, no external services
app.get('/health', (req, res) => {
res.status(200).send('OK');
});
// Separate readiness endpoint for deeper checks
app.get('/ready', async (req, res) => {
try {
await db.query('SELECT 1');
res.status(200).send('Ready');
} catch (e) {
res.status(503).send('Not Ready');
}
});Python/Flask example:
@app.route('/health')
def health():
# Immediate response, no blocking calls
return 'OK', 200
@app.route('/ready')
def ready():
# Deeper check for readiness
try:
db.session.execute('SELECT 1')
return 'Ready', 200
except Exception:
return 'Not Ready', 503Use the lightweight endpoint for health checks:
# Use fast /health, not slow /ready
HEALTHCHECK --timeout=10s CMD curl -f http://localhost:8080/health || exit 1This pattern separates:
- Liveness (/health): Is the process running? (fast)
- Readiness (/ready): Can the service handle requests? (can be slower)
For applications that take a long time to start, use the start_period parameter:
HEALTHCHECK --interval=30s --timeout=30s --start-period=120s --retries=3 \
CMD curl -f http://localhost:8080/health || exit 1In docker-compose.yml:
services:
api:
image: my-api:latest
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
interval: 30s
timeout: 30s
start_period: 120s # Wait 2 minutes before counting failures
retries: 3The start_period provides a grace period during which:
- Health checks still run
- Failures don't count toward the retry limit
- Once a check succeeds, the start period ends
- Container starts as "starting" status, not "unhealthy"
For Java/JVM applications (known for slow starts):
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/actuator/health"]
interval: 30s
timeout: 30s
start_period: 180s # 3 minutes for JVM warmup
retries: 5Docker Engine 25.0+ adds start_interval:
HEALTHCHECK --start-period=60s --start-interval=5s --interval=30s --timeout=10s \
CMD curl -f http://localhost:8080/health || exit 1The start_interval runs health checks more frequently during the start period, allowing faster transition to healthy state.
Understanding timeout vs interval behavior: The timeout value determines how long Docker waits for a single health check to complete. If the check takes longer than this, Docker terminates it and considers it failed. The interval starts counting after the previous check completes (or times out).
Formula for maximum time to unhealthy status:
Time to unhealthy = start_period + (retries * (interval + timeout))With defaults (30s interval, 30s timeout, 0s start_period, 3 retries):
Maximum time = 0 + (3 * (30 + 30)) = 180 seconds = 3 minutesHealth check in high-availability setups: In Docker Swarm or Kubernetes, health check timeouts trigger container replacement:
# Docker Swarm service with health-aware deployment
services:
api:
deploy:
replicas: 3
update_config:
failure_action: rollback
healthcheck:
test: curl -f http://localhost:8080/health
interval: 30s
timeout: 10s
retries: 3Debugging intermittent timeouts: If health checks timeout sporadically:
1. Check for garbage collection pauses (JVM):
docker logs <container_name> 2>&1 | grep -i "gc pause"2. Monitor system load during timeouts:
docker stats --no-stream <container_name>3. Check for network issues:
docker exec <container_name> ping -c 3 localhostOverride health check timeout at runtime: You can override health check settings without rebuilding the image:
# docker run override
docker run --health-cmd "curl -f http://localhost:8080/health" \
--health-interval=30s \
--health-timeout=60s \
--health-retries=3 \
--health-start-period=60s \
myimage
# docker-compose override (no need to change Dockerfile)
services:
api:
healthcheck:
test: curl -f http://localhost:8080/health
timeout: 60s # Override Dockerfile timeoutDisable health check for debugging:
docker run --health-cmd="true" myimage # Always healthy
docker run --no-healthcheck myimage # Disable entirelyHealth check timeout vs HTTP timeout: When using curl or wget, set the HTTP timeout lower than the Docker health check timeout:
# Docker timeout: 30s, curl max-time: 20s
# This ensures curl returns an error before Docker times out
HEALTHCHECK --timeout=30s CMD curl -f --max-time 20 http://localhost:8080/health || exit 1This gives you a proper error message instead of just a timeout with no output.
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