Skip to main content

Як підняти залежності сервісів у 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. НЕ чекає, поки db ready.
  • depends_on: db: condition: service_healthy = чекає, поки healthcheck db пройде. Це те, що хочеш для проду.
  • Три умови: service_started, service_healthy, service_completed_successfully (для one-off init container).
  • Без depends_on сервіси стартують паралельно. З ним Compose enforce'ить порядок.
  • Для не-Compose оркестраторів та сама ідея існує: K8s initContainers, Swarm dependency-via-healthcheck-on-routes.

Швидкий приклад

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 # покрутився, успіх, вийшов 0 ✔ Container api Started 8.8s # чекав, поки migrate завершиться

Чотири сервіси, три різні dep-умови, ідеальний порядок: db ready → migrate крутиться → migrate succeeds → api стартує (redis уже up).

Три умови

service_started (дефолт)

yaml
depends_on: - db # АБО явно: depends_on: db: condition: service_started

Чекає, поки container стартує. НЕ чекає, поки застосунок всередині ready. Майже ніколи не те, що хочеш.

service_healthy

yaml
depends_on: db: condition: service_healthy

Чекає, поки healthcheck dep пройде. Потребує, щоб у db був визначений healthcheck:. Правильний дефолт для runtime-залежностей.

Якщо у db немає healthcheck, це провалює старт dependent (Compose кидає «service has no healthcheck»).

service_completed_successfully

yaml
depends_on: migrate: condition: service_completed_successfully

Чекає, поки dep вийде з кодом 0. Використовується для one-shot init/migration container, вони крутяться, роблять роботу, виходять, потім головний сервіс стартує.

Чому depends_on сам по собі недостатньо

yaml
# НЕПРАВИЛЬНО: лише впорядковує старти; api стартує, поки postgres ще бутиться services: api: depends_on: - db db: image: postgres:16
bash
$ 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:

js
// Псевдокод 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 для реальної залежності

yaml
# НЕПРАВИЛЬНО depends_on: [db]

Стартує api до того, як db ready. Застосунок падає на першому з'єднанні.

Забути, що dep потребує healthcheck

yaml
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:

dockerfile
CMD ["./wait-for-it.sh", "db:5432", "--", "node", "server.js"]

З Compose v3+ і service_healthy вони не потрібні для inter-service start-order. Все ще корисні для зовнішніх залежностей (cloud DB через мережу).

Циклічні залежності

yaml
services: a: depends_on: [b] b: depends_on: [a]
ERROR: dependency cycle detected

Compose відмовляє. Реальні dep-графи мають бути ациклічними.

Dependency дотримується лише при старті, не при рестарті одного сервісу

bash
$ docker compose restart api # Рестартує лише api. НЕ верифікує health db знову.

Healthcheck-gated dep enforce'ить під час up. restart швидша операція. Зазвичай нормально; рідко foot-gun.

Реальні патерни

Патерн: 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 крутиться → migrate exit 0 → app стартує. Три умови, детермінований startup.

Патерн: 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 dep-ланцюг, кожен gate healthcheck-driven.

Питання для поглиблення

Q: Чи depends_on працює між кількома Compose-файлами?


A: Так, depends_on резолвиться у merged-результаті усіх -f файлів. Посилання мають вказувати на сервіси, визначені десь у merged-config.

Q: Чи може сервіс залежати від кількох сервісів зі змішаними умовами?


A: Так. Бери map-форму, щоб поставити умови per-dep:

yaml
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

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:

Чотири-сервісний стек, детермінований startup, кожен сервіс має restart-policy і healthcheck, міграції gate'ять api.

Init container patтерн (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 це one-shot job. Крутиться після того, як db healthy, виконує setup, exit 0, потім app стартує.

Коли dep зовнішній

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

Крихітний check-container, що чекає external-dep, потім виходить. api gate'нуто на нього.

Коротка відповідь

Для співбесіди
Premium

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.

Коментарі

Ще немає коментарів