Suggest an editImprove this articleRefine the answer for “How to manage service dependencies in Docker Compose?”. Your changes go to moderation before they’re published.Approval requiredContentWhat you’re changing🇺🇸EN🇺🇦UAPreviewTitle (EN)Short answer (EN)**Use `depends_on` with `condition: service_healthy`** in Compose. Plain `depends_on: [db]` only orders container starts; with `condition: service_healthy` Compose waits until the dependency's healthcheck passes before starting the dependent. ```yaml services: api: depends_on: db: condition: service_healthy db: image: postgres:16 healthcheck: test: ["CMD-SHELL", "pg_isready -U postgres"] interval: 5s retries: 5 ``` **Key:** start-order alone is not enough — a started container is not always a ready one. The `service_healthy` condition (with a real healthcheck on the dep) is what gives you actual readiness gating.Shown above the full answer for quick recall.Answer (EN)Image**Service dependencies in Docker Compose** are about more than start order. The dep being "started" rarely means "ready to accept connections". The right pattern: `depends_on: condition: service_healthy` with a healthcheck on the dependency. ## Theory ### TL;DR - Plain `depends_on: [db]` = start order only. Compose starts `db` first, then `api`. Does NOT wait for `db` to be ready. - `depends_on: db: condition: service_healthy` = waits until `db`'s healthcheck passes. This is what you want for production. - Three conditions: `service_started`, `service_healthy`, `service_completed_successfully` (for one-off init containers). - **Without depends_on**, services start in parallel. With it, Compose enforces the order. - For non-Compose orchestrators, the same idea exists: K8s `initContainers`, Swarm dependency-via-healthcheck-on-routes. ### Quick example ```yaml # compose.yaml services: api: image: myapp environment: DATABASE_URL: postgres://postgres:dev@db:5432/app depends_on: db: condition: service_healthy redis: condition: service_started migrate: condition: service_completed_successfully db: image: postgres:16 environment: POSTGRES_PASSWORD: dev POSTGRES_DB: app healthcheck: test: ["CMD-SHELL", "pg_isready -U postgres"] interval: 5s retries: 5 start_period: 5s redis: image: redis:7 migrate: image: mymigrator depends_on: db: condition: service_healthy command: ["npm", "run", "migrate"] ``` ```bash $ docker compose up -d [+] Running 4/4 ✔ Container db Healthy 2.3s ✔ Container redis Started 2.4s ✔ Container migrate Exited 8.7s # ran, succeeded, exited 0 ✔ Container api Started 8.8s # waited for migrate to finish ``` Four services, three different dependency conditions, perfect ordering: db ready → migrate runs → migrate succeeds → api starts (with redis already up). ### The three conditions #### `service_started` (the default) ```yaml depends_on: - db # OR explicitly: depends_on: db: condition: service_started ``` Waits until the container starts. Does NOT wait for the app inside to be ready. **Almost never what you want.** #### `service_healthy` ```yaml depends_on: db: condition: service_healthy ``` Waits until the dep's healthcheck passes. Requires `db` to have a `healthcheck:` defined. **The right default for runtime dependencies.** If `db` has no healthcheck, this fails the dependent's start (Compose errors out with "service has no healthcheck"). #### `service_completed_successfully` ```yaml depends_on: migrate: condition: service_completed_successfully ``` Waits for the dep to **exit with code 0**. Used for one-shot init/migration containers — they run, do their job, exit, then the main service starts. ### Why `depends_on` alone is not enough ```yaml # WRONG: only orders starts; api starts while postgres is still booting services: api: depends_on: - db db: image: postgres:16 ``` ```bash $ docker compose up Container db Started Container api Started # but db needs ~3 seconds to actually accept connections # api hits db → Connection refused → api crashes or retries ``` Most databases need 1-5 seconds after "started" before they accept queries. `depends_on` alone does not know this. The healthcheck does. ### App-level retry as a safety net Even with `service_healthy`, transient db disconnects happen at runtime. Production apps should retry with backoff: ```js // Pseudo-code async function connectDB() { for (let i = 0; i < 10; i++) { try { return await pg.connect(...); } catch (e) { console.log(`db retry ${i+1}/10: ${e.message}`); await sleep(1000 * (i + 1)); } } throw new Error('db unreachable after 10 attempts'); } ``` App-level retry + Compose `service_healthy` together = robust startup. ### Common mistakes **Using plain `depends_on` for a real dependency** ```yaml # WRONG depends_on: [db] ``` Starts api before db is ready. App fails on first connection. **Forgetting the dep needs a healthcheck** ```yaml services: api: depends_on: db: condition: service_healthy db: image: postgres:16 # no healthcheck: ``` ``` ERROR: ...service "db" has no healthcheck defined ``` Fix: add a `healthcheck:` block to `db`. **Wait-for scripts are not always needed** The pre-`condition: service_healthy` era required scripts like [wait-for-it](https://github.com/vishnubob/wait-for-it) or [dockerize](https://github.com/jwilder/dockerize): ```dockerfile CMD ["./wait-for-it.sh", "db:5432", "--", "node", "server.js"] ``` With Compose v3+ and `service_healthy`, you do not need these for inter-service start order. They are still useful for *external* dependencies (cloud DBs reached over the network). **Cyclic dependencies** ```yaml services: a: depends_on: [b] b: depends_on: [a] ``` ``` ERROR: dependency cycle detected ``` Compose refuses. Real dependency graphs must be acyclic. **Dependency only honored at startup, not at restart of a single service** ```bash $ docker compose restart api # Restarts only api. Does NOT verify db's health again. ``` The healthcheck-gated dependency is enforced during `up`. `restart` is a quicker operation. Usually fine; rarely a foot-gun. ### Real-world patterns #### Pattern: db + migrator + app ```yaml services: db: image: postgres:16 healthcheck: test: ["CMD-SHELL", "pg_isready -U postgres"] interval: 3s retries: 10 migrate: image: myapp command: ["npm", "run", "migrate"] depends_on: db: condition: service_healthy app: image: myapp depends_on: migrate: condition: service_completed_successfully db: condition: service_healthy ``` `db` healthy → `migrate` runs → `migrate` exits 0 → `app` starts. Three conditions, deterministic startup. #### Pattern: web + api + db ```yaml services: db: image: postgres:16 healthcheck: test: ["CMD-SHELL", "pg_isready -U postgres"] interval: 5s api: build: ./api depends_on: db: condition: service_healthy healthcheck: test: ["CMD", "wget", "-q", "--spider", "http://localhost:3000/health"] web: image: nginx depends_on: api: condition: service_healthy ports: ["80:80"] ``` Three-tier dependency chain, each gate is healthcheck-driven. ### Follow-up questions **Q:** Does `depends_on` work across multiple Compose files? **A:** Yes — `depends_on` resolves within the merged result of all `-f` files. References must point at services defined somewhere in the merged config. **Q:** Can a service depend on multiple services with mixed conditions? **A:** Yes. Use the map form to set per-dep conditions: ```yaml depends_on: db: { condition: service_healthy } redis: { condition: service_started } migrate: { condition: service_completed_successfully } ``` **Q:** Does `depends_on` carry over to `docker compose run`? **A:** Yes — Compose starts deps when you do `docker compose run --rm api ...`. Disable with `--no-deps`. **Q:** What if my dep is an external service (like AWS RDS)? **A:** Compose cannot health-check it. Either add a small wrapper container with a healthcheck that probes the external service, or rely on app-level retry. **Q:** (Senior) How does this map to Kubernetes? **A:** Kubernetes does not have `depends_on` for pods directly. The equivalents: `initContainers` (run before main containers, must succeed), readiness probes (gate traffic to the pod), and external orchestration tools (Argo Workflows, etc.) for complex dependencies. Helm charts often hardcode startup-order assumptions using deployments + services + readiness probes; the explicit dependency graph is a Compose-flavored convention. ## Examples ### Real prod-shape Compose ```yaml services: db: image: postgres:16 environment: POSTGRES_PASSWORD: ${DB_PASSWORD:-dev} POSTGRES_DB: app volumes: - pgdata:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U postgres -d app"] interval: 3s timeout: 2s retries: 10 start_period: 5s restart: unless-stopped migrate: image: myapp:${TAG:-latest} command: ["npm", "run", "migrate"] depends_on: db: condition: service_healthy environment: DATABASE_URL: postgres://postgres:${DB_PASSWORD:-dev}@db:5432/app restart: "no" api: image: myapp:${TAG:-latest} depends_on: migrate: condition: service_completed_successfully db: condition: service_healthy environment: DATABASE_URL: postgres://postgres:${DB_PASSWORD:-dev}@db:5432/app healthcheck: test: ["CMD-SHELL", "wget -q --spider http://localhost:3000/health"] interval: 30s retries: 3 start_period: 10s restart: unless-stopped web: image: nginx:1.27-alpine depends_on: api: condition: service_healthy ports: ["80:80"] volumes: - ./nginx.conf:/etc/nginx/nginx.conf:ro restart: unless-stopped volumes: pgdata: ``` Four-service stack, deterministic startup, every service has its restart policy and healthcheck, migrations gate api. ### Init container pattern (one-shot setup) ```yaml services: db-init: image: postgres:16 command: ["sh", "-c", "echo 'CREATE EXTENSION IF NOT EXISTS pg_stat_statements;' | psql -h db -U postgres"] depends_on: db: condition: service_healthy restart: "no" app: image: myapp depends_on: db-init: condition: service_completed_successfully ``` `db-init` is a one-shot job. It runs after db is healthy, performs setup, exits 0, then `app` starts. ### When the dep is external ```yaml services: db-check: image: alpine command: ["sh", "-c", "until nc -z external-db.aws.com 5432; do sleep 1; done"] restart: "no" api: image: myapp depends_on: db-check: condition: service_completed_successfully ``` A tiny check container that waits for an external dep, then exits. The `api` is gated on it.For the reviewerNote to the moderator (optional)Visible only to the moderator. Helps review go faster.