Skip to content

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
   ▼
database

You might expect this:

start database
      │
      ▼
database ready
      │
      ▼
start backend

But 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 started

and:

service ready

A 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: alpine

This tells Compose that backend depends on database.

Conceptually:

database
   │
   │ dependency
   ▼
backend

Compose 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: alpine

The dependency tells Compose about the startup relationship, but the basic form does not mean:

wait until database accepts connections

It is closer to:

database starts
      │
      ▼
backend can start

If 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: 5

The container starts immediately.

But the health check initially fails because:

/tmp/ready

doesn’t exist.

After five seconds, the command creates it:

sleep 5
    │
    ▼
create /tmp/ready
    │
    ▼
healthcheck succeeds

Docker can then report the container as healthy.

The lifecycle becomes:

container starts
      │
      ▼
healthcheck runs
      │
      ├── fails → unhealthy
      │
      └── succeeds → healthy

This gives you a way to distinguish:

running

from:

healthy

What Do The healthcheck Options Mean?

A typical health check looks like:

healthcheck:
  test: ["CMD", "test", "-f", "/tmp/ready"]
  interval: 2s
  timeout: 1s
  retries: 5

The 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 succeeds

interval

interval: 2s

This tells Docker how frequently to run the health check.

Here:

every 2 seconds

timeout

timeout: 1s

This is how long Docker waits for an individual health check before considering that check failed.

retries

retries: 5

This 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
   │
   ▼
unhealthy

Or:

starting
   │
   ├── check fails
   ├── check succeeds
   ├── check succeeds
   │
   ▼
healthy

How 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: alpine

you 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: 5

Now the relationship is:

database starts
      │
      ▼
healthcheck runs
      │
      ▼
database becomes healthy
      │
      ▼
backend starts

This 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_healthy

means 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
   ▼
healthy

The 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:

ConditionMeaning
service_startedThe dependency has started
service_healthyThe dependency has passed its health check
service_completed_successfullyThe dependency has exited successfully

The default short syntax:

depends_on:
  - database

is essentially concerned with the dependency being started.

For readiness, use:

condition: service_healthy

For a one-time setup service that must successfully finish before another service starts, you can use:

condition: service_completed_successfully

What Is service_completed_successfully Useful For?

Not every dependency is a long-running service.

Imagine a setup task:

setup
  │
  │ completes successfully
  ▼
backend

You 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_successfully

The lifecycle is:

setup starts
    │
    ▼
setup finishes
    │
    ├── exit 0 → backend can start
    │
    └── non-zero exit → dependency not successful

This 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: true

means 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 restarted

This 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-stopped

This 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 considered

Compare the two:

SettingPurpose
services.<name>.restartControls restart behavior of that service’s container
depends_on.<name>.restartControls 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
starting

It does not automatically mean:

unhealthy → restart container

A 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 behavior

An application can therefore be:

container: running
health:    unhealthy

without 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:

RUNNING

while the application inside it is:

NOT READY

For example:

database container
        │
        ▼
process starts
        │
        ▼
database initializes
        │
        ├── container is running
        │
        └── database isn't ready yet

After initialization:

database ready
      │
      ▼
healthcheck succeeds
      │
      ▼
healthy

So:

running ≠ healthy

This 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-dependencies

Create 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_healthy

Start it:

docker compose up

The sequence is:

database container starts
        │
        ▼
database healthcheck begins
        │
        ▼
wait approximately 5 seconds
        │
        ▼
/tmp/ready is created
        │
        ▼
database becomes healthy
        │
        ▼
backend starts

The 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_healthy

The 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 backend

The problem is that ten seconds is an assumption.

The database might be ready after two seconds:

wasted waiting

or it might need fifteen seconds:

backend starts too early

A 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
    │
    ▼
frontend

Compose 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_started

The dependency graph becomes:

database
    │
    │ healthy
    ▼
backend
    │
    │ started
    ▼
frontend

The 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 unavailable

depends_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 failures

Therefore 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 restart

Once you separate these responsibilities, the configuration becomes much easier to reason about.

Cleanup

Stop and remove the example:

docker compose down

Then return to the parent directory:

cd ..

Remove the example directory when you no longer need it:

rm -rf compose-dependencies

What You Should Remember

The most important distinction in Compose service dependencies is:

container started
       ≠
application ready

Use:

depends_on:
  database:
    condition: service_healthy

when 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: true

when 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-stopped

The mental model is:

depends_on describes relationships, healthcheck describes 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.

Last updated on