A Docker container can be running without the application inside it being ready. A database may still be starting, an API may not be accepting connections, or a web service may have failed after its process launched.

A Docker Compose healthcheck gives Docker a repeatable test for service readiness. Compose can then use that health status when starting dependent services, instead of assuming that a running container is automatically ready.

This guide shows how to write healthchecks, use them with depends_on, inspect failures, and avoid the most common configuration mistakes.

What a Docker Compose healthcheck does

A healthcheck runs a command inside a container at a defined interval. Docker records the result as one of three states:

starting
healthy
unhealthy

The container can remain running while its health status is unhealthy. A healthcheck does not restart a container by itself and does not replace application monitoring. It is a readiness signal that other Docker tooling can inspect.

A basic Compose healthcheck looks like this:

services:
  web:
    image: nginx:alpine
    healthcheck:
      test: ["CMD-SHELL", "wget --no-verbose --tries=1 --spider http://localhost || exit 1"]
      interval: 30s
      timeout: 5s
      retries: 3
      start_period: 10s

The command must exit with status 0 for a successful check. Any non-zero exit status counts as a failure.

The healthcheck options

test

test defines the command Docker runs.

Use CMD when you want Docker to execute the program directly:

healthcheck:
  test: ["CMD", "pg_isready", "-U", "appuser", "-d", "appdb"]

Use CMD-SHELL when the check needs shell features such as ||, variables, pipes, or redirection:

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

The executable must exist in the image. A check using curl fails if the image does not contain curl.

To disable an inherited healthcheck:

healthcheck:
  disable: true

interval

interval controls how often Docker runs the check after the container starts.

interval: 30s

A shorter interval detects failures sooner but creates more command overhead. A longer interval reduces overhead but delays status changes.

timeout

timeout is the maximum time allowed for one check:

timeout: 5s

Set it long enough for a normal response, but keep it below the interval. A healthcheck that regularly reaches its timeout is usually exposing a slow service or an unsuitable probe.

retries

retries is the number of consecutive failures needed before Docker marks the container unhealthy:

retries: 3

One temporary failed request should not necessarily make a service unhealthy. Use retries to tolerate brief startup or network fluctuations.

start_period

start_period gives the service time to initialize:

start_period: 20s

Failures during this period do not count toward the retry limit in the same way as failures after startup. Use it for services that need migrations, cache warming, or database recovery before they can answer a probe.

HTTP healthcheck example

For an HTTP service, expose a lightweight endpoint that checks the service itself:

services:
  api:
    build: .
    ports:
      - "8080:8080"
    healthcheck:
      test: ["CMD-SHELL", "curl -fsS http://localhost:8080/healthz || exit 1"]
      interval: 10s
      timeout: 3s
      retries: 5
      start_period: 15s

A good health endpoint should be cheap and deterministic. It should not perform an expensive full business transaction on every probe.

If the image does not include curl, use a tool that is already available, add a small probe utility deliberately, or use the application’s own command-line client. Do not assume that common debugging tools exist in minimal images.

PostgreSQL healthcheck example

PostgreSQL images commonly include pg_isready, which is designed to check whether the server accepts connections:

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

This confirms PostgreSQL readiness, but it does not prove that every table or migration required by the application exists. If schema readiness matters, handle migrations explicitly rather than making the healthcheck perform destructive or slow work.

For a related persistence guide, see Postgres Docker Compose.

Using depends_on with a healthcheck

A basic depends_on controls startup order, but startup order is not the same as readiness. When the Compose implementation supports the long syntax, require the dependency to become healthy:

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

  db:
    image: postgres:16
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U appuser -d appdb"]
      interval: 5s
      timeout: 5s
      retries: 10

This tells Compose to wait for the database health condition before creating the dependent service according to the dependency relationship. The application should still retry connections because services can become unavailable after startup.

Do not treat condition: service_healthy as a substitute for resilient application code. A database restart, network interruption, or later health failure can still occur while the application is running.

Inspecting health status

See the current status for all services:

docker compose ps

Inspect one container’s health details:

docker inspect --format '{{json .State.Health}}' my-project-db-1

For readable logs from the service:

docker compose logs --tail 100 db

To follow logs while diagnosing startup:

docker compose logs -f --tail 100 db api

The health inspection includes recent probe output. That output often reveals a missing executable, a wrong port, invalid credentials, or a service that is still starting.

For more log filtering and timestamp options, see Docker Compose Logs.

Common healthcheck failures

The command is missing

exec: "curl": executable file not found

The image does not contain the command. Check the image contents or use a supported probe already installed in the image.

The wrong port is used

Inside a container, use the service’s container port and internal hostname. The host-side published port is not normally needed for a check running inside the same container.

For example, a service listening on container port 8080 should check localhost:8080, even if Compose publishes it as 8000:8080.

localhost points to the wrong service

A healthcheck runs inside the container where it is declared. localhost refers to that container, not another Compose service. To check another service, use its Compose service name over the internal network.

Credentials are not available

A database probe may fail because the username, database name, or password does not match the service configuration. Keep credentials out of committed healthcheck examples and verify environment variable expansion carefully.

The probe is too strict

A probe that requires an external dependency, a third-party API, or a complete application workflow can report false failures. Prefer a local readiness check that tests the service’s own ability to accept useful work.

The startup window is too short

If a service needs more time for initialization, increase start_period and review the logs. Do not hide a permanently failing service by setting very large timeouts or retry counts.

A practical multi-service example

services:
  api:
    build: ./api
    environment:
      DATABASE_URL: postgresql://appuser:change-me-locally@db:5432/appdb
    depends_on:
      db:
        condition: service_healthy
    healthcheck:
      test: ["CMD-SHELL", "curl -fsS http://localhost:8080/healthz || exit 1"]
      interval: 10s
      timeout: 3s
      retries: 5
      start_period: 20s

  db:
    image: postgres:16
    environment:
      POSTGRES_USER: appuser
      POSTGRES_PASSWORD: change-me-locally
      POSTGRES_DB: appdb
    volumes:
      - postgres_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U appuser -d appdb"]
      interval: 5s
      timeout: 5s
      retries: 10
      start_period: 10s

volumes:
  postgres_data:

Validate the Compose file before starting it:

docker compose config

Then start the services and inspect their health:

docker compose up -d
docker compose ps
docker compose logs --tail 100 db api

Healthcheck best practices

  • Check readiness, not merely whether a process exists.
  • Use a command available in the image.
  • Keep the probe fast and deterministic.
  • Set start_period for expected initialization time.
  • Use depends_on conditions where supported, but keep application retries.
  • Check the internal container port, not the published host port.
  • Keep credentials out of source control.
  • Inspect health output and service logs before changing retries.
  • Test the Compose file with docker compose config.
  • Do not use a healthcheck to perform migrations or destructive actions.

Frequently Asked Questions

Does a healthcheck restart an unhealthy container?

No. It reports the health state. Restart behavior must be configured separately, and an unhealthy status should be investigated rather than automatically hidden.

Does depends_on wait for a service to be ready?

Short syntax mainly expresses a dependency and startup order. A healthcheck with a supported service_healthy condition provides a readiness signal, but applications should still retry connections after startup.

Why is my healthcheck unhealthy when the container is running?

The process may be running while the application is not ready, the probe command may be missing, the port may be wrong, or the probe may use incorrect credentials. Inspect .State.Health and the service logs.

Should every Compose service have a healthcheck?

Not necessarily. Add one when readiness matters to dependent services, orchestration, monitoring, or deployment checks. Keep it meaningful rather than adding a superficial process check.

Summary

Docker Compose healthchecks make service readiness observable. Use a small local probe, give the service enough startup time, inspect failures with docker inspect and docker compose logs, and combine readiness checks with resilient application connection logic.

A healthcheck is a reliable signal when it measures the condition users and dependent services actually need—not just whether a container process happens to be running.