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_periodfor expected initialization time. - Use
depends_onconditions 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.