Managing Services Dependencies in Docker Compose
In a multi-service application, starting containers in the right order is not always enough.
A backend may depend on a database. A worker may depend on another service. If Compose starts everything at once, a service can be running before the service it needs is actually ready to accept requests and application may crash or become unfunctional.
Compose gives you several tools for describing these relationships: depends_on, dependency conditions, restart, and healthcheck.
The important distinction is:
Starting a container is not the same as the application inside that container being ready.
Why Does Service Dependency Matter?
Imagine:
backend
│
│ needs
▼
databaseYou might expect this:
start database
│
▼
database ready
│
▼
start backendBut without dependency configuration, Compose can start both services:
start database ────────┐
├──► both containers start
start backend ────────┘The database container may still be initializing when the backend tries to connect.
This is the difference between:
container startedand:
service readyA container can be running while the application inside it is still starting up.
What Does depends_on Do?
The simplest form is:
services:
backend:
image: alpine
depends_on:
- database
database:
image: alpineThis tells Compose that backend depends on database.
Conceptually:
database
│
│ dependency
▼
backendCompose uses this relationship when starting and stopping services.
The important part is that depends_on expresses a dependency between services.
It does not, by itself, prove that the application inside the dependency is ready.
Does depends_on Wait For The Application To Be Ready?
Not in its short form.
Consider:
services:
backend:
image: alpine
depends_on:
- database
database:
image: alpineThe dependency tells Compose about the startup relationship, but the basic form does not mean:
wait until database accepts connectionsIt is closer to:
database starts
│
▼
backend can startIf the database application takes additional time to initialize, the backend may still start too early.
That is why healthcheck becomes important.
What Is A healthcheck?
A health check lets Docker test whether a container’s application is healthy.
For example:
services:
app:
image: alpine
command: ["sh", "-c", "sleep 5; touch /tmp/ready; sleep infinity"]
healthcheck:
test: ["CMD", "test", "-f", "/tmp/ready"]
interval: 2s
timeout: 1s
retries: 5The container starts immediately.
But the health check initially fails because:
/tmp/readydoesn’t exist.
After five seconds, the command creates it:
sleep 5
│
▼
create /tmp/ready
│
▼
healthcheck succeedsDocker can then report the container as healthy.
The lifecycle becomes:
container starts
│
▼
healthcheck runs
│
├── fails → unhealthy
│
└── succeeds → healthyThis gives you a way to distinguish:
runningfrom:
healthyWhat Do The healthcheck Options Mean?
A typical health check looks like:
healthcheck:
test: ["CMD", "test", "-f", "/tmp/ready"]
interval: 2s
timeout: 1s
retries: 5The pieces have different jobs.
test
test: ["CMD", "test", "-f", "/tmp/ready"]This is the command Docker uses to check the container.
A successful command means the check passes.
A failed command means the check fails.
For this example:
file exists
│
▼
health check succeedsinterval
interval: 2sThis tells Docker how frequently to run the health check.
Here:
every 2 secondstimeout
timeout: 1sThis is how long Docker waits for an individual health check before considering that check failed.
retries
retries: 5This controls how many consecutive failures are required before the container is considered unhealthy.
The exact state transitions can therefore look like:
starting
│
├── check fails
├── check fails
├── check fails
│
▼
unhealthyOr:
starting
│
├── check fails
├── check succeeds
├── check succeeds
│
▼
healthyHow Do depends_on And healthcheck Work Together?
This is where Compose becomes much more useful.
Instead of:
services:
backend:
image: alpine
depends_on:
- database
database:
image: alpineyou can define a condition:
services:
backend:
image: alpine
depends_on:
database:
condition: service_healthy
database:
image: alpine
healthcheck:
test: ["CMD", "test", "-f", "/tmp/ready"]
interval: 2s
timeout: 1s
retries: 5Now the relationship is:
database starts
│
▼
healthcheck runs
│
▼
database becomes healthy
│
▼
backend startsThis is much closer to what you usually mean when saying:
“The backend depends on the database.”
You don’t merely care that the database container exists.
You care that the database service is ready enough for the backend to use it.
What Is condition: service_healthy?
This:
depends_on:
database:
condition: service_healthymeans Compose waits for the database service to become healthy before starting the dependent service.
The complete relationship is:
backend
│
│ depends_on
│ condition: service_healthy
▼
database
│
│ healthcheck
▼
healthyThe health check belongs to the dependency.
The condition belongs to the dependent service.
This is an important distinction.
What Other Dependency Conditions Exist?
Compose supports conditions for different dependency situations.
The commonly useful conditions are:
| Condition | Meaning |
|---|---|
service_started | The dependency has started |
service_healthy | The dependency has passed its health check |
service_completed_successfully | The dependency has exited successfully |
The default short syntax:
depends_on:
- databaseis essentially concerned with the dependency being started.
For readiness, use:
condition: service_healthyFor a one-time setup service that must successfully finish before another service starts, you can use:
condition: service_completed_successfullyWhat Is service_completed_successfully Useful For?
Not every dependency is a long-running service.
Imagine a setup task:
setup
│
│ completes successfully
▼
backendYou can describe that relationship:
services:
setup:
image: alpine
command: ["sh", "-c", "echo setup complete"]
backend:
image: alpine
command: ["sleep", "infinity"]
depends_on:
setup:
condition: service_completed_successfullyThe lifecycle is:
setup starts
│
▼
setup finishes
│
├── exit 0 → backend can start
│
└── non-zero exit → dependency not successfulThis is useful when one service performs a task that must finish before another service starts.
For example, a setup operation might prepare something the next service requires.
The important idea is:
A dependency doesn’t always mean “keep this service running.” Sometimes it means “this task must successfully finish first.”
What Does restart Mean Inside depends_on?
Compose can also express a restart relationship:
services:
backend:
image: alpine
depends_on:
database:
condition: service_healthy
restart: true
database:
image: alpine
healthcheck:
test: ["CMD", "test", "-f", "/tmp/ready"]Here:
restart: truemeans Compose should restart the dependent service when the dependency is explicitly restarted through a Compose operation.
Think of it as:
database
│
│ explicitly restarted
▼
backend
│
▼
also restartedThis is useful when a dependent service needs to reconnect or reinitialize after its dependency is explicitly restarted.
Note
This restart under depends_on is different from a service-level restart policy. depends_on.restart describes what Compose should do to a dependent service when the dependency is explicitly restarted. It is not the same thing as automatically restarting a crashed container.
What Is A Service-Level restart Policy?
A service can also have its own restart setting:
services:
app:
image: alpine
command: ["sh", "-c", "sleep 5"]
restart: unless-stoppedThis is a different concept. Here, restart is a service-level restart policy. It controls whether Docker should restart the container under the configured restart behavior.
Conceptually:
service-level restart
│
▼
container exits
│
▼
restart policy consideredCompare the two:
| Setting | Purpose |
|---|---|
services.<name>.restart | Controls restart behavior of that service’s container |
depends_on.<name>.restart | Controls whether a dependent service is restarted after its dependency is explicitly restarted by Compose |
Don’t confuse these two because they use the same word for different relationships.
Does A Health Check Restart An Unhealthy Container?
No.
This is another important distinction.
A health check reports a health state:
healthy
unhealthy
startingIt does not automatically mean:
unhealthy → restart containerA health check answers:
“Is the application inside this container currently healthy?”
A restart policy answers:
“What should happen when the container exits?”
These are separate mechanisms.
Think of them as:
healthcheck
│
└── reports health
restart policy
│
└── controls restart behaviorAn application can therefore be:
container: running
health: unhealthywithout the container automatically being restarted merely because the health check failed.
What Is The Difference Between Running And Healthy?
This distinction is important enough to remember separately.
A container can be:
RUNNINGwhile the application inside it is:
NOT READYFor example:
database container
│
▼
process starts
│
▼
database initializes
│
├── container is running
│
└── database isn't ready yetAfter initialization:
database ready
│
▼
healthcheck succeeds
│
▼
healthySo:
running ≠ healthyThis is one of the most important concepts when designing service dependencies.
A Complete Dependency Example
Let’s put these ideas together.
Create a directory:
mkdir compose-dependencies
cd compose-dependenciesCreate compose.yaml:
services:
database:
image: alpine
command: ["sh", "-c", "sleep 5; touch /tmp/ready; sleep infinity"]
healthcheck:
test: ["CMD", "test", "-f", "/tmp/ready"]
interval: 2s
timeout: 1s
retries: 5
backend:
image: alpine
command: ["sh", "-c", "echo 'backend started'; sleep infinity"]
depends_on:
database:
condition: service_healthyStart it:
docker compose upThe sequence is:
database container starts
│
▼
database healthcheck begins
│
▼
wait approximately 5 seconds
│
▼
/tmp/ready is created
│
▼
database becomes healthy
│
▼
backend startsThe Compose configuration now expresses the actual dependency rather than relying on timing.
This is much more reliable than simply hoping the database starts quickly enough.
What If The Dependency Never Becomes Healthy?
Suppose the database health check never succeeds.
Then:
database
│
├── running
│
└── unhealthy
│
▼
backend
│
└── waits because condition is
service_healthyThe backend should not be started merely because the database container exists. This is exactly why health checks are valuable in dependency relationships. A health check should therefore test something meaningful. For a real service, a good health check should answer something close to:
“Can this service perform the basic operation that its dependents need?”
A superficial check that only verifies that a process exists may not tell you whether the application is actually ready.
Don’t Use Arbitrary Sleep As A Readiness Mechanism
You may be tempted to do something like:
start database
│
▼
sleep 10 seconds
│
▼
start backendThe problem is that ten seconds is an assumption.
The database might be ready after two seconds:
wasted waitingor it might need fifteen seconds:
backend starts too earlyA health check is better because it tests an actual condition:
flowchart
db["database starts"] --> ready{"Is It Ready?"}
ready -- yes --> backend_starts["Backend Starts"]
ready -- no --> backend_no_start["Backend Never Starts"]
Tip
Prefer checking the actual readiness condition over guessing how long a service will take to start.
Dependency Chains
Dependencies can form a chain. For example: backend depends on database and frontend depends on backend:
database
│
▼
backend
│
▼
frontendCompose can represent that:
services:
database:
image: alpine
healthcheck:
test: ["CMD", "test", "-f", "/tmp/ready"]
interval: 2s
timeout: 1s
retries: 5
backend:
image: alpine
depends_on:
database:
condition: service_healthy
frontend:
image: alpine
depends_on:
backend:
condition: service_startedThe dependency graph becomes:
database
│
│ healthy
▼
backend
│
│ started
▼
frontendThe important part is not to create dependencies unnecessarily. A service should only depend on another service when there is a real reason for that dependency.
Dependency Management Is Not Application Retry Logic
Even with depends_on and health checks, an application should generally be able to handle temporary connection failures.
Why?
Because services can become unavailable after startup.
For example:
backend starts
│
▼
database healthy
│
▼
backend connects
│
▼
database temporarily unavailabledepends_on helped with the initial startup relationship. It does not turn the application’s runtime communication into a guaranteed connection. The application itself may still need to handle temporary failures.
This gives you another useful distinction:
Compose dependency management
│
└── startup relationships
Application retry/reconnection logic
│
└── runtime failuresTherefore it’s worth thinking this way:
Compose helps coordinate the application. It does not replace the application’s own error handling — application should be designed to handle errors.
A Useful Mental Model
You can now think about service startup as several different states:
stateDiagram-v2
state if_healthcheck_pass <<choice>>
state "Container Running" as Running
[*] --> Running
Running --> healthcheck
healthcheck --> if_healthcheck_pass
if_healthcheck_pass --> Healthy: pass
if_healthcheck_pass --> Unealthy: fail
Healthy --> DependentServiceStarts
Unealthy --> DependentServiceNeverStarts
You may question about “never starts” but it is intentional — there is no point starting dependent service if it’s dependencies are unhealthy — you should debug and solve why the dependencies are failing first instead of tempting to start a service.
And the dependency tools fit into different parts of this model:
depends_on
│
└── defines dependency relationship
condition
│
└── defines what must happen before dependent starts
healthcheck
│
└── determines whether a service is healthy
service restart
│
└── controls restart behavior of a service
depends_on.restart
│
└── coordinates dependent restart after
an explicit dependency restartOnce you separate these responsibilities, the configuration becomes much easier to reason about.
Cleanup
Stop and remove the example:
docker compose downThen return to the parent directory:
cd ..Remove the example directory when you no longer need it:
rm -rf compose-dependenciesWhat You Should Remember
The most important distinction in Compose service dependencies is:
container started
≠
application readyUse:
depends_on:
database:
condition: service_healthywhen a service should wait for another service to pass its health check.
Use:
healthcheck:
...to define how Docker determines whether a container’s application is healthy.
Use:
depends_on:
database:
restart: truewhen you want a dependent service to be restarted after its dependency is explicitly restarted through Compose.
And keep service-level restart policies separate:
restart: unless-stoppedThe mental model is:
depends_ondescribes relationships,healthcheckdescribes readiness/health, and restart settings describe restart behavior.
With dependency management added, you now have another important piece of the Compose architecture. The final Zero-to-Hero example can use these relationships to build a realistic application rather than simply starting several unrelated containers.