
A Docker Compose healthcheck runs a command inside a container and records whether the service passes that test. Add depends_on with condition: service_healthy when another service must wait for that result before starting. A process can be running while its application is still loading or unable to answer requests.
This guide is for developers running an API or a small application stack on a Linux host. It includes a complete HTTP example, a database dependency pattern, and a way to investigate failures. It also explains a limit that matters in production: an unhealthy status alone does not make ordinary Docker Compose restart a container.
Write the expected behavior before writing the command. For an API, that might be “the application can answer its readiness endpoint.” For a worker, it might be “the worker has initialized and can reach the queue it needs.” A green status is useful only when the test represents the work the service must do.
Keep the probe small and read-only. Avoid sending emails, creating database rows, running migrations, or calling a paid external API every few seconds. Separate a readiness test, which asks whether a service can accept work, from a liveness test, which asks whether it is still responsive. A temporary database outage should not automatically become a reason to restart every application connected to it.
Create a new directory containing the following three files. This is a local demonstration, not a production web server. Python is already present in the image, so the health check does not depend on installing curl or wget.
Save this as app.py. The /healthz route returns a small, predictable response. In a real application, connect your readiness route to the minimum initialization and dependencies needed to serve requests.
from http.server import BaseHTTPRequestHandler, HTTPServer
class Handler(BaseHTTPRequestHandler):
def do_GET(self):
ready = self.path == "/healthz"
body = b"ok" if ready else b"not found"
self.send_response(200 if ready else 404)
self.send_header("Content-Type", "text/plain")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
if __name__ == "__main__":
HTTPServer(("0.0.0.0", 8080), Handler).serve_forever()
Save this as check_health.py. It requires both HTTP 200 and the expected body. A login redirect, an error page, or a different successful response will fail the check. The connection has a two-second timeout, and the command returns exit code 0 for success or 1 for failure.
import http.client
import sys
connection = http.client.HTTPConnection("127.0.0.1", 8080, timeout=2)
try:
connection.request("GET", "/healthz")
response = connection.getresponse()
ok = response.status == 200 and response.read(64).strip() == b"ok"
print("ready" if ok else "readiness check failed")
except (OSError, http.client.HTTPException):
ok = False
print("readiness endpoint unavailable")
finally:
connection.close()
sys.exit(0 if ok else 1)
services:
api:
image: python:3.13-slim
working_dir: /app
command: ["python", "app.py"]
volumes:
- ./app.py:/app/app.py:ro
- ./check_health.py:/app/check_health.py:ro
ports:
- "127.0.0.1:8080:8080"
healthcheck:
test: ["CMD", "python", "/app/check_health.py"]
interval: 15s
timeout: 3s
retries: 3
start_period: 20s
start_interval: 2s
restart: unless-stopped
The published port is bound to the host's loopback address for local use. Inside the container, the probe connects to port 8080 on that same container. It does not use the host's published port. Other containers on the Compose network would reach this service as api:8080, following Docker's service discovery rules.
This example uses a readable image tag. For a repeatable production release, select and record a tested image digest, build your application into the image, and use a production server suited to your application. Do not expose this demonstration directly to the internet.
The values above are an example, not a universal schedule. Tune them against measured startup time and the cost of running the probe:
interval: 15s sets the normal pause between checks.timeout: 3s limits each healthcheck run.retries: 3 requires three consecutive counted failures before the container is marked unhealthy.start_period: 20s gives initialization failures a grace period. A successful probe during that period ends that protection for subsequent failures.start_interval: 2s checks more frequently during startup.start_interval needs Docker Compose 2.20.2 or later and Docker Engine 25.0 or later. Check both components. The Compose reference defines the configuration, while the Engine healthcheck reference explains the state transitions and timing. Probe failures continue to be evaluated after a container becomes unhealthy; a successful check can make it healthy again.
From the directory containing the files, run:
docker compose version
docker version
docker compose config --quiet
docker compose up --wait --wait-timeout 90
docker compose ps
config --quiet checks the Compose configuration. up --wait starts services in detached mode and waits for them to be running or, when they have healthchecks, healthy. The 90-second limit is a deployment wait budget, separate from the probe's timeout. A service without a healthcheck can satisfy this command simply by running. See the Compose up options.
After a successful start, request http://127.0.0.1:8080/healthz from the host. When you have finished with this disposable example, docker compose down removes its containers and default network. Run that command from the example directory so you target the correct project.
Starting a database process does not prove it is accepting connections. If an application needs PostgreSQL during initialization, add a check to the database service and make the application depend on its healthy state.
This is a fragment to merge into an existing Compose file with configured api and PostgreSQL db services. It is not a second standalone example. The database service must already define its image, credentials, storage, and the POSTGRES_USER and POSTGRES_DB environment variables.
services:
api:
depends_on:
db:
condition: service_healthy
db:
healthcheck:
test:
- CMD-SHELL
- 'pg_isready -h 127.0.0.1 -U "$$POSTGRES_USER" -d "$$POSTGRES_DB"'
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
CMD executes the listed program directly. CMD-SHELL uses the container's shell, which is needed here to expand the database variables. Compose's double-dollar escape preserves the dollar sign so expansion happens inside the container rather than on the host.
pg_isready checks the server's connection status. It does not verify that migrations have finished or that the application can execute its required queries. If migrations must complete before startup, model them separately; Compose supports the service_completed_successfully dependency condition for a job that must finish successfully.
Dependency conditions control startup ordering. They do not continuously stop the API if the database later becomes unhealthy. The application still needs connection retries and a sensible response when a dependency is unavailable.
First obtain the container ID, then replace CONTAINER_ID in the second command:
docker compose ps -q api
docker inspect --format "{{json .State.Health}}" CONTAINER_ID
docker compose exec api python /app/check_health.py
docker compose logs --tail 100 api
The health log and application log answer different questions. One shows what the probe reported; the other helps explain what the application was doing. Check the following causes before increasing timeouts:
No. With ordinary Docker Engine and Compose, restart: unless-stopped responds to the container stopping; it does not restart a running container merely because its health status changed. Docker's restart policy documentation describes the relevant exit behavior.
Also distinguish that service-level setting from restart: true nested under depends_on. The latter can restart a dependent service after an explicit Compose operation on its dependency. It is not continuous health-based recovery.
Decide what should happen after a failed check: alert an operator, remove an instance from traffic through a separately configured system, or use an orchestrator with an appropriate recovery policy. Restarting a service cannot repair missing credentials, a failed migration, or a database outage.
If you need host setup first, follow our guide to running Docker on a Compute with Hivenet VM. Once Docker is available, readiness checks remain part of your application configuration and operations.
A passing local probe does not validate public DNS, TLS, a reverse proxy, or a firewall. Test the user-facing route separately. Our guide to exposing a service from a VM covers the connectivity decisions that sit outside the container healthcheck.
Yes. A service's Compose healthcheck can override the image's settings. To intentionally disable an inherited check, use healthcheck: {disable: true}. First understand what monitoring or dependency behavior relies on it.
Docker exposes one healthcheck result per container. If several conditions are necessary, combine them in one bounded script and return failure when a required condition fails. Keep the output short enough to diagnose.
Only for the behavior the probe tested. Keep application tests, external monitoring, and recovery procedures alongside the healthcheck. A probe that checks one endpoint cannot establish that every user workflow is available.
Pick one AI, compute, or storage workload and see the difference for yourself. Spin it up in minutes, or let our team map your fastest path to production.