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.
Table of Contents
- 1. Why an open port does not mean readiness
- 2. The three depends_on conditions at a glance
- 3. Healthcheck as a prerequisite for service_healthy
- 4. Practical example: PHP-FPM waits for MySQL
- 5. Practical example: combining Redis and multiple dependencies
- 6. service_completed_successfully for init containers and migrations
- 7. Behavior on failed healthchecks and restarts
- 8. Limits: what condition service_healthy does not solve
- 9. service_healthy compared to wait scripts and retry logic
- 10. Summary
- 11. FAQ
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.