Docker Compose depends_on with condition service_healthy
AI generated
FROM
RUN
Docker · Docker Compose · Healthcheck · Orchestration
depends_on with condition service_healthy
Real startup order instead of guessing at ports

An open database port does not mean MySQL is ready to process connections. depends_on with condition service_healthy ties the startup order of Docker Compose services directly to the healthcheck status, instead of relying on a plain reachability signal, and eliminates one of the most common sources of errors in multi service stacks.

18 min read depends_on · service_healthy · service_completed_successfully Docker Compose 2.x

1. Why an open port does not mean readiness

The classic depends_on directive in Docker Compose only guarantees one thing: the dependent service is started after the referenced service's container has been started. It explicitly does not guarantee that the application inside the referenced container is actually ready to process requests. With MySQL, for example, there are often several seconds between the start of the container process and actual readiness to accept connections, during which the database server is still initializing, checking tables or running recovery processes.

A PHP-FPM service that establishes a connection to MySQL immediately after the plain depends_on start runs into exactly this time window and gets a connection refused error, even though the MySQL container is formally already running. This problem affected almost every Docker Compose stack with a database dependency for a long time and led to an entire generation of workarounds, from sleep commands in entrypoint scripts to external wait for it tools.

depends_on with condition service_healthy solves this problem structurally by tying the startup order not to the container start, but to the actual healthcheck status. A service with this condition only starts once the healthcheck of the referenced service reports the status healthy, which entirely removes the need for external wait mechanisms in the vast majority of cases.

2. The three depends_on conditions at a glance

In the long form of depends_on, Docker Compose knows three different conditions, each defining a different criterion for startup order. service_started is the default condition and matches the classic behavior: the dependent service starts as soon as the referenced container has started, regardless of the internal state of the application. service_healthy requires that the referenced service has a defined healthcheck and that it reports the status healthy before the dependent service starts.

The third condition, service_completed_successfully, is intended for one time init processes and waits until the referenced container has exited successfully, that is, with exit code 0. This condition is particularly suited for database migrations or setup scripts that run once and are meant to finish before the actual application starts.


# compose.yaml — long-form depends_on with conditions
services:
  php-fpm:
    build: .
    depends_on:
      mysql:
        condition: service_healthy
      redis:
        condition: service_healthy
      db-migrate:
        condition: service_completed_successfully

  mysql:
    image: mysql:8.0
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
      interval: 5s
      timeout: 3s
      retries: 10

  redis:
    image: redis:7
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 5s
      timeout: 3s
      retries: 10

  db-migrate:
    build: .
    command: ["php", "bin/console", "migrate"]
    depends_on:
      mysql:
        condition: service_healthy

In this example, php-fpm waits simultaneously for two service_healthy conditions and one service_completed_successfully condition. Only once MySQL and Redis are reported as healthy and the migration container has run through successfully does php-fpm actually start. Docker Compose resolves this dependency chain automatically and starts the services in the correct order, without needing any manual wait time in the application code.

3. Healthcheck as a prerequisite for service_healthy

The service_healthy condition only works if the referenced service actually defines a healthcheck block. Without its own healthcheck, Docker Compose does not know any health status at all for that service, and a depends_on with condition service_healthy on a service without a healthcheck causes a configuration error at startup. The healthcheck itself consists of a test command executed inside the container, along with interval, timeout, retries and optionally start_period.

For databases, the usual healthcheck command is the respective built in ping tool: mysqladmin ping for MySQL, redis-cli ping for Redis, pg_isready for PostgreSQL. These commands check not just whether the process is running, but whether the service is actually accepting connections and answering basic requests, which is the decisive difference from a plain port check.

4. Practical example: PHP-FPM waits for MySQL

For a typical PHP-FPM service that establishes a database connection at startup and does not function without it, condition service_healthy is the obvious solution. The MySQL healthcheck only reports healthy once mysqladmin ping answers successfully, which in practice means the server is actually ready to accept connections, instead of just having started the container process.

Without this safeguard, developers would have to either build retry logic with exponential backoff into the application itself, or run an external wait script in the PHP-FPM container's entrypoint that checks via a TCP connection attempt whether MySQL is already reachable. Both approaches work, but are extra code that condition service_healthy makes entirely unnecessary, because Docker Compose itself takes over this waiting.


# compose.yaml — PHP-FPM waits for a fully ready MySQL
services:
  php-fpm:
    build: ./docker/php
    depends_on:
      mysql:
        condition: service_healthy
    environment:
      DB_HOST: mysql

  mysql:
    image: mysql:8.0
    environment:
      MYSQL_ROOT_PASSWORD_FILE: /run/secrets/mysql_root_password
      MYSQL_DATABASE: magento
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-p$$(cat /run/secrets/mysql_root_password)"]
      interval: 5s
      timeout: 3s
      retries: 15
      start_period: 20s
    secrets:
      - mysql_root_password

secrets:
  mysql_root_password:
    file: ./secrets/mysql_root_password.txt

5. Practical example: combining Redis and multiple dependencies

In real stacks, a service rarely depends on just one single resource. A typical Magento or PHP application server needs both a database connection and a Redis cache for sessions and full page cache. Docker Compose allows combining multiple depends_on entries, each with its own condition, and only starts the dependent service once all referenced conditions are met at the same time.

Important here: the different dependencies are checked in parallel, not sequentially. Docker Compose does not wait for MySQL first and then start checking Redis afterwards, it starts the healthchecks of all dependencies simultaneously and only lets the dependent service start once the slowest dependency is also healthy. This minimizes the overall startup time of the stack compared to a purely sequential wait logic.

6. service_completed_successfully for init containers and migrations

Besides ongoing operation, many stacks have one time initialization steps that need to be completed before the actual application start: database migrations, seeding data or generating static assets. For these cases, service_completed_successfully is the fitting condition, because it does not wait for a persistent healthcheck status, but for the successful exit of a one time container.

A common pattern in Magento projects: a dedicated migrate service runs bin/magento setup:upgrade and then exits with exit code 0. The actual php-fpm service references this migrate service with condition service_completed_successfully and only starts after the migration has run through successfully. If the migration fails and the container exits with an error exit code, the dependent service does not start at all, which prevents the application from booting up against an inconsistent database schema.


# compose.yaml — one-shot migration gates the application start
services:
  magento-setup:
    build: .
    command: ["php", "bin/magento", "setup:upgrade"]
    depends_on:
      mysql:
        condition: service_healthy

  php-fpm:
    build: .
    depends_on:
      magento-setup:
        condition: service_completed_successfully
      mysql:
        condition: service_healthy

7. Behavior on failed healthchecks and restarts

An important detail concerns the behavior when a service whose healthcheck serves as a condition becomes unhealthy after startup, for example because the database briefly fails during operation. depends_on with condition service_healthy only takes effect at the initial start of the dependent service, not as ongoing monitoring during operation. If MySQL becomes unhealthy after php-fpm has started, this does not automatically cause php-fpm to be stopped or restarted.

For real ongoing monitoring, additional mechanisms are needed, for example dedicated healthchecks in the dependent service itself that also report unhealthy on database connection errors, combined with a restart policy like unless-stopped or on-failure, so an externally detected failure leads to an automatic restart. condition service_healthy is thus a tool for correct startup order, not a tool for ongoing failover management.

8. Limits: what condition service_healthy does not solve

A common misunderstanding is that condition service_healthy makes the application itself more robust against later connection failures. That is not the case. The condition only ensures that the service does not run against a not yet ready dependency too early on the very first start. Connection losses during ongoing operation, for example due to a MySQL restart or a network issue, still need to be caught by retry logic in the application itself.

Another limit: condition service_healthy only works within a single docker compose up invocation. If an already running dependent service is manually restarted with docker compose restart while the referenced dependency is briefly unhealthy, Compose does not automatically wait in this case, because restart is a different command path than the initial up. For production like robustness, a combination of service_healthy for startup and retry logic in the application for ongoing operation remains the complete solution.

9. service_healthy compared to wait scripts and retry logic

Before condition service_healthy was introduced, teams usually solved the startup order problem with external wait scripts or retry logic directly in the application. Both approaches work, but bring extra code and extra complexity that condition service_healthy, as a built in Compose feature, entirely avoids.

Approach Location of the logic Extra code Declarative in compose.yaml
sleep in the entrypoint Shell script inside the container Yes, fragile and slow No
wait-for-it / dockerize External tool in the image Yes, extra dependency No
Retry logic in the application Application code Yes, ongoing maintenance No
depends_on condition service_healthy compose.yaml No Yes

The decisive advantage of condition service_healthy is that the startup order becomes part of the versioned compose.yaml, instead of being hidden in shell scripts or application code. Retry logic in the application remains sensible for ongoing operation, but for the pure startup order problem it is the more expensive solution compared to the declarative Compose condition.

Mironsoft

Docker Compose orchestration and healthcheck design

Stacks that no longer hit connection refused at startup?

We set up condition service_healthy for your multi service stacks, remove fragile wait scripts and ensure a reliable startup order between MySQL, Redis and PHP-FPM.

Healthcheck design

Defining fitting test commands and timings for databases and caches

Startup order

Structuring depends_on with service_healthy and service_completed_successfully

Magento migrations

setup:upgrade as a gate before the actual application container starts

10. Summary

depends_on with condition service_healthy replaces the assumption that a started container is ready with a check of an actual healthcheck status. Instead of relying on an open port, a dependent service only waits until MySQL, Redis or another service is reported healthy via its healthcheck. The condition service_completed_successfully complements this pattern for one time init processes like database migrations that need to complete successfully before the actual application start.

It remains important that condition service_healthy only secures the initial start, not ongoing operation. For real fault tolerance during runtime, the application still needs its own retry logic. Compared to external wait scripts or manually built sleep logic, condition service_healthy is nevertheless the significantly cleaner, declarative solution for the startup order problem in Docker Compose stacks.

depends_on with condition service_healthy — The Essentials at a Glance

service_started

Default behavior, only waits for the container to start, not for application readiness.

service_healthy

Waits for a successful healthcheck of the referenced service, requires a defined healthcheck block.

service_completed_successfully

Waits for successful exit of a one time container, ideal for migrations and setup scripts.

Limit

Only takes effect at the initial start, no ongoing monitoring during operation.

11. FAQ: depends_on with condition service_healthy

1What does condition service_healthy do?
Waits to start a service until the referenced service is reported healthy.
2service_started vs. service_healthy?
service_started only waits for container start, service_healthy for real application readiness.
3Does it need its own healthcheck?
Yes, without a healthcheck block service_healthy leads to a configuration error.
4What is service_completed_successfully for?
For one time init processes like migrations that must exit successfully before the app starts.
5Does it protect against later failures?
No, only relevant at initial start, retry logic during operation remains necessary.
6How do I define a healthcheck for MySQL?
mysqladmin ping as the test command, with interval, timeout, retries and start_period.
7Can I combine multiple conditions?
Yes, each dependency has its own condition, all are checked in parallel.
8What if a dependency never becomes healthy?
After exhausting retries, Compose reports an error and the dependent service does not start.
9Does it replace wait-for-it tools?
In most cases yes, external tools only needed for special cases.
10Does it work with docker compose restart?
Not reliably, restart takes a different path than the initial up call.