Docker Compose depends_on: Wait for Healthy, Not Just Running
Why Docker Compose depends_on can start your app before its backend is ready, and how to gate startup with a meaningful healthcheck. Includes a reproducible two-service example.
Your API works when you launch it after the database. Then you run docker compose up from a clean start and the API immediately fails to connect—even though the Compose file already contains depends_on. The tempting fix is a longer sleep. That is only a guess about how long startup takes.
Short-form `depends_on` orders container startup; it does not mean a dependency is ready to serve requests. For an initial readiness gate, give the dependency a meaningful healthcheck and use depends_on: condition: service_healthy on the service that needs it. Docker’s startup-order guidance makes this distinction explicit.
This guide is for developers using current Docker Compose v2 with ordinary local containers. It shows a self-contained two-service experiment, then explains how to adapt the result to a real database or API. It does not promise that a healthcheck prevents every future connection failure.
Started and healthy are different checkpoints
A container can have a running process before the application inside it has loaded its configuration, completed migrations, bound its listening socket or established other dependencies. Compose knows a container has started; it cannot infer the application's own definition of ready.
These are the three documented conditions for the long form of depends_on. They answer different questions:
| Condition | What it waits for |
|---|---|
| service_started | The dependency has started. It need not be ready to accept a request. |
| service_healthy | The dependency’s configured healthcheck has passed. |
| service_completed_successfully | A one-shot dependency has exited successfully. |
The short form depends_on: [backend] is equivalent to ordering on service_started. service_healthy adds a wait for a health status defined on the backend service. service_completed_successfully is appropriate for a task that must finish, such as a controlled one-shot setup job—not for a long-running server. See the Compose service specification for the precise rules.
The distinction is easier to see as two flows. The upper flow permits the client to begin after the backend process starts. The lower flow has an extra readiness checkpoint before the client begins:
*Started-only ordering versus a service_healthy gate. The sequences are conceptual and not timed measurements.*
The drawing is a conceptual sequence, not a timing benchmark. The lower flow helps only when the healthcheck itself is a useful test of readiness.
A small, reproducible Compose example
Use a new, empty working folder rather than editing an existing project. You need Docker Engine or Docker Desktop with the Docker Compose v2 plugin, network access to pull an official Python container image, and permission to run containers. No local Python installation is required, and this example publishes no host ports, attaches no local folders and uses no credentials.
Save this as compose.yaml in that folder:
services:
backend:
image: python:3.13-alpine
command:
- sh
- -c
- "sleep 6; exec python -u -m http.server 8000 --bind 0.0.0.0 --directory /tmp"
healthcheck:
test:
- CMD
- python
- -c
- "from urllib.request import build_opener, ProxyHandler; build_opener(ProxyHandler({})).open('http://127.0.0.1:8000/', timeout=2).close()"
interval: 2s
timeout: 3s
retries: 10
start_period: 2s
client:
image: python:3.13-alpine
depends_on:
backend:
condition: service_healthy
command:
- python
- -c
- "from urllib.request import build_opener, ProxyHandler; response = build_opener(ProxyHandler({})).open('http://backend:8000/', timeout=5); print('HTTP status:', response.status); response.close()" The backend deliberately pauses before starting Python's basic HTTP server. This is a teaching delay, not a recommended fix for production startup. Once listening, the server responds to an HTTP request at its root path. Python describes this server as unsuitable for production in the `http.server` documentation.
The healthcheck uses Python, which the image already contains, rather than assuming curl is installed. It calls 127.0.0.1:8000 from within the backend container. The client uses backend:8000 because, on the default Compose network, containers can address each other by service name. Nothing here needs the browser-facing or host-published ports covered by Docker's Compose networking documentation.
The code disables environment-provided proxies for these local requests and closes the HTTP responses. Its small HTTP client comes from the Python `urllib.request` standard library.
Validate the file, then run the experiment
In a terminal opened in that new folder, run:
docker compose version
docker compose config --quiet
docker compose up --exit-code-from client The version command confirms you are invoking Compose v2. The `docker compose config` command parses and validates the resolved model; --quiet suppresses normal output. If validation fails, do not continue: check indentation, quoting and whether you copied the complete YAML.
The final command starts the backend and dependent client. Compose waits for the healthcheck to pass before creating the client. Once the client runs, the expected line is:
HTTP status: 200 The specific timing is not the point. Depending on scheduling and image startup, the first several health probes can fail while the backend sleeps. With the gated configuration, those failures should not launch the client early. The documented `--exit-code-from client` option returns the client's exit code and stops the other containers when that service exits, making the example convenient to run once.
Verification boundary: the YAML was parsed locally and the embedded Python expressions passed a syntax check on 2 November 2026. Docker was not available in that checking environment, so the example's end-to-end Compose output above is an *expected result*, not a claimed executed test. Run the commands on your own installation to verify it.
Clean up even if the run fails
The experiment is disposable. After inspecting its output, run:
docker compose down The Compose `down` reference confirms that this removes the project's containers and its default network. This particular file defines no persistent volumes or host binds, but check a real project's Compose file before using down elsewhere. In this tutorial folder, cleanup is deliberately separate from deleting your local compose.yaml file.
What the healthcheck actually proves
The important line is not depends_on alone. It is the test under backend.healthcheck:
healthcheck:
test:
- CMD
- python
- -c
- "from urllib.request import build_opener, ProxyHandler; build_opener(ProxyHandler({})).open('http://127.0.0.1:8000/', timeout=2).close()"
interval: 2s
timeout: 3s
retries: 10
start_period: 2s The CMD array runs a process inside the container. The command exits successfully if the HTTP request completes, and exits unsuccessfully on an uncaught connection error or timeout. Docker uses those outcomes to determine health. The healthcheck section of the Compose specification documents the command forms and timing controls.
interval schedules checks, timeout limits how long one check may take, retries controls consecutive failures before the container is marked unhealthy, and start_period provides an initial grace period for failures. These are example values for a deliberately short-lived demo, not universal production defaults. A database that imports data on first run needs a different allowance from a small local web process.
There is an important limitation to this HTTP probe: it establishes that this HTTP endpoint responded. It does not prove that a real API has database access, can complete a transaction, or has finished applying its migrations. Production readiness checks should cover the dependencies needed for the application to perform its intended work, while avoiding unexpectedly expensive operations and leaking sensitive state.
A command that always exits with code zero is not a useful readiness test. Neither is a sleep 30 that succeeds even when the database is broken. If the image has its own documented readiness tool—such as a database-specific status command—prefer a properly configured, narrowly scoped check. Docker's official startup example uses PostgreSQL's pg_isready in this way; adapt credentials, database selection and startup tolerance to your actual service instead of copying that demonstration blindly.
If the client never starts
An unhealthy dependency can legitimately hold a dependent service back. This can look like a startup hang when the real failure is inside the dependency or its healthcheck.
From the same tutorial directory, inspect the project rather than changing a delay at random:
docker compose ps --all
docker compose logs backend `docker compose ps` shows service and container state, while `docker compose logs` shows what the backend wrote to standard output and standard error. Read the actual error before acting.
If the backend never starts its HTTP server, check the command and its startup logs. If the backend serves requests but stays unhealthy, inspect the probe's target address, path, timeout and available tools inside that container. If the healthcheck passes yet the client fails, inspect the client's actual destination and the service's readiness definition: the two tests might be exercising different paths or different assumptions.
Avoid treating one successful probe as proof of long-term stability. Health can change after startup. And when an error points to the wrong hostname or published port rather than startup timing, the issue is networking, not dependency readiness.
A useful diagnostic question is: can the dependency answer the specific kind of request the client needs when the client is permitted to start? If not, improve the probe or fix the dependency itself.
What this does not fix
A health-gated startup is a one-time ordering guarantee for Compose-managed creation. It is not a substitute for resilience after creation. A database can restart, a connection can drop or a network can briefly fail later; the client still needs application-level timeouts, reconnection logic and bounded retries for operations where retries are safe.
Do not confuse this dependency rule with the optional restart: true *inside the long-form depends_on entry*. Docker documents that option as restarting the dependent service following certain explicit Compose operations on its dependency, such as a Compose-triggered restart. It does not promise that every unexpected process crash automatically restarts every dependent container. See the restart behaviour in Docker's startup-order guide.
Likewise, docker compose up --wait is a project-level CLI option that waits for services to be running or healthy; it is not the same thing as defining the dependency's readiness criterion. The Compose up reference describes both the option and its detached-mode behaviour.
For a real application, use both layers: a suitable startup gate to avoid an avoidable initial race, and recovery behaviour for failures that occur later. Neither layer should hide a broken or misconfigured dependency indefinitely.
The practical decision
Keep short-form depends_on when mere creation order is sufficient. Use service_healthy when the dependent cannot safely initialise until a healthcheck succeeds. Use service_completed_successfully for a genuine prerequisite task whose successful exit is what matters.
Then make the healthcheck mean what your application needs it to mean, run the configuration through docker compose config --quiet, observe the real client request, and test at least one restart or failure case on a safe development setup. The fix is not to guess a longer delay; it is to define readiness and verify the behaviour you actually depend on.
Sources
The example is based on Docker's Compose startup-order guide, service definition reference, healthcheck specification, Compose CLI reference and Python's HTTP server documentation.