Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Як підняти залежності сервісів у docker-compose (наприклад, web після db)?». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Бери `depends_on` з `condition: service_healthy`** у Compose. Простий `depends_on: [db]` лише впорядковує старти container; з `condition: service_healthy` Compose чекає, поки healthcheck залежності пройде, перед стартом 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 ``` **Головне:** одного start-order недостатньо, started container не завжди ready. Умова `service_healthy` (з реальним healthcheck на dep) дає реальний readiness-gating.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**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](https://github.com/vishnubob/wait-for-it) або [dockerize](https://github.com/jwilder/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'нуто на нього.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.