Health Checks in Docker Compose: A Practical Guide

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.

Leave a Comment

Your email address will not be published. Required fields are marked *