How to Configure Docker Health Checks in Dockerfile and Docker Compose

Quick answer

A Docker health check runs a command inside a container on a schedule to see whether the application is actually working, rather than merely still running. Add a HEALTHCHECK instruction to the Dockerfile, or add a healthcheck section to the Compose service. The command needs to return exit code 0 when the container is healthy and a nonzero exit code when it isn’t.

For an HTTP service listening on port 8080, this is a practical Dockerfile configuration:

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

Make sure the image has curl installed, and that the application exposes a lightweight health endpoint.

What you’ll learn

  • How Docker decides a container’s health
  • Adding a health check to a Dockerfile
  • Configuring and overriding health checks in Docker Compose
  • Waiting for a dependency to become healthy
  • Inspecting health-check results and tracking down failures

Prerequisites

  • Docker Engine with the Docker Compose plugin
  • An existing Dockerfile or Compose project
  • A command inside the container that can test the application

A health check runs inside the container. A tool installed only on the Docker host won’t be available to the check.

How Docker health checks work

Without a health check, Docker can tell you that the container process is running. That’s not proof the application can accept requests. A web server might stay up while its worker pool, database connection, or internal initialization has failed.

Once a health check is configured, the container gets one of three health states:

StateMeaning
startingThe first checks are still running, or the start period has not finished.
healthyThe latest check finished successfully.
unhealthyThe configured number of checks failed in a row.

An unhealthy status does not restart a standalone Docker container on its own. An orchestrator, monitoring system, or some other automation has to handle restart behavior.

Step-by-step instructions

1. Create a useful application health endpoint

For an HTTP application, expose a small endpoint such as /health or /ready. It should return a successful status only once the service is ready to handle requests.

Don’t return large responses or run expensive diagnostics every time the check runs. Health checks can run several times a minute for each container replica.

2. Add HEALTHCHECK to the Dockerfile

Here’s an example that checks a service on port 8080 every 30 seconds:

FROM alpine:3.20

RUN apk add --no-cache curl

WORKDIR /app
COPY . .

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

CMD ["./start-server"]

Those timing options tell Docker when to run the command and how to judge the result:

OptionPurpose
--interval=30sRuns the check every 30 seconds.
--timeout=5sFails a check that takes more than five seconds.
--start-period=20sGives the application time to initialize before failures count toward unhealthy status.
--retries=3Marks the container unhealthy after three consecutive failures.

3. Configure a health check in Docker Compose

Compose can define a health check even if the image doesn’t include one:

services:
  api:
    build: .
    ports:
      - "8080:8080"
    healthcheck:
      test: ["CMD", "curl", "--fail", "--silent", "http://localhost:8080/health"]
      interval: 30s
      timeout: 5s
      retries: 3
      start_period: 20s

A Compose health check replaces the image’s Dockerfile health check for that service. To turn off a check inherited from an image, use this:

services:
  api:
    image: example/api:latest
    healthcheck:
      disable: true

4. Wait for a dependency to become healthy

Short-form depends_on sets startup order, but it doesn’t wait for a dependency to be ready. Use the service_healthy condition when an application needs to wait for a database health check:

services:
  db:
    image: postgres:16
    environment:
      POSTGRES_USER: appuser
      POSTGRES_PASSWORD: change-me
      POSTGRES_DB: appdb
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U appuser -d appdb"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 10s

  api:
    build: .
    depends_on:
      db:
        condition: service_healthy

This helps coordinate startup, though the application should still retry database connections. A dependency can become unavailable after startup.

5. Build and start the containers

docker compose up --build -d

Then check the status column:

docker compose ps

A working service should eventually show healthy. While the application initializes, it may briefly show health: starting.

CMD and CMD-SHELL

The exec form runs the executable directly:

test: ["CMD", "curl", "--fail", "http://localhost:8080/health"]

The shell form can use pipes, environment-variable expansion, and operators including ||:

test: ["CMD-SHELL", "curl --fail --silent http://localhost:8080/health || exit 1"]

Use CMD when shell features aren’t needed. It skips shell parsing and works with images that don’t contain a shell. Save CMD-SHELL for checks that genuinely need shell behavior.

Other Docker health check examples

PostgreSQL

healthcheck:
  test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
  interval: 10s
  timeout: 5s
  retries: 5

The doubled dollar signs tell Compose to pass the variables into the container instead of substituting them while it parses the Compose file.

MySQL

healthcheck:
  test: ["CMD-SHELL", "mysqladmin ping -h 127.0.0.1 -u root -p$${MYSQL_ROOT_PASSWORD} --silent"]
  interval: 10s
  timeout: 5s
  retries: 5

Be careful with credentials in command arguments. Process details and container configuration may expose them. For production environments, use a purpose-built script or protected configuration.

Common mistakes

  • Checking the published host port: The check runs inside the container, so use its internal port and, usually, localhost.
  • Using a missing utility: Minimal images often leave out curl, wget, and database clients.
  • Checking only whether a process exists: A process can be running and still unable to serve traffic.
  • Setting too short a timeout: Temporary load can cause false failures and unstable status changes.
  • Putting complex logic in Compose: Put long checks in a script, copy it into the image, then test that script on its own.
  • Assuming unhealthy means restarted: Docker records the status, but it won’t restart a standalone container solely because it is unhealthy.

Best practices

  • Keep the check fast, local, and deterministic.
  • Set a start period that reflects realistic application startup time.
  • Require multiple failures before marking a container unhealthy.
  • Test critical readiness without calling every external dependency.
  • Don’t log normal successful checks at an error level.
  • Use separate liveness and readiness behavior when deploying to an orchestrator that supports both.
  • Keep application-level retry logic even when Compose coordinates startup.

Verify and troubleshoot container health

Inspect the current status along with recent check output:

docker inspect --format='{{json .State.Health}}' container_name

For a quick status-only result, run:

docker inspect --format='{{.State.Health.Status}}' container_name

If the container is unhealthy, run the check manually from inside it:

docker exec -it container_name curl --fail --verbose http://localhost:8080/health

Check the application logs too:

docker logs --tail 100 container_name

Running the command manually will usually show whether you’re dealing with a missing executable, wrong port, DNS failure, authentication issue, slow response, or application error.

Leave a Comment

Related Posts