Docker Compose depends_on on its own only controls the order in which containers are created and started — it does not wait for the database inside the container to accept connections. To stop your app from starting before Postgres or MySQL is ready, pair depends_on with condition: service_healthy and give the database a healthcheck that reports when it is actually serving queries.
This guide walks through a working Compose file, the long depends_on syntax, one-shot migration jobs, and the timing settings that decide whether a start takes five seconds or two minutes.
Before you start
- A Linux VPS running Docker Engine with the Compose v2 plugin. Confirm with
docker compose version. If that command is missing, follow Install Docker and Docker Compose on a Linux VPS. - A Compose file with at least two services — typically an application and a database.
- The
condition:form ofdepends_onrequires Compose v2 (thedocker composeplugin). The older Pythondocker-compose1.x only understood it in file format 2.x, so upgrade instead of fighting it. - Remove any top-level
version:key from the file. Compose v2 ignores it and prints a warning.
Tip: Test in a copy of the file or on a git branch so you can compare startup behaviour before and after the change.
Why depends_on alone is not enough
The short syntax looks harmless:
services:
app:
depends_on:
- dbCompose starts db, sees the container is running, and immediately starts app. Postgres needs a few seconds to initialise its data directory before it listens on port 5432, so the app exits with connection refused, could not connect to server, or a driver-specific ECONNREFUSED. With a restart policy the container loops until the database catches up, which works but produces noisy logs and slow starts.
The long syntax adds a condition. Compose supports three:
service_started— the container is running. Same as the short syntax.service_healthy— the container's healthcheck reports healthy.service_completed_successfully— the container ran and exited with code 0.
Step 1: Give the database a healthcheck
A healthcheck is a command Compose runs inside the container on a schedule. Exit code 0 means healthy.
services:
db:
image: postgres:16
environment:
POSTGRES_USER: deploy
POSTGRES_PASSWORD: example
POSTGRES_DB: app
healthcheck:
test: ["CMD-SHELL", "pg_isready -U deploy -d app"]
interval: 5s
timeout: 5s
retries: 10
start_period: 30s
volumes:
- db_data:/var/lib/postgresql/data
volumes:
db_data:pg_isready returns 0 only when the server accepts connections, which is exactly the signal the app needs. For MySQL or MariaDB, use a ping instead:
healthcheck:
test: ["CMD-SHELL", "mysqladmin ping -h 127.0.0.1 -u root -p$$MYSQL_ROOT_PASSWORD --silent"]
interval: 5s
timeout: 5s
retries: 10
start_period: 30sThe double $$ escapes the dollar sign so Compose passes $MYSQL_ROOT_PASSWORD through to the shell inside the container rather than substituting it on the host.
Note: start_period is a grace window. Failures during it do not count towards retries, so a slow first-time initialisation does not mark the container unhealthy.
Step 2: Point depends_on at the healthy condition
Now the app service waits for that health status:
app:
build: .
depends_on:
db:
condition: service_healthy
environment:
DATABASE_URL: postgres://deploy:example@db:5432/app
ports:
- "8080:8080"Compose resolves the hostname db through the project network, so the connection string uses the service name, not localhost or 127.0.0.1. If your app connects to 127.0.0.1 inside its own container it will never reach the database, no matter how the startup order is configured.
You can list several dependencies, each with its own condition:
depends_on:
db:
condition: service_healthy
cache:
condition: service_healthyStep 3: Start the stack and watch the order
docker compose up -d
docker compose ps
docker compose logs -f appdocker compose ps shows a STATUS column with Up 12 seconds (healthy) once the check passes. The app container stays in Created until then, so the log stream for app stays empty for the first few seconds — that is expected, not a hang.
To make the command itself block until everything is healthy, add --wait:
docker compose up -d --waitThis is useful in deploy scripts: the command returns non-zero if a service never becomes healthy, so a broken release fails the pipeline instead of going live.
Step 4: Wait for one-shot jobs with service_completed_successfully
Database migrations are a common third service. They must run after the database is healthy and finish before the app starts.
migrate:
build: .
command: ["./manage.py", "migrate", "--noinput"]
depends_on:
db:
condition: service_healthy
app:
build: .
depends_on:
migrate:
condition: service_completed_successfully
db:
condition: service_healthyWarning: Do not give the migration service a restart: always policy. A one-shot container that exits 0 would be restarted forever, and service_completed_successfully would never be satisfied in the way you expect. Leave the restart policy off for job containers.
Step 5: Tune the timing
The defaults are conservative. Adjust them so a healthy start is fast and a genuinely broken start still fails:
interval— how often the check runs.5sis a good default;1sadds load for little gain.timeout— how long a single check may take before it counts as a failure.retries— consecutive failures before the container is marked unhealthy.10with a5sinterval gives roughly 50 seconds of tolerance afterstart_period.start_period— set this to your worst realistic initialisation time, especially on first boot when the data directory is empty.
Note: A failing healthcheck marks the container unhealthy; it does not restart it. If you want automatic replacement, that is a separate mechanism — see Docker restart policies: keep containers running after a reboot.
Also remember that depends_on only applies at start time. If the database restarts later, Compose does not restart the app for you, so keep reconnect logic in the application or a restart policy on the app container.
Check it works
- Bring the stack down and up again:
docker compose down && docker compose up -d --wait. - Confirm the health status directly:
docker inspect --format '{{.State.Health.Status}}' $(docker compose ps -q db)— expecthealthy. - Confirm the app started after the database by checking timestamps:
docker compose logs --timestamps db app | head -40. - Validate the file before deploying:
docker compose configprints the resolved configuration and fails on syntax errors.
Troubleshooting
- App still exits with
connection refused— the healthcheck passes too early.pg_isreadyconfirms the server accepts connections but not that your database and user exist yet. Check the credentials inDATABASE_URLand lengthenstart_period. depends_oncondition is ignored, or Compose errors with an invalid type — you are running the legacydocker-compose1.x binary or a file format 3.x file with an old CLI. Switch todocker compose(v2) and drop theversion:key.docker compose upwaits forever and never starts the app — the dependency never becomes healthy. Rundocker compose logs dbanddocker compose psto see whether the container is restarting, and test the healthcheck command manually with Open a shell inside a running Docker container.- Healthcheck reports
command not found— the image does not includecurlorwget. Use a tool that ships with the image, or install it in your own Dockerfile. - Everything is healthy but the app cannot reach the database — the app is connecting to
127.0.0.1instead of the service name, or the two services are on different networks. See Troubleshooting container networking issues: bridge vs host mode in Docker.
Next steps
- Add health checks to Docker containers for the full set of healthcheck options and patterns.
- Docker Compose file explained: services, volumes, networks and ports to structure the rest of the file properly.
- View and rotate Docker container logs so startup failures are easy to read after the fact.
- Docker persistent storage for non-root users on a Linux VPS to keep database data safe across rebuilds.
