Як підняти залежності сервісів у docker-compose (наприклад, web після db)?
Service-залежності у Docker Compose це більше, ніж start-order. «Started» dep рідко означає «ready приймати з'єднання». Правильний патерн: depends_on: condition: service_healthy з healthcheck на залежності.
Теорія
TL;DR
- Простий
depends_on: [db]= тільки start-order. Compose стартуєdbспочатку, потімapi. НЕ чекає, покиdbready. depends_on: db: condition: service_healthy= чекає, поки healthcheckdbпройде. Це те, що хочеш для проду.- Три умови:
service_started,service_healthy,service_completed_successfully(для one-off init container). - Без depends_on сервіси стартують паралельно. З ним Compose enforce'ить порядок.
- Для не-Compose оркестраторів та сама ідея існує: K8s
initContainers, Swarm dependency-via-healthcheck-on-routes.
Швидкий приклад
# 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"]$ docker compose up -d
[+] Running 4/4
✔ Container db Healthy 2.3s
✔ Container redis Started 2.4s
✔ Container migrate Exited 8.7s # покрутився, успіх, вийшов 0
✔ Container api Started 8.8s # чекав, поки migrate завершитьсяЧотири сервіси, три різні dep-умови, ідеальний порядок: db ready → migrate крутиться → migrate succeeds → api стартує (redis уже up).
Три умови
service_started (дефолт)
depends_on:
- db
# АБО явно:
depends_on:
db:
condition: service_startedЧекає, поки container стартує. НЕ чекає, поки застосунок всередині ready. Майже ніколи не те, що хочеш.
service_healthy
depends_on:
db:
condition: service_healthyЧекає, поки healthcheck dep пройде. Потребує, щоб у db був визначений healthcheck:. Правильний дефолт для runtime-залежностей.
Якщо у db немає healthcheck, це провалює старт dependent (Compose кидає «service has no healthcheck»).
service_completed_successfully
depends_on:
migrate:
condition: service_completed_successfullyЧекає, поки dep вийде з кодом 0. Використовується для one-shot init/migration container, вони крутяться, роблять роботу, виходять, потім головний сервіс стартує.
Чому depends_on сам по собі недостатньо
# НЕПРАВИЛЬНО: лише впорядковує старти; api стартує, поки postgres ще бутиться
services:
api:
depends_on:
- db
db:
image: postgres:16$ docker compose up
Container db Started
Container api Started # але db потребує ~3 секунди, щоб реально приймати з'єднання
# api б'є у db → Connection refused → api крашиться або retry'їтьБільшість баз потребують 1-5 секунд після «started», поки приймають запити. Сам depends_on цього не знає. Healthcheck знає.
App-level retry як safety-net
Навіть з service_healthy transient db-disconnect трапляються у runtime. Прод-застосунки мають retry'їти з backoff:
// Псевдокод
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 разом = надійний старт.
Типові помилки
Простий depends_on для реальної залежності
# НЕПРАВИЛЬНО
depends_on: [db]Стартує api до того, як db ready. Застосунок падає на першому з'єднанні.
Забути, що dep потребує healthcheck
services:
api:
depends_on:
db:
condition: service_healthy
db:
image: postgres:16
# без healthcheck:ERROR: ...service "db" has no healthcheck defined
Фікс: додай блок healthcheck: до db.
Wait-for скрипти не завжди потрібні
Ера до condition: service_healthy потребувала скриптів типу wait-for-it або dockerize:
CMD ["./wait-for-it.sh", "db:5432", "--", "node", "server.js"]З Compose v3+ і service_healthy вони не потрібні для inter-service start-order. Все ще корисні для зовнішніх залежностей (cloud DB через мережу).
Циклічні залежності
services:
a:
depends_on: [b]
b:
depends_on: [a]ERROR: dependency cycle detected
Compose відмовляє. Реальні dep-графи мають бути ациклічними.
Dependency дотримується лише при старті, не при рестарті одного сервісу
$ docker compose restart api
# Рестартує лише api. НЕ верифікує health db знову.Healthcheck-gated dep enforce'ить під час up. restart швидша операція. Зазвичай нормально; рідко foot-gun.
Реальні патерни
Патерн: db + migrator + app
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_healthydb healthy → migrate крутиться → migrate exit 0 → app стартує. Три умови, детермінований startup.
Патерн: web + api + db
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 dep-ланцюг, кожен gate healthcheck-driven.
Питання для поглиблення
Q: Чи depends_on працює між кількома Compose-файлами?
A: Так, depends_on резолвиться у merged-результаті усіх -f файлів. Посилання мають вказувати на сервіси, визначені десь у merged-config.
Q: Чи може сервіс залежати від кількох сервісів зі змішаними умовами?
A: Так. Бери map-форму, щоб поставити умови per-dep:
depends_on:
db: { condition: service_healthy }
redis: { condition: service_started }
migrate: { condition: service_completed_successfully }Q: Чи depends_on переноситься на docker compose run?
A: Так, Compose стартує deps, коли робиш docker compose run --rm api .... Вимкни через --no-deps.
Q: Що, якщо dep це зовнішній сервіс (як AWS RDS)?
A: Compose не може його health-check'нути. Або додай малий wrapper-container з healthcheck, що probe'ить external-сервіс, або довірся app-level retry.
Q: (Senior) Як це мапиться на Kubernetes?
A: Kubernetes не має depends_on для pod напряму. Еквіваленти: initContainers (крутяться перед головними container, мають успіх), readiness probes (gate-трафік до pod) і зовнішні оркестраційні tools (Argo Workflows тощо) для складних dep. Helm-чарти часто hardcoдять startup-order через deployments + services + readiness probes; явний dep-граф це Compose-flavored конвенція.
Приклади
Реальний прод-shape Compose
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:Чотири-сервісний стек, детермінований startup, кожен сервіс має restart-policy і healthcheck, міграції gate'ять api.
Init container patтерн (one-shot setup)
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_successfullydb-init це one-shot job. Крутиться після того, як db healthy, виконує setup, exit 0, потім app стартує.
Коли dep зовнішній
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Крихітний check-container, що чекає external-dep, потім виходить. api gate'нуто на нього.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.
Коментарі
Ще немає коментарів