Ви піднімаєте стек. Контейнер бази даних «up». Контейнер API стартує. Потім він падає, бо не може підключитися.
Ви додаєте depends_on. Все одно падає. Ви додаєте sleep 10. Працює… поки не настане понеділок.
Якщо ви коли-небудь бачили, як стек Compose «мигає» як вимираючий неоновий знак, ось чому: depends_on ніколи не був воротами готовності.
Це підказка для порядку запуску. Сприймати її як гарантію готовності — шлях до періодичних помилок, що повторюються лише під час демонстрацій.
Що насправді робить depends_on (і чого не робить)
Compose має дві окремі ідеї, які люди постійно зливають воєдино:
порядок запуску і готовність.
depends_on стосується лише першого — в певній мірі.
Чиста правда
- Воно може запускати контейнери в заданому порядку. І лише це.
- Воно не чекає, поки ваш сервіс стане готовим. «Контейнер запущено» ≠ «база даних приймає запити».
- Воно не перевіряє мережеву досяжність. Ваш залежний сервіс може бути «up», але недоступний через DNS, правила файрволу або неправильний хостнейм.
- Воно не запобігає умовам гонки. Якщо ваш додаток виконує міграції при старті, а БД ще ініціалізується, ви все одно програєте.
Деякі реалізації і версії Compose підтримують depends_on з умовами типу service_healthy.
Це ближче до того, чого люди хочуть, але навіть тоді: це працює настільки добре, наскільки коректний ваш healthcheck.
Поганий healthcheck — це просто sleep 10 з більшою кількістю паперів.
Ось зміна мислення: готовність — це контракт на рівні програми.
Docker може запускати процеси. Він не може знати, коли ваша БД відтворила WAL, коли додаток прогрів кеші
або коли завершилися міграції схеми. Ви маєте визначити ці сигнали самі.
Жарт №1: Використовувати sleep 10 як ознаку готовності — це як вирішувати втрату пакетів криком на роутер. Виглядає продуктивно, але не працює.
Чому «контейнер запущено» — маркер без значення
Якщо ви використовуєте Postgres, «запущено» може означати, що він ще виконує скрипти ініціалізації, створює користувачів або відтворює логи.
Для Elasticsearch «запущено» може означати, що JVM існує, але кластер у стані red.
Для обʼєктних сховищ «запущено» може означати, що креденшіали ще не завантажено.
Compose не знає вашої семантики. І навіть якби знав, багатоступенева готовність — звична річ:
DNS доступний, TCP порт відкритий, TLS рукопотискання можливе, автентифікація працює, схема присутня, міграції завершені, фонові воркери запущені.
Оберіть стадію, від якої ви справді залежите, і тестуйте саме її.
Факти та історія, які знадобляться в аргументах
Коли ви намагаєтеся переконати команду перестати кидати «wait-for-it.sh» в точку входу, корисно знати, звідки пішов цей безлад.
- Compose спочатку орієнтувався на робочі процеси розробника, а не на продакшн-оркестрацію. Порядок запуску був «достатнім» для ноутбуків.
- «Healthy» не є нативним runtime-станом Docker так само, як «running»; це результат healthcheck, опціональний і визначений додатком.
- Класичні версії файлу Compose змінювали семантику з часом; деякі можливості були в синтаксисі v2, але у v3 стали менш однозначними (особливо у контексті Swarm).
- Swarm і Kubernetes просунули різні моделі: Swarm спирався на lifecycle контейнера; Kubernetes зробив readiness/liveness головними абстракціями, але теж визначеними додатком.
- Порти можуть бути відкриті значно раніше, ніж сервіс стає придатним. Багато демонів привʼязуються рано, а потім виконують внутрішню ініціалізацію.
- DNS всередині Docker-мереж — це eventualmente consistent під час швидких рестартів; помилки резолюції при різких запусках трапляються.
- Політики рестарту можуть створювати грізні хвилі: додаток, що падає швидко, може атакувати БД, яка і так ще завантажується.
- Healthchecks розроблялися для питання «чи живий?» і були репурпознуті для «чи готовий?», що не завжди одне й те саме.
Висновок: ви не «помиляєтесь», бо Compose поганий. Ви помиляєтесь, бо просите Compose бути Kubernetes.
Compose усе ще можна зробити надійним. Треба лише бути явними.
Режими відмов, які ви постійно невірно діагностуєте
1) Відмова підключення при старті, потім пізніше все працює
Зазвичай це гонка. Цільовий процес ще не прив’язав порт або прив’язав його на іншому інтерфейсі.
Іноді навпаки: порт відкритий, але протокол ще не готовий (TLS не завантажено, БД не приймає автентифікацію).
2) «Temporary failure in name resolution»
Вбудований DNS Docker загалом стабільний, але при швидких перезапусках ви все ще можете отримати транзієнтні помилки резолюції.
Якщо ваш додаток сприймає одну DNS-помилку як фатальну, ви створили крихкий старт.
План готовності має включати повторні спроби з backoff для пошуку і підключень по імені.
3) Healthcheck каже «healthy», але додаток усе ще падає
Healthcheck занадто поверхневий. TCP-підключення — не те саме, що «схема існує».
curl /, що повертає 200, може означати «вебсервер піднявся», але не «додаток може говорити з БД».
Healthcheck має відображати межу залежності.
4) Все працює локально, але падає в CI
CI-хости мають інший CPU, диск і поведінку епізоду ентропії.
Повільний I/O затягує ініціалізацію БД. Повільний DNS робить ранню резолюцію ненадійною.
Таймаути, налаштовані для вашого ноутбука, втрачають сенс на «гальмованому» раннері.
Якщо ваше рішення — «додати sleep на 30 секунд», ви просто змістили флейк.
5) Каскадні перезапуски
Залежний сервіс падає швидко і агресивно перезапускається. Кожен рестарт тригерить повтори, міграції, прогрів кешів.
Тим часом БД ще завантажується і під навантаженням.
Ви отримаєте зворотний звʼязок: залежний сервіс стає інструментом DoS по відношенню до власної залежності.
Правильні патерни готовності (без «дрімання»)
Патерн A: Використовуйте healthcheck, що перевіряє те, що вам реально потрібно
Не перевіряйте, що порт відкритий. Перевіряйте, чи система може завершити мінімальну операцію, яку вимагає ваш залежний сервіс.
Для бази даних це може бути «може автентифікуватися і виконати тривіальний запит».
Для HTTP-сервісу — «повертає 200 на endpoint готовності, який перевіряє залежності».
Приклад: healthcheck для Postgres має запускати pg_isready і, ідеально, виконувати запит, якщо ви залежите від конкретної бази.
Для Redis достатньо redis-cli ping. Для Kafka це складніше.
Патерн B: Обмежуйте старт по статусу здоровʼя (коли доступно), а не по старту контейнера
Якщо ваш Compose підтримує depends_on з умовою service_healthy, використовуйте її.
Але сприймайте це як механізм виконання, а не як основну архітектуру.
Основний дизайн все одно: healthcheck має відображати готовність.
Патерн C: Вбудуйте повтори/backoff у ваш додаток
Це те, чого інженери опираються, бо здається «залатуванням» проблем інфраструктури.
Це не так. Мережі ненадійні. Умови гонки при старті відбуваються. Залежності перезапускаються.
Якщо ваш додаток не може повторювати підключення до БД протягом 30–60 секунд з джиттером, він не готовий для продакшну.
Є різниця між «повторювати, бо світ брудний» і «повторювати вічно, бо ми відмовляємось виправляти конфігурацію».
Встановіть верхню межу. Видавайте структуровані логи. Завершуйте з помилкою після розумного таймауту.
Патерн D: Відділяйте «ініціалізаційну роботу» від «обслуговування трафіку»
Міграції схеми, створення бакетів, шаблони індексів і «створити admin користувача» не повинні виконуватись у головному процесі сервісу,
якщо ви не готові до проблем з конкурентністю та ідемпотентністю.
У Compose чистий патерн: одноразовий «init» сервіс, який виконується й успішно виходить, а на нього залежить ваш додаток.
Ваш init-контейнер має бути ідемпотентним: безпечний при повторному запуску, безпечний якщо частково завершений.
Патерн E: Віддавайте перевагу явним endpoint готовності для HTTP-сервісів
Якщо ваш API потребує БД + чергу + обʼєктне сховище, відкрийте endpoint /ready, який перевіряє ці залежності.
Потім ваш healthcheck викликає його. Тепер визначення «готовий» відповідає реальним вимогам.
Патерн F: Уникайте крихких «wait-for» скриптів, прикручуваних до ENTRYPOINT
Люди люблять кинути скрипт, що циклічно перевіряє порт.
Це просто. Але часто неправильно: відкритий порт ≠ готовність, і скрипт перетворюється на некерований міні-платформний компонент.
Якщо треба чекати, зробіть реальну клієнтську перевірку (наприклад, виконати SQL-запит). І тримайте це мінімальним.
Краще: healthchecks + обмеження запуску за здоровʼям + повтори в додатку.
Цитата (парафразована): Werner Vogels неодноразово підкреслював ідею, що «все ламається постійно», тож системи мають припускати відмову і автоматично відновлюватися.
Практичні завдання: команди, виводи, рішення
Ось ходи, які я реально використовую, коли стек Compose не піднімається чисто.
Кожне завдання містить: команду, типовий вивід, що це означає і яке рішення приймати.
Завдання 1: Доведіть, що Compose бачить саме ту конфігурацію
cr0x@server:~$ docker compose config
services:
api:
depends_on:
db:
condition: service_healthy
environment:
DATABASE_URL: postgres://app:***@db:5432/app
db:
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app -d app"]
interval: 5s
timeout: 3s
retries: 20
Значення: Це нормалізована конфігурація, яку Compose запустить, після злиттів і підстановок змінних.
Якщо condition: service_healthy зникає тут, ваша реалізація Compose його не підтримує.
Рішення: Якщо вихід конфігурації не відповідає очікуванням, припиніть дебаг рантайму.
Виправте YAML і невідповідність версій/реалізацій спочатку.
Завдання 2: Спостерігайте порядок старту та причини виходу одним махом
cr0x@server:~$ docker compose up --detach && docker compose ps
[+] Running 2/2
✔ Container stack-db-1 Started 0.7s
✔ Container stack-api-1 Started 0.2s
NAME IMAGE COMMAND SERVICE STATUS
stack-db-1 postgres:16 "docker-entrypoint.s…" db Up 2 seconds (health: starting)
stack-api-1 myapi:latest "/app/start" api Restarting (1) 2 seconds ago
Значення: БД в статусі «starting» по здоровʼю; API вже перезапускається. Це ваша гонка, видима неозброєним оком.
Рішення: Якщо залежний сервіс перезапускається, поки залежності «starting», потрібне обмеження запуску і/або повтори.
Завдання 3: Точно перевірте статус здоровʼя
cr0x@server:~$ docker inspect --format '{{json .State.Health}}' stack-db-1
{"Status":"starting","FailingStreak":2,"Log":[{"Start":"2026-01-02T10:01:01.123Z","End":"2026-01-02T10:01:01.456Z","ExitCode":1,"Output":"/bin/sh: pg_isready: not found\n"}]}
Значення: Команда healthcheck не знайдена в образі. Образи Postgres мають pg_isready, але slim-версії можуть не мати.
Рішення: Виправте healthcheck, щоб використовувати наявні інструменти, або встановіть клієнтські інструменти. Healthcheck з помилкою гірший за його відсутність.
Завдання 4: Підтвердіть, що процес слухає порт
cr0x@server:~$ docker exec -it stack-db-1 ss -lntp
State Recv-Q Send-Q Local Address:Port Peer Address:PortProcess
LISTEN 0 244 0.0.0.0:5432 0.0.0.0:* users:(("postgres",pid=1,fd=6))
Значення: Postgres прив’язав TCP 5432. Це необхідна, але не достатня умова.
Рішення: Якщо не слухає, перевірте логи і конфіг БД. Якщо слухає, але клієнти падають — підніміть рівнем вище: автентифікація, DNS, TLS, схема.
Завдання 5: Перевірте резолюцію імен із контейнера залежного сервісу
cr0x@server:~$ docker exec -it stack-api-1 getent hosts db
172.20.0.2 db
Значення: DNS-резолюція працює в цей момент.
Рішення: Якщо резолюція падає періодично, додайте повтори з backoff і подумайте про зменшення агресивності рестартів.
Завдання 6: Перевірте TCP-з’єднання (швидко, поверхнево)
cr0x@server:~$ docker exec -it stack-api-1 bash -lc 'timeout 2 bash -lc "
Значення: TCP-хендшейк можливий.
Рішення: Якщо TCP падає — це мережа/імʼя/файрвол/слухання. Якщо TCP працює — потрібні перевірки на рівні протоколу.
Завдання 7: Перевірте готовність реальною клієнтською операцією (Postgres)
cr0x@server:~$ docker exec -it stack-api-1 bash -lc 'psql "postgres://app:app@db:5432/app" -c "select 1;"'
?column?
----------
1
(1 row)
Значення: Автентифікація працює, БД існує, запити виконуються. Це реальна готовність.
Рішення: Якщо це падає, припиніть звинувачувати Compose. Виправляйте креденшіали, ініціалізацію БД або міграції.
Завдання 8: Читайте логи з таймштампами без безладу прокрутки
cr0x@server:~$ docker compose logs --timestamps --tail=80 api
2026-01-02T10:01:03.002Z api-1 ERROR db connect failed: dial tcp: lookup db: temporary failure in name resolution
2026-01-02T10:01:03.540Z api-1 ERROR exiting after 1 attempt
Значення: Це не «БД повільна», а транзієнтна DNS-резолюція + додаток виходить після однієї спроби.
Рішення: Додайте логіку повторів. Також подумайте про зменшення агресивності рестартів, щоб DNS/демони встигали стабілізуватися.
Завдання 9: Перевірте політику рестарту і поточний цикл рестартів
cr0x@server:~$ docker inspect --format '{{.HostConfig.RestartPolicy.Name}}' stack-api-1
always
Значення: Контейнер буде перезапускатися вічно, навіть якщо падає миттєво.
Рішення: Використовуйте on-failure для деяких сервісів у розробці або додайте backoff/таймаут у додатку, щоб не створювати self-DoS.
Завдання 10: Підтвердіть, що контейнери в тій самій мережі
cr0x@server:~$ docker network inspect stack_default --format '{{json .Containers}}'
{"a1b2c3d4":{"Name":"stack-db-1","IPv4Address":"172.20.0.2/16"},"e5f6g7h8":{"Name":"stack-api-1","IPv4Address":"172.20.0.3/16"}}
Значення: Вони ділять дефолтну мережу проекту.
Рішення: Якщо сервіс на іншій мережі, ваш хостнейм може не резолвитися чи маршрут не пройде. Виправте мережі перед налаштуванням таймаутів.
Завдання 11: Перевірте, що ваш healthcheck реально виконується
cr0x@server:~$ docker inspect --format '{{range .State.Health.Log}}{{.ExitCode}} {{.Output}}{{end}}' stack-db-1 | tail -n 3
0 /var/run/postgresql:5432 - accepting connections
0 /var/run/postgresql:5432 - accepting connections
0 /var/run/postgresql:5432 - accepting connections
Значення: Healthcheck виконується і проходить. Це необхідна умова, якщо ви покладаєтесь на service_healthy.
Рішення: Якщо healthcheck не запускається, перевірте, чи образ підтримує HEALTHCHECK і чи Compose налаштований правильно.
Завдання 12: Поміряйте cold-start час залежності
cr0x@server:~$ time docker compose up -d db && docker inspect --format '{{.State.Health.Status}}' stack-db-1
healthy
real 0m18.412s
user 0m0.071s
sys 0m0.052s
Значення: У цьому прогоні БД зайняла ~18 секунд, щоб стати healthy.
Рішення: Налаштуйте таймаути і вікна повторів залежних сервісів на основі вимірювань, а не відчуттів. Якщо CI повільніший — виміряйте і там.
Завдання 13: Перевірте endpoint готовності вашого додатку всередині мережі
cr0x@server:~$ docker exec -it stack-api-1 curl -fsS http://localhost:8080/ready
{"status":"ready","db":"ok","queue":"ok"}
Значення: Додаток декларує себе готовим і перевіряє власні залежності.
Рішення: Якщо цей endpoint бреше — виправте його. Оркестрація довіряє сигналам, які ви даєте.
Завдання 14: Виявити «порт відкритий, але сервіс не готовий» через HTTP-статус
cr0x@server:~$ docker exec -it stack-api-1 curl -i http://localhost:8080/
HTTP/1.1 503 Service Unavailable
Content-Type: application/json
Content-Length: 62
{"error":"warming up","details":"migrations running"}
Значення: Сервер живий, але не готовий. Це правильна поведінка.
Рішення: Нехай ваш healthcheck викликає /ready, а не /. Залиште / для користувацької поведінки, якщо хочете.
Завдання 15: Визначте повільне сховище як справжню «помилку готовності»
cr0x@server:~$ docker exec -it stack-db-1 bash -lc 'dd if=/dev/zero of=/var/lib/postgresql/data/.bench bs=1M count=256 conv=fsync'
256+0 records in
256+0 records out
268435456 bytes (268 MB, 256 MiB) copied, 9.82 s, 27.3 MB/s
Значення: Якщо ви бачите десятки MB/s з fsync, ваша «готовність БД» може просто означати «диск повільний».
Контейнери не скасовують фізику.
Рішення: Якщо сховище повільне, збільшіть таймаути готовності і виправте диск (або перемістіть томи), замість того щоб розкидати sleep у коді додатку.
Плейбук швидкої діагностики
Коли стек не піднімається, не метушіться. Запустіть це в порядку. Мета — знайти вузьке місце за менш ніж п’ять хвилин.
Перший крок: підтвердьте, чи у вас взагалі є сигнал готовності
- Запустіть
docker compose configі шукайте блокиhealthcheckта умови залежностей. - Запустіть
docker compose psі перевірте, чи залежності мають(health: starting),(health: unhealthy)або взагалі без health.
Якщо нема healthcheck — ваша «готовність» це бажане мислення. Додайте його.
Другий крок: визначте, чи проблема в мережі/DNS або в протоколі/автентифікації
- З контейнера, що падає:
getent hosts <service>(DNS) - Потім: TCP-перевірка порту (зʼєднання)
- Потім: реальна клієнтська операція (протокол/автентифікація)
Ця послідовність запобігає класичній помилці: годину налаштовувати БД, коли хостнейм був неправильний.
Третій крок: зупиніть шторм рестартів, поки вони не приховали справжню помилку
- Перевірте політику рестарту. Якщо flapping, тимчасово зменшіть масштаб залежного сервісу:
docker compose stop api. - Підніміть залежність окремо. Зробіть її здоровою першою.
- Потім стартуйте додаток і дивіться першу помилку, а не пʼятдесяту.
Четвертий крок: перевірте сховище і конкуренцію CPU
- Якщо БД «starting» вічно, часто причина — повільний диск, fsync-латентності або брак памʼяті.
- Заміряйте швидкість через простий fsync-запис або перевірте метрики хоста, якщо доступні.
Жарт №2: Compose не має прапорця «wait for SAN», бо визнати, що у вас є SAN — уже форма перевірки готовності.
Поширені помилки: симптом → корінна причина → виправлення
«Я використав depends_on, чому все ще падає?»
Симптом: Залежний сервіс стартує і миттєво помилково підключається до БД/черги.
Корінна причина: depends_on встановлює порядок запуску, а не готовність.
Виправлення: Додайте реальний healthcheck до залежності; обмежте запуск за service_healthy, якщо підтримується; додайте retry/backoff у додатку.
«Healthcheck каже healthy, але додаток помилково виконує міграції»
Симптом: БД healthy, додаток падає з «relation does not exist» або «database does not exist».
Корінна причина: Healthcheck перевіряв лише підключення, а не готовність схеми/даних.
Виправлення: Додайте init job, який виконує міграції ідемпотентно; або зробіть готовність залежною від завершення міграцій; або нехай healthcheck перевіряє наявність потрібних обʼєктів.
«Temporary failure in name resolution» при старті
Симптом: DNS lookup впав один раз; додаток вийшов; перезапускається; іноді працює.
Корінна причина: DNS-раса при старті + додаток не повторює спроби.
Виправлення: Повторюйте DNS/підключення з backoff протягом обмеженого часу; зменшіть агресивність рестартів; уникайте краху при першій помилці резолюції.
«Connection refused» хоча сервіс запущений
Симптом: Цільовий контейнер працює; клієнти бачать ECONNREFUSED.
Корінна причина: Сервіс ще не слухає, неправильний порт, привʼязка до іншого інтерфейсу або безпекова конфігурація відкидає зʼєднання на ранньому етапі.
Виправлення: Перевірте ss -lntp всередині контейнера; перевірте мапінг портів vs внутрішній порт; підтвердіть адресу прослуховування; використайте readiness-перевірку, що розуміє протокол.
«Працює після додавання sleep 30, тож ми в порядку»
Симптом: Флейки зникають локально; CI все ще флейкує; продакшн деплоя уповільнені.
Корінна причина: Sleep — це здогад; час старту варіюється через I/O, CPU і шляхи ініціалізації.
Виправлення: Видаліть sleep. Замініть на healthchecks + gating + повтори. Заміряйте холодні старти і налаштуйте таймаути на основі спостережень.
«Все healthy, але запити падають 2 хвилини»
Симптом: Healthchecks проходять, але додаток повертає 500, бо прогріває кеші або будує індекси.
Корінна причина: Неправильне визначення готовності; ви перевіряєте живість.
Виправлення: Реалізуйте readiness-endpoint, що перевіряє критичні залежності і внутрішнє прогрівання; використайте цей endpoint у healthcheck.
«Рестарт допомагає»
Симптом: Перший старт падає; другий старт працює.
Корінна причина: Схована проблема з порядком ініціалізації (користувачі/схема створюються при першому запуску).
Виправлення: Винесіть ініціалізацію в одноразову задачу або зробіть її ідемпотентною і безпечною для повторних запусків. Переконайтеся, що додаток чекає на завершення init.
Три корпоративні історії з практики
Міні-історія 1: Інцидент через хибне припущення
Середній SaaS запуск мав внутрішнє «integration environment» на потужній віртуальній машині. Це не було продакшном,
але інженери перевіряли там зміни перед відправкою. Стек містив Postgres і API-контейнери.
Хтось додав depends_on: [db] і відчув себе відповідальним. І був.
API мав шлях старту, який автоматично застосовував міграції. У більшості днів Postgres ініціалізовувався швидко, і перша спроба API проходила.
В деякі дні — після перезавантажень хоста або коли кеш диска був «холодним» — Postgres довше приймав підключення.
API спробував один раз, впав і вийшов. Політика рестарту його повертала. Друга спроба зазвичай працювала.
Потім прийшло зміни, що зробили старт Postgres повільнішим: додаткові розширення при першому завантаженні і більше init-скриптів.
API тепер падав три-шість разів перед успіхом. Інженери бачили логи миготіння, запускали docker compose up і рухалися далі.
В день, коли це мало значення — під час демонстрації клієнту — API ніколи не стабілізувався, бо шторм рестартів призвів до повторних спроб міграцій,
кожна блокувала таблиці і продовжувала час старту.
Хибне припущення було не «depends_on працює». Воно було тоншим: «якщо воно врешті підніметься, то все ок».
Такі періодичні помилки стають повними відмовами під навантаженням або у критичні моменти.
Виправлення виявилося нудним: healthcheck для Postgres, gating за service_healthy і міграції винесені в одноразовий job,
що запускався точно один раз на деплой і голосно логував помилки.
Міні-історія 2: Оптимізація, що повернулась бумерангом
Інша організація гналася за швидшими CI-збірками. Вони агресивно зменшували образи: базові образи менші, менше пакетів, менше утиліт.
Хтось прибрав клієнтські інструменти Postgres з образу додатку, бо «нам не потрібен psql в продакшині».
Це частково правда. Але вони також використовували psql у скрипті готовності при старті, щоб перевіряти схему.
Пайплайн почав падати. Не постійно — кешування означало, що деякі раннери мали старі шари, а деякі роботи йшли різними шляхами збірки.
У невдалих прогонів API-контейнер стартував, намагався виконати psql і падав з psql: command not found.
Контейнер виходив, політика рестарту пробувала знову, і робота тайм-аутувала. Люди звинувачували базу даних. Вони були невинні.
«Оптимізація» погіршилася: щоб зменшити шум у логах, хтось змінив entrypoint, щоб поглинати помилку і підмінити перевірку на порт.
Тепер контейнер «чекав» TCP 5432 і стартував додаток.
Додаток одразу ж натикається на «relation missing», бо міграції не гарантовані, і помилка перемістилась далі в послідовності старту.
Зрештою вони зробили те, що мали зробити спочатку: замінили ad-hoc скрипт на proper healthcheck для Postgres,
а додаток отримав обмежений цикл повторів для підключень до БД.
Якщо треба було перевірити схему, вони додали окремий контейнер для міграцій, який містив потрібні інструменти і мав одне призначення.
CI став швидшим, але важливіше — передбачуванішим.
Міні-історія 3: Нудна, але правильна практика, що врятувала день
Команда фінансових сервісів тримала кілька стеків Compose для тестових оточень на спільних хостах.
Оточення не були «іграшковими»: їх використовували для відпрацювання інцидентів і верифікації ролбеків.
У команди було правило: кожна залежність має healthcheck, що відповідає реальній клієнтській операції,
і кожен додаток має повторювати критичні залежності під час старту.
Це зробило їхні файли Compose трохи довшими. Але життя — коротшим, у доброму сенсі.
У них були readiness-endpoint для HTTP-сервісів, healthcheck для баз з автентифікацією, і одноразові init-сервіси для міграцій.
Вони також свідомо налаштовували політики рестарту: бази не перезапускалися при кожній транзієнтній помилці, а додатки не атакували залежності миттєвими повторними спробами.
Одного ранку після патчу хоста кілька середовищ піднялися повільніше.
Сховище було тимчасово деградоване після resync RAID. Контейнери Postgres довше ставали готовими.
Стеки не впали в шторм рестартів. Додатки чекали. Healthchecks лишалися «starting», поки БД реально не була придатна.
Команда помітила це, бо вони моніторили статус здоровʼя і час старту, а не лише «контейнер запущений».
Вони відклали репетицію на 20 хвилин, замість того щоб проводити двогодинні суперечки з привидами.
Нудні практики не виглядають героїчними. Вони просто позбавляють вас потреби в героях.
Чеклісти / покроковий план
Покроково: перетворення нестабільного Compose-стека на надійний
-
Визначте готовність для кожної залежності.
Для БД: «може автентифікуватися і виконати запит». Для HTTP-сервісів: «/ready повертає ok і залежності ок». -
Додайте healthcheck для кожної станної залежності.
Уникайте чисто портових перевірок, якщо це не є єдиною потребою. -
Обмежуйте запуск залежних сервісів за станом health, коли підтримується.
Якщо ваш Compose підтримуєservice_healthy, використовуйте його. Якщо ні — покладайтеся на retry в додатку і розгляньте патерн init job. -
Додайте обмежені повтори з backoff у застосунку.
Включіть помилки резолюції DNS, таймаути підключень і помилки автентифікації, що можуть виникати під час ініціалізації. -
Відділіть ініціалізацію від сервісу.
Міграції, створення бакетів, шаблони індексів — в одноразовий сервіс, що можна безпечно перезапустити. -
Зробіть init ідемпотентним.
Використовуйте «create if not exists», транзакційні міграції і безпечні повторні запуски. Припускайте, що воно виконається двічі. -
Налаштуйте політики рестарту, щоб уникнути штормів.
Якщо сервіс падає через неготові залежності, він не повинен перезапускатися 20 разів на хвилину. -
Інструментуйте час старту.
Логуйте «starting», «connected to db», «migrations complete», «ready». Ви не зможете виправити те, що не вимірюєте. -
Тестуйте холодні старти в CI.
Час від часу очищуйте кеші або запускайте на чистих раннерах. Заміряйте повільний шлях. -
Перестаньте використовувати sleep як механізм контролю.
Замініть їх перевірками реальної готовності або покладіться на повтори.
Чекліст: як виглядає «хороший» healthcheck
- Працює швидко (ідеально < 1s) коли сервіс здоровий.
- Надійно падає, коли сервіс непридатний для залежних.
- Використовує реальний клієнтський протокол, коли можливо (SQL-запит, HTTP-запит).
- Не потребує зовнішнього мережевого доступу або крихких залежностей.
- Має розумні інтервали й повтори на основі виміреного часу старту.
- Має ясний вивід помилок у
docker inspect.
Чекліст: що має робити ваш додаток при старті
- Повторювати підключення до залежностей протягом обмеженого вікна (наприклад, 60–180 секунд залежно від оточення).
- Використовувати експоненційний backoff з джиттером, щоб уникнути синхронізованих хвиль запитів.
- Логувати кожну невдалу спробу з причиною, але не спамити: агрегуйте або обмежуйте частоту при потребі.
- Виходити з чіткою помилкою, якщо залежність недоступна після вікна спроб.
- Експонувати readiness-endpoint, що відображає реальну здатність обслуговувати запити.
FAQ
1) Чи залежить depends_on коли-небудь від готовності?
Само по собі — ні. Деякі реалізації Compose підтримують умови типу service_healthy, які можуть обмежувати старт по healthcheck.
Але healthcheck має існувати і відображати реальну готовність.
2) Чи валідна TCP-перевірка порту як healthcheck?
Іноді. Якщо ваш залежний сервіс дійсно вимагає лише «порт відкритий», то ок. Але це рідкість.
Більшість сервісів потребують автентифікацію, маршрутизацію, схему або внутрішню ініціалізацію — тому перевірка на рівні протоколу безпечніша.
3) Чому просто не збільшити sleep до 60 секунд?
Бо час старту змінний. Ви зробите швидкі шляхи повільнішими і все одно впадете для повільних шляхів.
Також sleep ховає реальні проблеми: неправильні креденшіали, хостнейми, відсутні міграції.
4) Чи робити міграції під час старту додатку?
Якщо робите, ви повинні коректно обробляти конкурентність, ідемпотентність і відмови.
У Compose стеку одноразовий сервіс для міграцій зазвичай чистіший і легший для спостереження.
5) Яка різниця між liveness і readiness тут?
Liveness: «процес живий і не завис». Readiness: «він може коректно обслуговувати запити зараз».
Healthcheck у Compose часто використовують як liveness; ви можете використовувати його для readiness, але лише якщо визначили його відповідно.
6) Мій сервіс «healthy», але все одно недоступний з іншого контейнера. Як так?
Healthcheck виконується всередині контейнера. Він може пройти навіть якщо сервіс недоступний по мережі (неправильна адреса прослуховування, інша мережа, правила файрволу).
Перевірте адресу прослуховування (ss -lntp) і приналежність до мережі (docker network inspect).
7) Використання restart: always — добре чи погано?
Це інструмент, а не вирок. Для залежностей, які можуть падати, може допомогти.
Для додатків, що падають швидко через неготові залежності, воно може створити шторм рестартів і приховати корінні причини.
Поєднуйте з розумними retry і якісними логами.
8) Чи можна покладатися на порядок запуску docker compose up для баз і кешів?
Ви можете покладатися на те, що Compose намагатиметься запускати контейнери в цьому порядку.
Ви не можете покладатися на те, що «залежність придатна, коли залежний сервіс стартує».
Якщо ваш додаток потребує придатної БД, потрібні readiness-перевірки і повтори.
9) Як працювати з множинними залежностями (БД + черга + обʼєктне сховище)?
Визначте готовність на межі додатку: створіть readiness-endpoint, що перевіряє всі критичні залежності.
Обмежуйте прийняття трафіку за цією готовністю і забезпечуйте healthcheck для кожної залежності, коли можливо.
Висновок: наступні кроки, які реально потрібно зробити
Перестаньте просити depends_on робити роботу, на яку воно не наймалось. Використовуйте його для порядку старту, якщо треба.
Але для надійності потрібні реальні сигнали готовності, і системи, що терпимі до гонок при старті.
- Додайте healthcheck до кожної важливої залежності (БД, кеш, черга, шлюзи обʼєктного сховища).
- Нехай healthcheck відображає реальну придатність, а не «порт відкритий».
- Якщо підтримується, обмежуйте старт по
service_healthy. Якщо ні — покладайтеся на retry у додатку. - Винесіть міграції й одноразову ініціалізацію в окремий, ідемпотентний job-контейнер.
- Реалізуйте обмежений retry з backoff у кожному сервісі, що спілкується з залежностями при старті.
- Коли щось іще ламається, користуйтеся плейбуком швидкої діагностики — бо воно точно зламається, але менше драматично.
Мета не ідеальність. Мета — нудні старти. Нудні старти дозволяють скеровувати увагу на продукт замість рулетки під час завантаження.