09:12. Ваш деплой «зелений», бо контейнери запущені, але API повертає 500. Логи показують відмову з’єднання до Postgres. Ви додаєте depends_on, деплоїте знову, і… нічого не змінюється, хіба що ваша впевненість падає.
Це — пастка залежностей у Docker Compose: depends_on керує порядком запуску, а не готовністю. Це зручна фіча, а не контракт на надійність. Якщо ви будете трактувати її як такий, система рано чи пізно навчить вас скромності — зазвичай під час демо.
Що depends_on насправді робить (і чого воно ніколи не обіцяло)
Compose має вирішити порядок запуску контейнерів. Ось і все, що робить depends_on: орієнтований граф, який каже «запусти A перед B». Воно не каже «A приймає з’єднання», «A завершив міграції», «A прогрів кеші» або «A не впаде за дві секунди».
Коли люди кажуть «depends_on не працює», вони зазвичай мають на увазі: воно працювало точно так, як спроєктовано, а дизайн не відповідав їхнім очікуванням.
Порядок запуску проти готовності: чітке розмежування
- Порядок запуску: процес контейнера було запущено (або принаймні Docker намагався його запустити).
- Готовність: сервіс придатний для використання за призначенням (слухає сокет, успішна автентифікація, наявна схема, досяжні апстріми тощо).
Це різні проблеми, і Compose за замовчуванням вирішує лише першу.
Нюанс «service_healthy» (і чому це не універсальне вирішення)
Деякі реалізації Compose підтримують умовні залежності, наприклад condition: service_healthy, що блокує запуск залежного сервісу до проходження healthcheck залежності. Це корисно, але все ще не повний контракт:
- Healthchecks можуть бути хибними (занадто поверхневі, надто повільні або надто оптимістичні).
- Одного разу «healthy» не означає, що сервіс буде здоровим завжди.
- Ваш додаток все одно потребує повторних спроб, бо мережі й сховища не зважають на ваш YAML.
Операційна правда така: навіть з healthchecks, ви проектуєте додаток так, ніби залежності можуть запізнюватися, флапати або бути тимчасово недоступними. Compose допомагає оркеструвати; воно не звільняє вас від стійкості.
Цитата, яка заслуговує на місце на стіні: перефразована думка Werner Vogels: «Все ламається постійно; будуйте системи, які цього очікують.»
Чому «готовність» складна: що відбувається під час старту
На чистому ноутбуку з розігрітими кешами та без навантаження легко повірити, що готовність миттєва. У реальних середовищах старт — це плутанина I/O, DNS, планування CPU, латентності сховища та інколи неприємний fsck, якого ви не просили.
Типові фази ініціалізації залежностей (те, про що ви забуваєте)
- Контейнер створено: змонтовані шари файлової системи, налаштовані неймспейси, приєднано мережу.
- Entrypoint запускається: процес починає працювати; може форкатися; може чекати на шаблони конфігів.
- Сервіс ініціалізується: читає конфіг, виділяє пам’ять, перевіряє права.
- Готовність сховища: монтування томів, відтворення журналу, відновлення після збою, відтворення WAL.
- Готовність мережі: розповсюдження DNS, прив’язка сервісу до сокетів, правила фаєрволу.
- Готовність застосунку: міграції, прогрів кешу, ініціалізація даних, вибір лідера.
Будь-яка з цих фаз може відтермінувати «готовність» на мілісекунди або хвилини. І так, я бачив хвилини.
Жарт №1: «Працювало на моїй машині» — це інша версія «у моєї машини нижчі вимоги».
Сховище ускладнює ситуацію (особливо при першому запуску)
Бази даних не «працюють», коли процес існує; вони працюють, коли можуть прийняти з’єднання й надійно обробляти запити. Postgres може відтворювати WAL. MySQL може оновлювати системні таблиці. Redis може завантажувати RDB-сніпшот. Якщо ви використовуєте мережеве сховище — додається ще один шар часової варіабельності.
Для SRE важливо моделювати доступність залежностей як стохастичну, а не детерміністичну. Ваш додаток або коректно це обробляє, або стає джерелом дзвінків тривоги.
Факти та історичний контекст (бо це не сталося випадково)
Трохи контексту корисно, бо поведінка Compose часто плутається з семантикою Swarm/Kubernetes, а екосистема еволюціонувала неідеальними кроками. Ось конкретні факти, що важливі з операційної точки зору:
- Початкова мета Compose — ергономіка для розробника, а не оркестрація високої доступності. Воно оптимізоване для «запустити стек локально», а не «керувати провалами».
depends_onісторично лише забезпечував порядок запуску. Готовність була явно поза сферою відповідальності довгий час, бо це залежить від конкретного застосунку.- Healthchecks з’явилися в Docker пізніше, ніж багато хто думає; ранні Compose-конфіги використовували ad-hoc скрипти «wait-for», бо не було нативного примітиву.
- Swarm і Kubernetes популяризували поняття health/readiness, через що команди стали очікувати подібної семантики скрізь — навіть там, де її немає.
HEALTHCHECKDocker запускається всередині неймспейсу контейнера, що зручно для перевірки внутрішнього стану сервісу, але може пропустити проблеми зовнішньої досяжності.- Compose v2 — це плагін, а не старий Python-бінарник; деталі реалізації і підтримувані фічі різняться в різних середовищах, що підживлює плутанину «в мене працює».
- Порядок запуску ≠ порядок перезапуску; залежність може впасти й повернутися пізніше, і Compose не переструктурує світ за вас автоматично.
- DNS в мережах Compose зазвичай стабільний, але не миттєвий; ранні спроби підключення можуть падати з помилками резолвінгу на швидких клієнтах.
- «DB приймає TCP» ≠ «схема готова»; міграції можуть ще виконуватися й викликати таймаути або помилки відсутніх таблиць.
Ось у чому пастка: межа інструмента вже ширша, ніж операційна проблема, а наш мозок домальовує відсутні фічі.
Режими відмов, які ви побачите в продакшені (навіть якщо клянетесь, що ні)
1) Відмова з’єднання під час старту, потім «раптом» працює
Контейнер бази запускається швидко. Процес БД прив’язується пізніше. Ваш додаток робить одну спробу, зривається й виходить. Compose перезапускає його, або ви робите це. При другій спробі все працює.
Діагностика: додаток не має retry/backoff, і ви сплутали порядок запуску з готовністю сервісу.
2) «No such host» або транзієнтні помилки DNS
Швидкі клієнти можуть намагатися резолвити ім’я сервісу до того, як вбудований DNS повністю готовий або мережа приєднана. Зараз це трапляється рідше, але все ще відбувається під навантаженням або на повільних вузлах.
3) Відсутня схема / міграції працюють
Postgres приймає з’єднання, але ваш job міграцій ще не виконався. Додаток стартує, виконує запити й падає. Ви отримуєте короткий простій і купу марних алертів.
4) Контейнер здоровий, система — ні
Ваш DB healthcheck використовує pg_isready, яке повертає успіх. Але диск повний, база в режимі «лише читання» або всі підключення вичерпані. Healthchecks хороші настільки, наскільки добре вони визначені.
5) Зворотний тиск і таймаути, що маскуються під проблеми старту
Ваша залежність «працює», але вкрай повільна: холодні кеші, високий I/O wait, CPU steal. Додаток витрачає час на таймаут і виходить, і всі звинувачують Compose, бо воно поруч.
Жарт №2: depends_on — це як сказати «я прийшов у ресторан першим» і припустити, що вечеря вже приготовлена.
Патерни, що працюють: healthchecks, повторні спроби та розумна послідовність
Якщо ви хочете надійності, потрібні шарування. Compose може допомогти, але додаток має виконувати дорослу частину: повторювати спроби, робити backoff і завершуватися контрольовано.
Патерн A: Додайте явні healthchecks для залежностей
Для популярних сервісів визначте healthcheck, який тестує змістовну готовність. Не тільки «процес існує». Краще — реальний запит або ping, що перевіряє відповідний підсистем.
Приклад: healthcheck для Postgres, що валідує TCP, автентифікацію й базовий запит:
cr0x@server:~$ cat docker-compose.yml
services:
db:
image: postgres:16
environment:
POSTGRES_PASSWORD: example
POSTGRES_USER: app
POSTGRES_DB: appdb
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app -d appdb -h 127.0.0.1 || exit 1"]
interval: 5s
timeout: 3s
retries: 20
start_period: 10s
api:
image: myorg/api:latest
depends_on:
db:
condition: service_healthy
Операційне зауваження: навіть якщо ви блокуєте запуск на підставі health, вам все одно потрібні client-side retries для перезапусків, failover-ів і переривань під час виконання.
Патерн B: Зробіть додаток стійким (повторні спроби з backoff)
Найкраще місце для обробки готовності залежності — клієнт. Ваш API має терпіти запізнення БД на 30–120 секунд без завершення. Нехай логуватиме чітко, повторює спроби з експоненційним backoff, і має окремий сигнал liveliness, щоб не приймати трафік передчасно.
Коли люди ухиляються від цього через «це приховує помилки», вони насправді мають на увазі «я віддаю перевагу простоям перед повільними стартами». Ви все одно можете сигналити про повільну готовність; вам не потрібно створювати crash-loop, щоб щось відчути.
Патерн C: Відокремте міграції/ініціалізацію від запуску додатку
Запускайте міграції як одноразову job, яка блокує завершення деплою, а не старт додатку. В термінах Compose це може бути окремий сервіс, що запускається явно, або entrypoint, що робить міграції з локом і зрозумілою спостережливістю.
Не запускайте міграції одночасно в 10 репліках додатку, якщо вам не подобаються помилки на кшталт «relation already exists» і суперечки про те, хто стартував першим.
Патерн D: Використовуйте політики перезапуску усвідомлено
restart: always може маскувати реальні проблеми, перетворюючи їх на вічний crash-loop. Іноді це прийнятно під час ініціалізації; це неприйнятно як режим стабільної роботи.
Мій вибір для більшості сервісів:
- Використовуйте
restart: unless-stoppedдля довготривалих сервісів у dev/test. - У production-подібних середовищах поєднуйте рестарт з логуванням, backoff у застосунку й чіткими healthchecks, щоб «перезапуск» не ставав «роботою».
Практичні завдання: команди, виводи та рішення, які ви приймаєте
Цей розділ спеціально практичний. Це завдання, які ви виконуєте о 02:00, коли намагаєтеся відповісти на одне питання: що саме не готове, і чому?
Завдання 1: Підтвердіть, що Compose вважає запущеним
cr0x@server:~$ docker compose ps
NAME IMAGE COMMAND SERVICE STATUS PORTS
stack-db-1 postgres:16 "docker-entrypoint.s…" db running (healthy) 5432/tcp
stack-api-1 myorg/api:latest "/bin/api" api running 0.0.0.0:8080->8080/tcp
Що це означає: Контейнери запущені; DB позначено як «healthy» згідно з healthcheck.
Рішення: Якщо API все ще повертає помилки, це не проблема порядку запуску; переходьте до логів і реальних тестів підключення.
Завдання 2: Перегляньте граф залежностей і злитий конфіг
cr0x@server:~$ docker compose config
services:
api:
depends_on:
db:
condition: service_healthy
image: myorg/api:latest
db:
environment:
POSTGRES_DB: appdb
POSTGRES_PASSWORD: example
POSTGRES_USER: app
healthcheck:
interval: 5s
retries: 20
start_period: 10s
test:
- CMD-SHELL
- pg_isready -U app -d appdb -h 127.0.0.1 || exit 1
timeout: 3s
image: postgres:16
Що це означає: Ви валідуєте те, що Compose реально запустить (після merge-ів, override-ів і підстановки середовища).
Рішення: Якщо тут відсутній healthcheck або умова залежності — ви дебагуєте не той файл або не ту реалізацію Compose.
Завдання 3: Читайте логи API з таймстемпами
cr0x@server:~$ docker compose logs --timestamps --tail=200 api
api-1 2026-02-04T08:12:09.441Z ERROR db connect failed: dial tcp 172.22.0.2:5432: connect: connection refused
api-1 2026-02-04T08:12:09.443Z INFO exiting with code 1
api-1 2026-02-04T08:12:11.012Z INFO starting api version=1.9.3
Що це означає: Клієнт спробував один раз і вийшов. Класична поведінка «без retry/backoff».
Рішення: Працюйте над логікою старту додатку, не над Compose. Додайте повторні спроби і завершуйтеся жорстко лише після обмеженого часу.
Завдання 4: Читайте логи DB під час ініціалізації
cr0x@server:~$ docker compose logs --timestamps --tail=200 db
db-1 2026-02-04T08:12:03.118Z PostgreSQL init process complete; ready for start up.
db-1 2026-02-04T08:12:04.002Z database system is ready to accept connections
Що це означає: DB була готова о 08:12:04Z, але API спробував о 08:12:09Z і отримав відмову. Такий розбіжний випадок вказує на мережу, неправильну адресу або перезапуск БД.
Рішення: Перевірте ціль з’єднання API (host, port, TLS) і протестуйте підключення зсередини неймспейсу мережі.
Завдання 5: Перевірте DNS і досяжність мережі з контейнера API
cr0x@server:~$ docker compose exec api getent hosts db
172.22.0.2 db
Що це означає: Резолвінг DNS всередині мережі Compose працює.
Рішення: Якщо це не вдається, у вас проблема приєднання до мережі або ім’я сервісу некоректне (не та мережа, не та назва сервісу або контейнер не в тій же мережі).
Завдання 6: Перевірте TCP-з’єднання до БД з контейнера API
cr0x@server:~$ docker compose exec api bash -lc 'timeout 2 bash -lc "
Що це означає: TCP доступний зараз.
Рішення: Якщо TCP добре, але автентифікація/запит падає — ваш пробник готовності має бути глибшим, ніж «порт відкритий».
Завдання 7: Виконайте реальний SQL-запит з контейнера API
cr0x@server:~$ docker compose exec api bash -lc 'PGPASSWORD=example psql -h db -U app -d appdb -c "select 1;"'
?column?
----------
1
(1 row)
Що це означає: Шлях автентифікації і базовий запит працюють.
Рішення: Якщо ваш додаток все ще повертає помилки, проблема, ймовірно, в конфігурації додатку (неправильний DSN), міграціях або лімітах пулу з’єднань — не в Compose.
Завдання 8: Перевірте деталі здоров’я контейнера (не гадіть)
cr0x@server:~$ docker inspect --format '{{json .State.Health}}' stack-db-1
{"Status":"healthy","FailingStreak":0,"Log":[{"Start":"2026-02-04T08:12:25.011Z","End":"2026-02-04T08:12:25.042Z","ExitCode":0,"Output":"/var/run/postgresql:5432 - accepting connections\n"}]}
Що це означає: Healthcheck повертає «accepting connections». Це не перевіряє готовність схеми або права користувача API понад простого з’єднання.
Рішення: Якщо вам потрібна готовність схеми, створіть пробник, що перевіряє наявність відомої таблиці або версії міграцій.
Завдання 9: Перевірте цикли перезапуску та коди виходу
cr0x@server:~$ docker compose ps --all
NAME SERVICE STATUS
stack-api-1 api restarting (1) 3 seconds ago
stack-db-1 db running (healthy)
Що це означає: API перебуває в crash-loop. Це не «очікування», це помилка й перезапуск.
Рішення: Призупиніть цикл, щоб зберегти логи/стан, потім виправте негайну причину. Crash-loop може також DoS-нути вашу залежність.
Завдання 10: Підтвердіть змінні оточення та фактичний DSN
cr0x@server:~$ docker compose exec api env | egrep 'DATABASE_URL|PGHOST|PGPORT|PGUSER'
DATABASE_URL=postgres://app:example@db:5432/appdb?sslmode=disable
Що це означає: Додаток налаштований підключатися до db всередині мережі Compose, а не до localhost.
Рішення: Якщо ви бачите localhost тут — це ваша помилка. У контейнерах localhost вказує на сам контейнер, а не на сервіс БД.
Завдання 11: Виявіть вузькі місця ресурсів на хості (CPU, пам’ять, IO)
cr0x@server:~$ docker stats --no-stream
CONTAINER ID NAME CPU % MEM USAGE / LIMIT MEM % NET I/O BLOCK I/O
a12b3c4d5e6f stack-db-1 215.32% 1.2GiB / 2GiB 60.00% 2.1MB / 3.4MB 1.2GB / 900MB
b98c7d6e5f4a stack-api-1 0.32% 55MiB / 512MiB 10.74% 800KB / 700KB 12MB / 4MB
Що це означає: DB завантажує CPU і здійснює багато блокових I/O. Це може затримувати готовність і спричиняти таймаути клієнтів.
Рішення: Якщо DB навантажений під час старту, налаштуйте start_period, збільшіть таймаути і розгляньте продуктивність сховища (тип тому, конкуренція за I/O на хості).
Завдання 12: Доведіть, що проблема в таймінгу старту, затримавши запуск додатку
cr0x@server:~$ docker compose stop api
[+] Stopping 1/1
✔ Container stack-api-1 Stopped
cr0x@server:~$ sleep 15
cr0x@server:~$ docker compose start api
[+] Starting 1/1
✔ Container stack-api-1 Started
Що це означає: Якщо це «вирішує» проблему, ви підтвердили boot race.
Рішення: Не лишайте sleep в конфігу. Впровадьте gating готовності (healthcheck + condition) і client-side retries.
Завдання 13: Перевірте хронологію подій, щоб зафіксувати перезапуски й переходи здоров’я
cr0x@server:~$ docker events --since 10m --filter 'container=stack-db-1' --filter 'container=stack-api-1'
2026-02-04T08:12:01.004Z container create a12b3c4d5e6f (name=stack-db-1)
2026-02-04T08:12:01.210Z container start a12b3c4d5e6f (name=stack-db-1)
2026-02-04T08:12:04.120Z container health_status: healthy a12b3c4d5e6f (name=stack-db-1)
2026-02-04T08:12:04.300Z container start b98c7d6e5f4a (name=stack-api-1)
2026-02-04T08:12:09.443Z container die b98c7d6e5f4a (name=stack-api-1, exitCode=1)
Що це означає: Ви отримуєте точну хронологію: DB стала healthy перед стартом API, але API все одно впав. Це вказує не на наївну проблему готовності, а на конфіг, права, TLS або те, що healthcheck не тестував.
Рішення: Розширіть healthcheck або додайте явне логування готовності в додаток, яке вказує, за чим саме він чекає.
Завдання 14: Перевірте монтування томів і права для stateful залежностей
cr0x@server:~$ docker compose exec db bash -lc 'ls -ld /var/lib/postgresql/data; df -h /var/lib/postgresql/data | tail -1'
drwx------ 19 postgres postgres 4096 Feb 4 08:12 /var/lib/postgresql/data
overlay 80G 78G 2.0G 98% /
Що це означає: Диск зайнятий на 98%. Postgres може «запуститися», пройти примітивний healthcheck, а потім некоректно працювати під навантаженням записів.
Рішення: Розглядайте тиск диска як вимогу готовності для stateful сервісів. Виправте ємність перед тим, як міняти YAML.
Швидкий плейбук діагностики
Якщо ви запам’ятаєте лише один розділ — запам’ятайте цей. Коли стек Compose «запускається», але не працює, ви хочете швидко знайти вузьке місце — не вигадувати фанфік про depends_on.
Перше: визначте клас відмови (crash-loop додатку vs додаток запущений, але працює неправильно)
cr0x@server:~$ docker compose ps
NAME SERVICE STATUS
stack-api-1 api restarting (1) 2 seconds ago
stack-db-1 db running (healthy)
Якщо restart loop: зосередьтеся на логах додатку і причині виходу. Не ганяйтеся за готовністю, поки не з’ясуєте, що саме падає.
Друге: читайте логи з обох боків, вирівняні за часом
cr0x@server:~$ docker compose logs --timestamps --tail=100 api
...application errors...
cr0x@server:~$ docker compose logs --timestamps --tail=100 db
...db startup and readiness...
Рішення: Якщо DB явно не була готова, коли API спробував — потрібен gating або retries. Якщо DB була готова — причина, ймовірно, в конфігурації/автентифікації/схемі/ресурсах.
Третє: тестуйте зсередини неймспейсу мережі контейнера, що падає
cr0x@server:~$ docker compose exec api getent hosts db
...ip...
cr0x@server:~$ docker compose exec api bash -lc 'timeout 2 bash -lc "
Рішення: DNS fail → проблеми з мережею. TCP fail → залежність не слухає або неправильний порт. TCP OK, але додаток падає → автентифікація/схема/TLS/таймаути/пул з’єднань.
Четверте: шукайте конкуренцію ресурсів на хості
cr0x@server:~$ docker stats --no-stream
...cpu/mem/io...
Рішення: Якщо DB завантажена I/O, ви побачите «флапи» готовності, бо світ повільний, а не тому, що YAML неправильний.
П’яте: валідуйте, що ви насправді запускаєте
cr0x@server:~$ docker compose config
...resolved config...
Рішення: Якщо конфіг не той, що ви очікували — зупиніться. Виправте джерело істини (не той файл, неправильний override, невірне середовище), перш ніж дебагувати симптоми.
Поширені помилки: симптом → корінь проблеми → виправлення
1) Симптом: API миттєво завершується з «connection refused»
Корінь проблеми: Клієнт робить одну спробу під час старту, швидко зривається і виходить. depends_on не допоміг, бо не чекає готовності.
Виправлення: Додайте retry/backoff у додаток або блокуйте старт на підставі змістовного healthcheck (service_healthy) плюс client-side обмежений час очікування.
2) Симптом: API резолвить «db», але не може підключитися
Корінь проблеми: DB слухає на іншому порту, прив’язана до іншої адреси або сама перебуває в crash-loop. Іноді DB стартує, а потім перезапускається через корупцію сховища або конфіг.
Виправлення: Перевірте логи DB, інспектуйте binding портів, забезпечте, щоб DB слухала на очікуваному інтерфейсі. Тестуйте TCP з контейнера API.
3) Симптом: Працює з другої спроби; падає при чистому деплої
Корінь проблеми: Boot race. Система випадково залежить від таймінгу.
Виправлення: Не «виправляйте» це ручними перезапусками. Робіть готовність явною з healthchecks і клієнтськими retries. Додайте дедлайн старту, щоб падіння було гучним, якщо воно справді невідновне.
4) Симптом: «relation does not exist» або «table not found» під час старту
Корінь проблеми: Міграції не завершені, коли додаток стартує, або кілька реплік одночасно виконують міграції.
Виправлення: Запускайте міграції як окремий крок/джоб. Якщо робите їх із додатку, використовуйте advisory locks або патерн single-runner і чітко логгуйте стан міграцій.
5) Симптом: Healthcheck DB — healthy, але додаток таймаутить
Корінь проблеми: Healthcheck перевіряє поверхневу умову (відкритий сокет), але не продуктивність, автентифікацію або готовність схеми. Або DB перевантажена (CPU/IO) і повільна.
Виправлення: Зробіть healthcheck змістовним (наприклад, реальний запит). Обережно збільшуйте таймаути. Дослідіть конкуренцію ресурсів і латентність сховища.
6) Симптом: Все «up», але запити періодично падають
Корінь проблеми: Переривання в середині польоту залежностей, виснаження пулу з’єднань, епізодичні DNS/мережеві глюки або політика рестарту, що приховує періодичні крахи.
Виправлення: Додайте circuit breakers і retries з jitter. Моніторьте перезапуски і флапи здоров’я. Не використовуйте політику рестарту як заміну виправлення причин крашів.
7) Симптом: Використання localhost в конфігу працює поза Docker, але не в контейнері
Корінь проблеми: Всередині контейнера localhost — це сам контейнер.
Виправлення: Використовуйте ім’я сервісу Compose (db) як хост або вкажіть явний network alias.
Три міні-історії з корпоративного життя (анонімізовано, правдоподібно, технічно точно)
Міні-історія 1: Інцидент через неправильне припущення
Середня SaaS-компанія мала «простий» стек Compose у staging з Postgres, контейнером міграцій і API. Контейнер міграцій залежав від Postgres. API залежав від контейнера міграцій. На папері — акуратний ланцюжок відповідальності.
Під час генеральної репетиції понеділка API піднявся й одразу почав помилятися. Команда зробила те, що роблять команди: перезапустила все. Другий запуск пройшов, і вони заспокоїлися.
Через два тижні вони перебудували staging-хости. Чисті диски, повільніше сховище, трохи інше ядро. Після першого деплою Postgres довше відновлював WAL. Контейнер міграцій стартував (бо контейнер Postgres було запущено), спробував підключитися, впав один раз і завершився з кодом ≠ 0. API стартував, бо ланцюжок залежностей кодував лише порядок запуску, а не «міграції завершено». Потім API помер через відсутність таблиць.
Інцидент не був драматичним, але був галасливим: потік алертів, збентежені інженери та питання від керівництва, чому «staging знову впав». Корінь проблеми був болісно простий: вони моделювали коректність як порядок старту контейнерів, а не як явну готовність і критерії успіху.
Виправлення теж було простим, але вимагало дисципліни: міграції стали явним кроком деплою з чітким успіхом/помилкою. API отримав retry/backoff і дедлайн старту. Compose лишився як раннер — не як двигун надійності.
Міні-історія 2: Оптимізація, що обернулася проти команди
Команда платформи даних хотіла швидшого feedback для розробників. Вони скоротили інтервали healthcheck і retries, щоб сервіси «швидко падали». У теорії, слабкий dependency швидше проявить проблему і розробники швидше її виправлять.
На практиці вони створили машину флапу. На ноутбуках БД була повільною під час холодного старту через обмеження Docker Desktop. Строгий healthcheck рано падав, Compose відмічав сервіс як unhealthy, і залежні сервіси ніколи не стартували. Розробники «вирішували» це, підвищуючи CPU локально або вимикаючи healthchecks взагалі.
Потім патерн просочився в CI. CI-ранери були ресурсно обмежені й шумні. Healthchecks часто падали під час start_period, і пайплайни ставали нестабільними. Інженери втратили довіру до сигналу і повторювали пайплайни, поки ті не пройдуть. Організація отримала повільніший delivery, більше марної витрати ресурсів і менше корисних алертів.
Вони відкотили це, визнавши буденну істину: healthchecks не повинні бути гонкою до дна. Вони мають відповідати очікуваній поведінці старту під реалістичним навантаженням. Збільшили start_period, зберегли розумні інтервали і додали client-side retries, щоб згладити варіації. «Fail fast» стало «fail clearly, з контекстом».
Міні-історія 3: Нудна, але правильна практика, що врятувала ситуацію
Сервіс, суміжний до платіжної системи, мав інтеграційне середовище на Compose, яке використовували кілька команд. Нічого складного: API, воркер, Postgres, Redis. Команда, що це підтримувала, була алергічна до хитрувань — і це комплімент.
Вони впровадили три правила: кожна залежність мала змістовний healthcheck; кожен клієнт мав retry/backoff з максимальним часом старту; кожен деплой запускав smoke-test зсередини неймспейсу після старту. Smoke-test був не великим — лише достатньо, щоб підтвердити критичний шлях.
Одного ранку ребут хоста співпав із погіршенням продуктивності сховища. Postgres піднявся, але був в’яленьким; healthchecks проходили довше, але все ж у межах конфігурації. API довше оголошував себе готовим, бо його внутрішня перевірка чекала успішного запиту плюс перевірку версії міграцій. Він не пішов у crash-loop, тому не завалював Postgres повторними холодними з’єднаннями.
Результат був глибоко непоказним: старт зайняв більше часу, але все продовжило працювати. Команди помітили затримку, але не простій. Різниця була не в героїчності, а в тому, що система була спроєктована для світу, де старт змінний, а залежності можуть запізнюватися.
Чеклісти / поетапний план
Покроковий план, як вийти з пастки залежностей
- Припиніть використовувати
depends_onяк готовність. Залишайте його лише для порядку запуску. - Додайте healthchecks для stateful сервісів. Робіть їх змістовними (не лише «порт відкритий»).
- Якщо підтримується, блокуйте за
condition: service_healthy. Сприймайте це як зручність, а не гарантію. - Реалізуйте client-side retries з експоненційним backoff + jitter. Додайте максимальний дедлайн старту (наприклад, 2–5 хвилин).
- Відокремте міграції від старту додатку. Запускайте їх як явний крок з логуванням і поведінкою при помилці.
- Проєктуйте готовність навколо користувацького шляху. «DB ping працює» може не означати «схема готова».
- Інструментуйте старт. Логуйте, за чим чекаєте, скільки це тривало і чому не вдалось.
- Валідуйте зсередини контейнерів. Тестуйте DNS, TCP і реальний запит із контейнера додатку.
- Слідкуйте за ресурсами. Якщо старт DB I/O-bound, виправляйте сховище/конкуренцію на хості, а не YAML.
- Запускайте після-стартовий smoke-test. Маленький швидкий тест ловить boot-race до користувачів.
Операційний чекліст для Compose-файлу, про який не шкодуватимете
- Кожен stateful сервіс має healthcheck із адекватними
start_period,timeoutйretries. - Клієнти використовують імена сервісів, а не
localhost, для зв’язку в стеку. - Політики рестарту вибрані свідомо; crash-loop розглядається як інцидент, а не «самовідновлення».
- Міграції/ініціалізація — одноразові і спостережувані.
- Логи мають таймстемпи й достатній контекст, щоб відновити хронологію старту.
FAQ
1) Чи чекає depends_on відкриття порту БД?
Ні. За замовчуванням воно лише гарантує, що Compose спробує запустити контейнер залежності перед залежним контейнером. Готовність порту не передбачена.
2) Якщо я додам healthcheck до Postgres, чи все готово?
Ви станете менш неправі, але не завершите справу. Healthcheck може допомогти при початковому запуску (якщо ви використовуєте service_healthy), але додаток все одно потребує повторних спроб для перезапусків і тимчасових збоїв.
3) Чому б просто не використовувати скрипт «wait-for-it» скрізь?
Тому що він часто перевіряє лише TCP-підключення — найменш глибоке визначення «готовності». Згодом такі скрипти стають племінним клеєм, про який забувають. Надавайте перевагу client-side retries і змістовним healthchecks; wait-скрипти використовуйте тільки коли потрібно.
4) Моя БД здорова, але міграції не завершені. Як це змоделювати?
Розділяйте обов’язки: здоров’я БД означає «DB може обслуговувати запити». Завершення міграцій — це стан деплою. Запускайте міграції як явний крок або одноразовий сервіс і блокуйте готовність додатку на перевірці схеми/версії.
5) Чи можу я покладатися на condition: service_healthy у всіх середовищах?
Ні. Підтримка фічі залежить від версії Compose і тулінгу. Завжди перевіряйте через docker compose config і тестуйте в тім середовищі, куди деплоїте.
6) Чому це відбувається тільки на свіжих машинах або після ребуту хоста?
Холодні старти посилюють варіабельність: кеші холодні, диски зайняті, сервіси виконують відновлення після збою, і планування CPU більш шумне. Якщо ваша система покладається на «зазвичай стартує швидко», вона впаде саме тоді, коли все холодне й повільне.
7) Чи погано використовувати restart: always?
Це не морально погано; це операційно ризиковано. Це може приховати реальні збої, створити навантаження на залежності і ускладнити аналіз логів. Поєднуйте політику рестарту з backoff, гарними логами і реальними healthchecks.
8) Як розрізнити проблему готовності й проблему продуктивності?
Проблеми готовності зазвичай падають з «connection refused», помилками DNS або автентифікації. Проблеми продуктивності показують таймаути, високу латентність і насичення ресурсів (у docker stats видно високий CPU/IO). Розглядайте їх по-різному.
9) Який найпростіший надійний підхід для малих стеків?
Поставте healthcheck для БД, додайте client-side retries з backoff і запустіть smoke-test після старту. Ця трійця запобігає більшій частині проблем boot-race без перетворення вашого Compose-файлу на сценарій кіно.
Наступні кроки, які дійсно зменшують кількість інцидентів
Якщо ваш стек іноді потребує «просто перезапустіть», у вас не проблема Compose. У вас проблема контракту готовності. depends_on підходить для порядку запуску. Це не рукостискання, не обіцянка і не заміна стійкості.
Зробіть ці кроки в такому порядку:
- Додайте змістовні healthchecks для stateful залежностей (DB, черги, кеші).
- Змусьте клієнтів повторювати спроби з експоненційним backoff, jitter і максимальним дедлайном старту.
- Припиніть змішувати міграції з випадковими стартами додатку; запускайте їх явно і спостерігайте успіх.
- Будуйте хронологію під час інцидентів за допомогою логів з таймстемпами і
docker events. - Підтвердіть з’єднання зсередини контейнерів перед тим, як переписувати конфіг.
Compose і надалі робитиме те, що завжди робило: запускатиме контейнери. Ваше завдання — зробити так, щоб «запущено» означало щось корисне. Це інженерія надійності: робити очевидні режими відмов нудними.