depends_on і порядок запуску в Docker Compose
depends_on задає лише порядок запуску контейнерів, а не готовність сервісу. Розбираємо умови service_healthy, service_started і чому потрібен retry у застосунку.
depends_on задає порядок запуску, а не готовність
depends_on у Docker Compose задає порядок запуску контейнерів і більше нічого. Compose створить і стартує залежність раніше за той сервіс, який її оголосив. На цьому його робота закінчується. Він не знає, чи Postgres усередині контейнера вже ініціалізував каталог даних, чи Redis відкрив сокет, чи міграції доїхали до кінця.
Саме тому перший docker compose up на свіжому VPS виглядає так. Контейнер db стартує. Через частку секунди стартує app. Застосунок одразу пробує відкрити з'єднання з базою, отримує відмову на рівні TCP і завершує роботу. Ви запускаєте стек удруге, і все працює, бо каталог даних Postgres уже створений і база піднімається майже миттєво. Проблема при цьому нікуди не поділася. Вона просто перестала потрапляти у те вікно, коли застосунок стукає у двері.
Нижче розберемо, що depends_on уміє за поточною специфікацією Compose (Compose Specification), які три умови він підтримує, що кожна з них реально чекає, і чому навіть з найточнішою умовою повторні спроби з боку застосунку лишаються головним рішенням.
Стек, на якому це видно
Мінімальний compose.yaml, який відтворює падіння:
services:
app:
build: .
depends_on:
- db
environment:
DATABASE_URL: postgres://app:secret@db:5432/app
db:
image: postgres:17
environment:
POSTGRES_USER: app
POSTGRES_PASSWORD: secret
POSTGRES_DB: app
volumes:
- pgdata:/var/lib/postgresql/data
volumes:
pgdata:Якщо ви тільки збираєте перший стек на орендованому сервері, структура файлу і базові команди описані в гайді про Docker Compose на VPS. Тут нас цікавить рівно один рядок: depends_on зі списком, у якому лежить db.
Короткий синтаксис: просто список імен
Список імен сервісів це найстаріша і найпоширеніша форма. Вона робить дві речі. По-перше, будує граф залежностей: Compose стартує db, і лише потім app. По-друге, тягне залежність за собою, тобто db буде піднято, навіть якщо ви просили підняти тільки app.
Імена в списку мають існувати в тому самому наборі сервісів. Помилка в імені це помилка конфігурації, а не мовчазне ігнорування, тому docker compose config ловить її ще до запуску. Граф має бути ациклічним: якщо app залежить від db, а db від app, Compose відмовиться будувати порядок. Залежності транзитивні, тому ланцюжок app залежить від migrate, а migrate від db працює так, як виглядає.
Важливо, чим цей список не є. У короткій формі Compose вважає залежність виконаною тієї ж миті, коли контейнер перейшов у запущений стан. Процес усередині ще тільки отримав керування. Між словами "контейнер запущено" і "сервіс готовий приймати запити" може бути п'ять секунд, а на першому старті Postgres із порожнім томом і всі тридцять.
Довга форма з condition
Щоб чекати не на контейнер, а на щось осмисленіше, depends_on пишуть як мапу, де кожен ключ це ім'я сервісу, а значення це об'єкт з полем condition:
services:
app:
build: .
depends_on:
db:
condition: service_healthy
cache:
condition: service_started
migrate:
condition: service_completed_successfullyДовга форма підтримує ще два поля. restart: true каже Compose перезапустити залежний сервіс після того, як Compose оновив або перезапустив його залежність, наприклад під час docker compose up зі зміненим образом бази. required: false перетворює жорстку вимогу на м'яку: якщо такий сервіс не піднявся, Compose не зупиняє весь стек. Це зручно для необов'язкової телеметрії чи панелі на кшталт Portainer поруч зі стеком, без якої застосунок цілком живе.
depends_on:
db:
condition: service_healthy
restart: true
metrics:
condition: service_started
required: falseЗвертайте увагу на форму запису. Короткий синтаксис це список під дефісами, довгий це вкладена мапа без дефісів. Змішати їх в одному блоці не вийде, і YAML тут скаржиться цілком зрозуміло.
Три умови і на що кожна чекає
service_startedчекає лише на те, що контейнер залежності перейшов у запущений стан. Це поведінка за замовчуванням, тобто рівно те саме, що дає короткий список. Підходить для сервісів, яким байдуже, коли саме співрозмовник відповість.service_healthyчекає, поки healthcheck залежності почне повідомляти про справний стан. Це єдина умова, яка щось знає про процес усередині контейнера.service_completed_successfullyчекає, поки контейнер залежності відпрацює і завершиться без помилки. Призначена для разових задач, а не для довгоживучих сервісів: сервіс, який працює постійно, ніколи не задовольнить цю умову.
Ось чому вибір між ними майже завжди зводиться до одного питання. Вам потрібен порядок чи готовність? Порядок дає service_started. Готовність дає тільки service_healthy, і лише настільки, наскільки чесний ваш healthcheck.
Щоб service_healthy щось означав, потрібен healthcheck
service_healthy не перевіряє базу самостійно. Вона читає стан, який Docker обчислює за блоком healthcheck у самій залежності. Якщо в сервісі db немає блоку healthcheck і його немає в образі, читати нема чого, і умова перетворюється на порожню обіцянку. Спочатку пишеться перевірка, потім на неї посилаються.
db:
image: postgres:17
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app -d app -h 127.0.0.1"]
interval: 5s
timeout: 3s
retries: 10
start_period: 30sТут ховається пастка, через яку service_healthy часто "спрацьовує", а застосунок усе одно падає. Офіційний образ Postgres під час першої ініціалізації піднімає тимчасовий сервер, доступний лише через локальний сокет, і саме в цей момент наївна перевірка вже може звітувати про готовність, хоча TCP-порт ще закритий для мережі Compose. Тому в прикладі вище стоїть -h 127.0.0.1. Деталі того, як правильно скласти саму перевірку і підібрати start_period, розібрані в матеріалі про healthcheck у Docker Compose, а конкретно для бази в healthcheck для Postgres. Тут достатньо запам'ятати правило: якість service_healthy дорівнює якості healthcheck, на який вона дивиться.
service_completed_successfully: місце для міграцій
Третя умова закриває сценарій, який раніше вирішували скриптами очікування. Разова задача, наприклад міграції схеми, оформлюється окремим сервісом, і застосунок стартує лише після того, як вона завершилася:
services:
migrate:
build: .
command: ["./manage.py", "migrate"]
restart: "no"
depends_on:
db:
condition: service_healthy
app:
build: .
depends_on:
migrate:
condition: service_completed_successfullyРядок restart: "no" тут не косметика. Політика перезапуску на кшталт unless-stopped бореться з самою ідеєю разової задачі: контейнер завершується, Docker піднімає його знову, і умова завершення ніколи не виконується стабільно. Значення no в YAML обов'язково береться в лапки, інакше воно читається як булеве значення.
Зверніть увагу на ланцюжок. migrate чекає на здорову базу, app чекає на завершені міграції. Порядок запуску вибудовується з двох простих правил, а не з одного складного.
Що робить depends_on, коли ви піднімаєте один сервіс
Це перше, що дивує на практиці. docker compose up -d app не піднімає лише app. Compose бере граф залежностей і стартує все, що в ньому є нижче, включно з базою і разовими задачами. Так само поводиться docker compose run.
Щоб цього уникнути, є прапорець --no-deps:
docker compose up -d --no-deps appЦе корисно, коли база вже працює і ви просто перекочуєте застосунок на новий образ. Але майте на увазі: з --no-deps жодна умова не перевіряється взагалі, тобто ви свідомо вимикаєте страхування. Повний набір випадків, коли варто піднімати один сервіс, а коли весь стек, розібраний у окремому пості про запуск одного сервісу.
Порядок зупинки дзеркальний
depends_on впливає не лише на старт. Зупинка йде у зворотному напрямку: спочатку зупиняється той, хто залежить, потім його залежність. Для стека з базою це саме те, що треба, бо застосунок встигає закрити з'єднання до того, як Postgres отримає сигнал.
Це стосується і docker compose stop, і docker compose down. Різниця між ними у тому, що лишається на диску після команди, і вона описана в порівнянні down і stop. Порядок в обох випадках однаковий.
Один нюанс: зворотний порядок не означає нескінченного очікування. Кожен контейнер отримує сигнал завершення і має обмежений час на вихід, після чого його зупиняють жорстко. Якщо ваш застосунок довго закриває пул з'єднань, збільшуйте stop_grace_period у самому сервісі, а не сподівайтеся на граф залежностей.
Чому retry у застосунку лишається основним рішенням
depends_on працює рівно один раз, під час запуску стека. Після цього він не існує. З цього випливає все інше.
База перезапускається не тільки на старті. Її вб'є OOM killer при нестачі пам'яті, її перезапустить політика restart, її зупинить оновлення образу. У всіх цих випадках app уже працює, Compose нічого не перебудовує, і застосунок просто бачить розірване з'єднання. Жодна умова в depends_on цього не покриває, а restart: true реагує тільки на дії самого Compose, не на падіння контейнера.
Здоровий healthcheck теж не дорівнює готовності до вашого конкретного запиту. Перевірка каже, що процес відповідає. Вона не каже, що потрібна база створена, що розширення встановлене, що пул не вичерпаний.
Після перезавантаження VPS порядок відновлення визначає systemd і політика перезапуску контейнерів, а не ваш YAML. Як налаштувати цю частину, описано в матеріалі про автозапуск стека після перезавантаження.
Висновок простий. Код, який підключається до бази, має вміти повторити спробу із наростаючою паузою і завершитися з помилкою лише після розумного ліміту. Більшість фреймворків мають це в налаштуваннях пулу, решті достатньо циклу на п'ять рядків. depends_on із service_healthy при цьому не зайвий: він прибирає шум на першому старті і робить логи читабельними. Він просто не є гарантією.
Де depends_on не діє
У режимі Swarm через docker stack deploy поле depends_on ігнорується. Оркестратор там будує порядок сам, тому переносити стек із Compose у Swarm без повторних спроб у застосунку не варто.
Звичайний docker run про файл Compose не знає взагалі, тому жодного порядку там немає.
І ще раз про --no-deps: цей прапорець вимикає не лише підняття залежностей, а й перевірку умов.
Як перевірити порядок запуску у себе
Спочатку подивіться на розгорнуту конфігурацію. Ця команда нічого не запускає, вона показує підсумковий файл після підстановки змінних і злиття кількох файлів:
docker compose configПотім підніміть стек з чистого стану і читайте логи в реальному часі, без -d. Зупиняється він тоді звичайним Ctrl+C:
docker compose down -v
docker compose upПрочитайте, у якому порядку з'являються перші рядки кожного сервісу і який саме момент застосунок вважає початком роботи. Паралельно в другій сесії SSH подивіться, який стан Compose показує для кожного сервісу і чи бачить він результат healthcheck:
docker compose psЩоб перевірити конкретно умову service_healthy, приберіть блок healthcheck із бази, підніміть стек і порівняйте поведінку з тією, що була з перевіркою. Різницю видно одразу, і вона пояснює, чому умова без healthcheck нічого не варта.
FAQ
Чи чекає depends_on, поки база почне приймати з'єднання?
Сам по собі ні. У короткій формі, тобто у вигляді списку імен, depends_on вважає залежність виконаною одразу після того, як контейнер перейшов у запущений стан. Процес усередині в цей момент тільки стартує. Чекати на готовність уміє лише довга форма з condition: service_healthy, і лише за умови, що в сервісі-залежності описаний блок healthcheck.
Чим короткий синтаксис depends_on відрізняється від довгого?
Короткий це список імен сервісів. Він рівносильний довгій формі з condition: service_started для кожного імені. Довга форма записується як мапа і дозволяє задати умову для кожної залежності окремо, а також поля restart і required. Змішувати список і мапу в одному блоці не можна.
Чи підніме docker compose up -d app його залежності?
Так. Compose бере граф залежностей і стартує все, від чого app залежить, разом із транзитивними залежностями. Якщо потрібно підняти рівно один сервіс, додайте --no-deps, але пам'ятайте, що цей прапорець вимикає і перевірку умов, тому застосунок може стартувати при непіднятій базі.
Чи впливає depends_on на порядок зупинки контейнерів?
Так, зупинка йде у зворотному порядку до запуску: спочатку зупиняється сервіс, який залежить, потім його залежність. Це стосується і docker compose stop, і docker compose down. Якщо вашому застосунку потрібно більше часу на коректне закриття з'єднань, збільшуйте stop_grace_period у цьому сервісі.