When orchestrating containers with Docker Compose, ensuring the health and readiness of your services is crucial for a resilient application. Docker provides a built-in health check mechanism to monitor container status and react accordingly. In this blog post, we’ll explore why health checks matter, how to define them in Docker Compose, and practical examples for real-world applications. We’ll also highlight a common pitfall that can cause misleading “unhealthy” container statuses and how to troubleshoot them.
Why Use Health Checks in Docker Compose?
By default, Docker considers a container “running” as long as the main process inside the container is active. However, a running process doesn’t always mean a healthy application. A service may be:
• Unresponsive due to an internal error.
• Waiting for dependencies (like a database).
• Experiencing startup delays before it’s ready to accept traffic.
Adding health checks ensures that dependent services only start once their dependencies are truly ready, improving stability in multi-container applications.
Defining a Health Check in Docker Compose
Docker Compose allows you to define health checks under the healthcheck
section within the services block.
Basic Syntax
services: my-service: image: my-image:latest healthcheck: test: ["CMD", "curl", "-f", "http://localhost:8080/health"] interval: 30s timeout: 10s retries: 3 start_period: 10s
Explanation of Parameters
• test: The command to run for checking health. Common options include:
• [“CMD”, “curl”, “-f”, “http://localhost:8080/health”] → Uses curl to check a web service.
• [“CMD-SHELL”, “pg_isready -U postgres”] → Used for PostgreSQL readiness.
• interval: How often the health check runs (e.g., every 30s).
• timeout: Maximum time a check can run before failing (10s in the example).
• retries: Number of failed attempts before marking the container as unhealthy.
• start_period: Grace period after container startup before running checks.
Practical Examples
1. Health Check for a Web API (Express/Node.js)
If your container runs a Node.js API, you can add a health check to confirm that it’s responding to HTTP requests:
services: web-api: image: my-web-api:latest healthcheck: test: ["CMD", "curl", "-f", "http://localhost:3000/health"] interval: 15s timeout: 5s retries: 5 start_period: 20s
Example Express.js health endpoint:
app.get('/health', (req, res) => { res.status(200).json({ status: 'healthy' }); });
2. Health Check for a PostgreSQL Database
For databases, a built-in utility can check readiness. PostgreSQL has pg_isready:
services: postgres: image: postgres:latest environment: POSTGRES_USER: user POSTGRES_PASSWORD: password healthcheck: test: ["CMD", "pg_isready", "-U", "user"] interval: 10s timeout: 5s retries: 5
3. Health Check for a Redis Service
To check if Redis is responsive:
services: redis: image: redis:latest healthcheck: test: ["CMD", "redis-cli", "ping"] interval: 10s timeout: 5s retries: 3
If the Redis service is healthy, the response will be PONG.
Common Pitfall: “Unhealthy” Containers Due to Missing Dependencies in Health Check
Now, for the real reason for this blog post — I wanted to highlight an issue that I’ve personally run into. One common issue developers face is troubleshooting an “unhealthy” container where the application is actually running fine, but Docker marks it as unhealthy, preventing dependent services from starting.
A classic example of this happens when using curl in a health check, but curl is not installed inside the container. In this case:
• The health check runs curl -f http://localhost:8080/health
• Docker expects a success response (exit code 0)
• Since curl is missing, the command fails with sh: 1: curl: not found
• Docker marks the container as unhealthy, but it provides no clear indication that curl is the issue.
How to Identify This Issue
If your container is marked as unhealthy, manually inspect the logs to confirm if the health check command is failing due to a missing tool:
1. Check container health status:
docker inspect --format='{{json .State.Health}}' container_name
This will show the health check logs, which may include error messages.
2. Run the health check command manually inside the container:
docker exec -it container_name sh
Then inside the shell:
curl -f http://localhost:8080/health
If you see an error like sh: 1: curl: not found, it means the container is missing curl.
How to Fix It
1. Ensure curl is installed: If your container uses an Alpine-based image, install curl in the Dockerfile:
RUN apk add --no-cache curl
For Debian-based images:
RUN apt-get update && apt-get install -y curl
2. Use an alternative health check method: If curl isn’t an option, use wget or nc (netcat):
healthcheck: test: ["CMD", "wget", "--spider", "-q", "http://localhost:8080/health"]
Or:
healthcheck: test: ["CMD", "nc", "-z", "localhost", "8080"]
3. Log health check failures: To debug further, modify your health check command to capture errors:
healthcheck: test: ["CMD-SHELL", "curl -f http://localhost:8080/health || echo 'Health check failed'"]
Using Health Status in Service Dependencies
When defining service dependencies, you can use depends_on with condition: service_healthy to ensure that a container starts only after its dependencies are healthy.
services: app: image: my-app:latest depends_on: postgres: condition: service_healthy redis: condition: service_healthy
Without condition: service_healthy, Docker Compose only waits for the dependent container to start but doesn’t check if it’s actually ready.
Checking Container Health
Once your services are running, you can inspect their health using:
docker ps
Example Output:
CONTAINER ID IMAGE STATUS abcdef123456 my-app:latest Up 5 minutes (healthy) 123456abcdef postgres:latest Up 5 minutes (unhealthy)
For more details:
docker inspect --format='{{json .State.Health}}' container_name
Conclusion
Adding health checks in Docker Compose ensures your services are actually ready before dependent containers start. However, it’s crucial to verify that the health check command itself is valid and the necessary tools exist inside the container.
If you run into an “unhealthy” container:
• Manually test the health check command inside the container.
• Check logs for errors.
• Ensure required utilities (like curl) are installed.
By avoiding this common pitfall, you’ll save hours of unnecessary debugging and make your containerized applications more robust.