Docker: порядок запуску проти готовності — підхід, що запобігає хибним запускам

Було корисно?

Найгірший тип інциденту — той, що виглядає так, ніби сам собі вирішив. Ваш стек «піднявся», панелі моніторингу стали зеленими,
а через п’ять хвилин додаток починає кидати 500, бо база даних насправді не була готова — лише технічно «запущена».
Тим часом оркестратор зробив саме те, що ви йому сказали. Ось де боляче.

Docker та Docker Compose — грубі інструменти: вони можуть запускати контейнери у певному порядку, але не можуть магічно
зрозуміти, коли залежність безпечна для використання, якщо ви не навчите їх, що означає «готова». Лікування — відокремити порядок запуску
від готовності, підключити перевірки, основані на доказах, і зробити «хибні старти» неможливими або принаймні голосно помітними.

Порядок запуску проти готовності: різниця, що має значення

Порядок запуску відповідає на питання: «Чи було запущено процес?» І все. Docker це може: контейнер A запускається,
потім контейнер B запускається. Гарно і охайно. Але небезпечно неповно.

Готовність відповідає на питання: «Чи сервіс зараз придатний для використання клієнтами?» Це включає: відкриті сокети,
завершені міграції, прогріті кеші, завантажені TLS-ключі, завершений вибір лідера, правильні дозволи та досяжні залежності.
Готовність — це контракт між сервісом і світом. Docker цього контракту не виводить самостійно. Ви його реалізуєте.

Хибні старти трапляються, коли ми використовуємо порядок запуску як проксі для готовності. Це як оголосити ресторан відкритим,
бо горить світло, коли кухар ще спорюється з коробкою замороженої картоплі.

Надійний стек робить дві речі:

  • Він запускає компоненти у розумному порядку коли це допомагає.
  • Він допускає залежні компоненти лише після перевіреної готовності, а не оптимізму.

Ви іноді почуєте «просто додайте sleep». Це не готовність; це забобон з таймером.
Ваше майбутнє «я» вас ненавидітиме, а канал інцидентів — ще швидше.

Чому відбуваються хибні старти (і чому це так поширено)

«Запущено» — це дешевий стан. Процес може стартувати, але бути марним. У сучасних системах «корисність» часто залежить від:
доступності мережі, стану файлової системи, облікових даних, схемних міграцій і верхніх сервісів. Будь-що з цього може відставати
від початку процесу на секунди або хвилини.

Класичні режими відмов, що породжують хибні старти

  • Сокет ще не слухає. Додаток завантажується, а прив’язка порту відбувається пізніше. Клієнти пробують негайно й падають.
  • Порт слухає, але сервіс не готовий. HTTP повертає 503, бо йдуть міграції або прогрів кешу.
  • База приймає TCP, але не виконує запити. PostgreSQL приймає з’єднання під час відтворення WAL або відновлення.
  • DNS не осів. Ім’я контейнера існує, але кеші/сайдкари резолверу ще не готові.
  • Залежність готова, але зі старою схемою. Додаток стартує до міграцій і падає з «relation does not exist».
  • Том не примонтовано або неправильні дозволи. Сервіс стартує, нічого не записує, а потім руйнується під власними помилками.
  • Ліміти і стадо запитів. Десять реплік одночасно «стартують», усі шквалять залежність і ви дізнаєтеся, що таке «шторм повторів».

Неприємна правда: багато додатків написані з припущенням, що людина їх перезапустить, якщо щось піде не так під час старту.
Контейнери забирають людину й ставлять автоматизацію, яка радо повторюватиме ту саму помилку вічно.

Жарт №1: Якщо ви думаєте, що depends_on означає «працює», Docker має вам міст — і він, мабуть, на техобслуговуванні.

Факти та історичний контекст (коротко, конкретно)

  1. Рання функція «link» у Docker (до зрілості Compose) намагалася з’єднати залежності, інжектуючи змінні оточення; це не вирішило готовність.
  2. Формат файлу Compose v2 популяризував depends_on для порядку запуску; багато команд неправильно читали це як gating готовності.
  3. Compose v3 змістив фокус до Swarm і прибрав деякі умовні семантики запуску; люди продовжували вважати, що старе поводження залишилось.
  4. Kubernetes ввів readiness probes як перший рівень концепції, бо «контейнер працює» ніколи не було достатнім для маршрутизації трафіку.
  5. systemd давно має ordering залежностей і навіть воно розрізняє «запущено» й «готово» через notification-механізми.
  6. PostgreSQL може приймати TCP-з’єднання раніше ніж стає повністю готовим для навантаження (відтворення, replay, контрольні точки).
  7. Варіанти MySQL можуть слухати порт рано, але відкидати автентифікацію чи блокувати внутрішні таблиці під час ініціалізації, створюючи пастку хибного старту.
  8. Healthchecks були додані в Docker щоб вийти за межі «процес існує» як єдиного сигналу; їх все ще недоопрацьовують або неправильно використовують.

Що Docker Compose насправді робить (і чого не робить)

depends_on: порядок, а не готовність

У Compose depends_on контролює порядок старту/зупинки. За замовчуванням це не чекає, доки залежність не стане
готовою. Воно гарантує лише, що Docker спробував запустити контейнер. І все.

Compose може використовувати healthchecks для gating у деяких режимах, але експлуатаційна реальність заплутана:
люди запускають різні версії Compose, різні Docker-рушії, і мають очікування, сформовані старими блог-постами.
Якщо ви хочете надійності, ви вбудовуєте логіку готовності в стек явно й тестовано.

Healthcheck: корисно, але його треба проєктувати

Healthcheck — це періодичний тест, який виконує рушій. Якщо він падає, Docker позначає контейнер як unhealthy. Це сигнал.
Те, що ви робите з цим — рестарти, gating, алертинг — це окремий вибір.

Якщо ваш healthcheck — «curl localhost:8080», але сервіс повертає 200, коли він усе ще відмовляє зовнішнім запитам,
ви створили брехуна. Брехуни проходять тести й підводять клієнтів.

Політики рестарту: не про готовність, а про наполегливість

Політики рестарту стосуються відновлення після аварій. Вони не є стратегією залежностей. Якщо ваш додаток виходить через те, що БД
не була готова, політики рестарту перетворять короткий прогрів БД у цикл рестартів. Той цикл також може посилити навантаження на БД.

Єдиний сигнал готовності, що має значення: «Чи зможе клієнт успішно виконати запит?»

Готовність треба визначати з перспективи клієнта. Якщо сервіс залежить від бази і черги, «готовий» означає,
що він може підключитися, пройшов автентифікацію, виконати простий запит і опублікувати/спожити невелике повідомлення — або будь-яку іншу
«мінімально корисну операцію» для вашої системи.

Одна цитата, щоб не давати собі спочивати на лаврах, переказ ідеї від John Allspaw: парафраз ідеї: Надійність приходить від навчання та зворотних звʼязків, а не від удавання, що відмов не буває.

Патерни готовності, що працюють у продакшні

Патерн 1: Розмістіть реальні healthchecks на залежностях

Якщо ви запускаєте PostgreSQL, Redis або HTTP API в контейнері, дайте йому healthcheck, що відображає реальну придатність.
Для Postgres це може бути pg_isready плюс реальний запит, якщо потрібно перевірити схему. Для HTTP хітайте endpoint,
який перевіряє залежності, а не статичний маршрут «OK».

Патерн 2: Допуск залежних сервісів за готовністю (явно)

Існує три поширені підходи до gating:

  • Gating у Compose через статус здоровʼя (коли це підтримується у вашому середовищі): depends_on + health conditions.
  • Скрипти очікування в entrypoint всередині залежного контейнера: чекати TCP + валідацію на рівні додатка; потім стартувати.
  • Нативні повтори в додатку з backoff і jitter: кращий довгостроковий варіант, бо працює повсюди, а не лише в Docker.

Найміцніший підхід: додаток коректно робить повтори і оркестратор має healthchecks. Пояс і підтяжки.
Це операційна справа. Одягаємось під ту погоду, що є, а не ту, на яку заслуговуємо.

Патерн 3: Зробіть міграції первинною задачею

Схемні міграції — не побічний ефект. Ставте їх як окремий, явний крок: однорозовий контейнер/job, що виконує міграції й завершує роботу успішно.
Лише після цього запускайте app-контейнери. Це запобігає хаосу «п’ять апів змагаються, хто перший накладе міграції».

Патерн 4: Використовуйте endpoint готовності, що перевіряє залежності

Для HTTP-сервісів експонуйте:

  • /healthz (liveness): «чи процес живий?»
  • /readyz (readiness): «чи можу я обслуговувати реальний трафік?» (БД досяжна, черга досяжна, критичні конфіги завантажені)

Навіть у Docker Compose це допомагає, бо ваш healthcheck може хітати /readyz, а не гадати.

Патерн 5: Обмежте повтори, потім фейлніть голосно

Нескінченні повтори можуть ховати реальні відмови. Обмежені повтори з чіткими логами дають контрольовану затримку старту, коли все повільно,
але все ж фейлять, якщо світ справді зламався.

Патерн 6: Додайте jitter і backoff

Якщо 20 контейнерів стартують одночасно й усі штурмують БД кожні 100 мс, ви самі собі створюєте DDoS. Backoff і jitter перетворюють стадний штурм на струмок.

Жарт №2: «Просто додайте sleep на 30 секунд» — це шлях до 31-секундного простою й трьохгодинного розслідування.

Практичні завдання: команди, виводи, рішення (12+)

Це перевірки, які я реально виконую, коли стек «запущено», але додаток поводиться так, ніби він алергічний до понеділка.
Кожне завдання включає: команду, що означає її вивід, і рішення, яке ви приймаєте.

Завдання 1: Подивитися стан контейнерів та здоровʼя одним поглядом

cr0x@server:~$ docker ps --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}'
NAMES              STATUS                          PORTS
app                Up 18 seconds (health: starting) 0.0.0.0:8080->8080/tcp
db                 Up 22 seconds (healthy)          5432/tcp
redis              Up 21 seconds (healthy)          6379/tcp

Значення: app працює, але ще не готовий; DB і Redis — healthy.

Рішення: Не переводьте трафік. Якщо статус додатка довго «starting», перевірте його healthcheck і логи старту.

Завдання 2: Дослідити, чому падає healthcheck

cr0x@server:~$ docker inspect --format '{{json .State.Health}}' app
{"Status":"unhealthy","FailingStreak":3,"Log":[{"Start":"2026-01-03T10:10:01.123Z","End":"2026-01-03T10:10:01.456Z","ExitCode":1,"Output":"curl: (7) Failed to connect to localhost port 8080: Connection refused\n"}]}

Значення: додаток ще не слухає, або слухає на іншому порту/інтерфейсі.

Рішення: Перевірте логи додатка та адресу/порт прослуховування. Якщо він привʼязаний до 127.0.0.1 всередині контейнера,
це підходить для healthcheck, але не для зовнішніх клієнтів, якщо порт не опубліковано правильно.

Завдання 3: Прочитати логи запуску з часовими мітками

cr0x@server:~$ docker logs --since 10m --timestamps app | tail -n 30
2026-01-03T10:09:44.001234567Z boot: loading config
2026-01-03T10:09:44.889012345Z db: connection failed: dial tcp db:5432: connect: connection refused
2026-01-03T10:09:45.889045678Z db: connection failed: dial tcp db:5432: connect: connection refused
2026-01-03T10:09:46.889078901Z boot: giving up after 2 retries

Значення: додаток пробував двічі і зійшов занадто рано. DB тоді не була готовою.

Рішення: Збільшити кількість повторів/backoff або затримати запуск, доки не буде перевірено готовність DB. Також оцініть, чи правильна поведінка «вихід при відсутності БД».

Завдання 4: Підтвердити, що логи DB вказують на реальну готовність, а не лише запуск

cr0x@server:~$ docker logs --since 10m --timestamps db | tail -n 30
2026-01-03T10:09:30.100000000Z PostgreSQL init process complete; ready for start up.
2026-01-03T10:09:31.200000000Z database system is ready to accept connections

Значення: DB оголосила готовність о 10:09:31; додаток почав падати о 10:09:44. Ця невідповідність вказує або на неправильне імʼя хоста,
або мережеву проблему, або на перезапуск DB.

Рішення: Перевірте мережу між контейнерами й історію рестартів DB; упевніться, що додаток використовує правильний service name і порт.

Завдання 5: Перевірити цикли рестарту контейнерів

cr0x@server:~$ docker ps -a --format 'table {{.Names}}\t{{.Status}}\t{{.RunningFor}}'
NAMES   STATUS                     RUNNING FOR
app     Restarting (1) 3 seconds   2 minutes
db      Up 2 minutes (healthy)     2 minutes

Значення: додаток падає й перезапускається.

Рішення: Не намагайтеся «виправити» це простим рестартом. Подивіться код виходу й помилку; виправте обробку залежностей або конфіг.

Завдання 6: Отримати код виходу додатка і причину останньої помилки

cr0x@server:~$ docker inspect --format 'ExitCode={{.State.ExitCode}} Error={{.State.Error}} FinishedAt={{.State.FinishedAt}}' app
ExitCode=1 Error= FinishedAt=2026-01-03T10:10:02.002002002Z

Значення: ненульовий код виходу без помилки від рушія; процес сам вирішив завершитись.

Рішення: Розглядайте це як проблему поведінки додатка (обробка залежностей, перевірка конфігурації), а не проблему Docker.

Завдання 7: Підтвердити сервіс-дискавері (DNS) всередині мережі

cr0x@server:~$ docker exec -it app getent hosts db
172.20.0.3      db

Значення: DNS для db резолвиться всередині контейнера.

Рішення: Переходьте до перевірки зʼєднання та автентифікації; DNS сьогодні не є вузьким місцем.

Завдання 8: Протестувати TCP-зʼєднання до залежності з контейнера залежного сервісу

cr0x@server:~$ docker exec -it app bash -lc 'nc -vz db 5432'
nc: connect to db (172.20.0.3) port 5432 (tcp) failed: Connection refused

Значення: DB досяжна по IP, але не приймає TCP. Або DB не слухає, або інтерфейс/порт інший, або вона перезапускається.

Рішення: Перевірте прослуховувані сокети DB і історію рестартів; також перевірте, чи DB не привʼязана лише до localhost.

Завдання 9: Підтвердити, що DB слухає на очікуваному порту всередині свого контейнера

cr0x@server:~$ docker exec -it db bash -lc 'ss -lntp | grep 5432 || true'
LISTEN 0      244          0.0.0.0:5432      0.0.0.0:*    users:(("postgres",pid=1,fd=7))

Значення: PostgreSQL слухає на всіх інтерфейсах.

Рішення: Якщо клієнти все ще бачать connection refused, підозрюйте таймінг (перезапуск DB) або політику мережі/iptables.

Завдання 10: Перевірити команду healthcheck і параметри таймінгу

cr0x@server:~$ docker inspect --format '{{json .Config.Healthcheck}}' db
{"Test":["CMD-SHELL","pg_isready -U postgres -h 127.0.0.1 -p 5432"],"Interval":3000000000,"Timeout":1000000000,"Retries":3,"StartPeriod":0}

Значення: Інтервал 3s, таймаут 1s, retries 3, і StartPeriod = 0. На повільних дисках або під час відновлення це може помітити DB як unhealthy зарано.

Рішення: Додайте початковий період (наприклад, 30–60 с) і збільшіть таймаут. Healthchecks мають виявляти реальні відмови, а не нормальний прогрів.

Завдання 11: Виявити повільне сховище, що затримує готовність

cr0x@server:~$ docker exec -it db bash -lc 'dd if=/var/lib/postgresql/data/pg_wal/000000010000000000000001 of=/dev/null bs=4M count=16 status=none; echo $?'
0

Значення: Базове читання пройшло. Це не доводить продуктивність, але виключає очевидні i/o помилки.

Рішення: Якщо старт все ще повільний, перевірте навантаження хоста на I/O і затримку файлової системи; затримки готовності часто мають коріння в диску.

Завдання 12: Перевірити тиск на ресурси хоста (CPU, памʼять, I/O), що подовжує прогрів

cr0x@server:~$ docker stats --no-stream
CONTAINER ID   NAME    CPU %     MEM USAGE / LIMIT     MEM %     NET I/O       BLOCK I/O
a1b2c3d4e5f6   app     180.12%   512MiB / 1GiB         50.00%    1.2MB / 800KB  12MB / 2MB
b2c3d4e5f6g7   db      95.33%    1.8GiB / 2GiB         90.00%    900KB / 1.1MB  2.3GB / 1.9GB

Значення: DB близька до ліміту памʼяті і активно працює з блоковим I/O. Це рецепт повільної готовності й періодичних збоїв.

Рішення: Збільшіть памʼять, зменшіть shared buffers, перемістіть томи на швидше сховище або зменшіть паралелізм старту. Виправте вузьке місце, перш ніж «налаштовувати» healthchecks так, щоб вони брехали.

Завдання 13: Подивитися граф залежностей Compose і згенерований конфіг

cr0x@server:~$ docker compose config
services:
  app:
    depends_on:
      db:
        condition: service_healthy
    environment:
      DATABASE_URL: postgres://postgres:postgres@db:5432/app
  db:
    healthcheck:
      test:
      - CMD-SHELL
      - pg_isready -U postgres -h 127.0.0.1 -p 5432

Значення: У виводі конфігурації присутній задуманий gating (добре), якщо ваша реалізація Compose це підтримує.

Рішення: Якщо поведінка суперечить конфігу, перевірте версію Compose і рушія; розгляньте перенос gating в entrypoint або реалізацію повторів в додатку для портативності.

Завдання 14: Підтвердити версії Compose і Docker (поведінка залежить від цього)

cr0x@server:~$ docker version --format 'Client={{.Client.Version}} Server={{.Server.Version}}'
Client=27.2.0 Server=27.2.0

Значення: Ви знаєте, на якому рантаймі ви реально працюєте. Це важливо, коли хтось клянеться: «раніше працювало».

Рішення: Узгодьте версії між середовищами або вбудуйте готовність у додаток, щоб ваш стек не залежав від особливостей інструментів.

Завдання 15: Заміряти шлях до готовності

cr0x@server:~$ time docker compose up -d
[+] Running 3/3
 ✔ Container db     Started
 ✔ Container redis  Started
 ✔ Container app    Started

real    0m2.114s
user    0m0.082s
sys     0m0.061s

Значення: Compose звітує «Started» швидко. Це не означає готовність.

Рішення: Заміряйте готовність окремо (статус здоровʼя, ready endpoints, синтетичні перевірки). Не приймайте вивід оркестрації за істину.

Завдання 16: Виконати синтетичну перевірку «чи можу я виконувати реальну роботу»

cr0x@server:~$ docker exec -it app bash -lc 'curl -fsS http://127.0.0.1:8080/readyz && echo READY'
READY

Значення: Додаток заявляє готовність до реального трафіку (за умови, що ваш /readyz чесний).

Рішення: Лише тепер доречно ставити сервіс за лоад-балансер, відкривати фаєрвол або оголошувати деплой завершеним.

Плейбук швидкої діагностики

Коли «контейнери підняті», але система непридатна, час вирішує усе. Трюк — не гадати, яка залежність бреше.
Ось порядок, що швидко знаходить вузьке місце з мінімальним трешем.

По-перше: визначте, який компонент не готовий (а не який «вниз»)

  • Запустіть docker ps і шукайте (health: starting) або (unhealthy).
  • Якщо healthchecks відсутні — це ваша перша проблема. Але діагноз все одно робиться через логи й синтетичні перевірки.

По-друге: скорелюйте часові мітки у логах

  • Зберіть останні 5–10 хв логів з часовими мітками для додатка й залежностей.
  • Шукайте: connection refused, timeouts, auth failures, помилки міграцій, помилки диску.
  • Визначте, чи це таймінгова проблема (прогрів залежності), чи жорстка помилка конфігу (неправильний хост/креденшали).

По-третє: тестуйте з мережевого неймспейсу клієнта

  • Зсередини залежного контейнера перевірте DNS, потім TCP, потім протокол на рівні додатка.
  • Не тестуйте з хоста й не вважайте це еквівалентним. Інший мережевий шлях — інша правда.

По-четверте: перевірте тиск на ресурси й затримки сховища

  • Використовуйте docker stats для виявлення насичення CPU/памʼяті/диску.
  • Якщо DB повільно стає готовою — спочатку підозрюйте I/O. Додатки нетерплячі; диски — вічні.

По-п’яте: вирішіть, який рівень має відповідати за виправлення

  • Виправлення в додатку: retry з backoff/jitter; endpoint готовності; краща обробка помилок.
  • Виправлення в Compose: healthchecks; gating; порядок виконання міграцій.
  • Платформне виправлення: продуктивність сховища; ресурси; уникнення галасливих сусідів.

Три міні-історії з корпоративного життя

Інцидент через неправильне припущення: «запущено» означало «готово»

Середня компанія запускала Docker Compose-стек для внутрішнього порталу білінгу: веб, API, PostgreSQL і фоновий воркер. Деплої були «прості»:
витягнути образи, docker compose up -d, готово. Місяцями все здавалося нормальним. Потім звичайний перезавантаження хоста перетворився
на південноінцидент.

Після ребуту Compose стартував у звичному порядку. API-контейнер піднявся, спробував виконати міграцію під час старту й одразу впав,
бо Postgres все ще відтворював WAL. API вийшов з ненульовим кодом. Політика рестарту піднімала його знову.
Він падав знову. І знову. Міграція ніколи не завершувалась, API не тримався довго, а воркер бив по черзі з повторними спробами без rate-limit.

Панель показувала «контейнери працюють», бо база була запущена, а інші постійно рестартувалися. Он-кал інженер спочатку дивився на лоад-балансер,
бо користувачі бачили 502. Класична відвертка уваги. Лише порівнявши часові мітки в логах стало ясно: API залежав від стану DB, який не був гарантований.

Виправлення було нудним і рішучим: міграції перемістили в one-shot контейнер, що виконувався після того, як DB стала healthy, а API отримав експоненційний backoff
на підключення до DB. Також додали endpoint готовності, який повертав помилку, поки міграції не закінчились.
Наступний ребут пройшов без інцидентів. Команда білінгу не надіслала квітів, але й не прислала злих листів — це SRE-еквівалент успіху.

Оптимізація, що відштовхнулась: healthchecks «підганяли» до брехні

Інша організація мала dev-середовище на Compose, що нагадувало продакшн: мікросервіси, централізована БД і пошукова система.
Розробники скаржились, що стек стартує занадто довго, особливо на ноутбуках. Хтось «оптимізував» healthchecks: інтервали 1s, таймаути 1s, без стартперіоду.
Це прискорило відображення «healthy» у UI у хороший день.

У посередній день — коли пошуковій системі треба було більше часу на ініціалізацію індексів — контейнер позначався unhealthy зарано.
Обгортковий скрипт трактував unhealthy як «зламаний», вбивав і перезапускав його. Вийшов цикл, у якому сервіс ніколи не отримував достатньо часу,
щоби завершити ініціалізацію. Оптимізація не скоротила час старту; вона фактично перешкоджала запуску.

Команда ганялася за привидами: DNS, порти, налаштування Java heap. Усе, окрім очевидного: їхня політика healthcheck саботувала систему.
Вони створили тест готовності, що карав нормальну ініціалізацію.

Згодом додали start_period, збільшили таймаути, і healthcheck для пошуку змінили з «порт відкритий» на «стан кластера yellow/green»
(ознака, що ініціалізація пройшла значущий поріг). Старт трішки затримався. Але він починався кожного разу. Ось що таке «швидше» в продакшні:
менше повторів, менше циклів, менше брехні.

Нудна, але правильна практика, що врятувала ситуацію: синтетичний gate «ready» у CI

Регульована компанія запускала нічні інтеграційні тести проти Compose-середовища. У них була звичка, яка здавалася нудною:
у кожному пайплайні був явний етап «чекати готовність» зі скриптом, що опитував endpoints готовності сервісів і виконував мінімальний DB-запит.
Лише після цього починалися тести. Інженери іноді бурчали через зайву хвилину.

Потім оновили образ БД. Новий образ робив додатковий крок ініціалізації при наявності певного стану файлової системи. На деяких раннерах цей крок займав стільки часу,
що апи стартували й відразу валилися при спробі підключення до DB. Без gating тести почалися під час флапання й дали випадкові фейли.

Натомість пайплайн просто дочекався. Коли готовність не надійшла в межах дедлайну, він ясно впав з повідомленням:
«DB не готова після N секунд». Ніяких флейків у тестах. Ніяких напівзламаних артефактів. Команда відкатила образ і завела внутрішній таск зафіксувати версію, поки не зрозуміють поведінку.

Нудна практика окупилася: відмова була детермінованою, локалізованою й швидкою для діагностики. Ось мрія.
Саме тому я кажу: ставте готовність у ранг першокласного сигналу, а не відчуття.

Типові помилки: симптом → корінь → виправлення

1) «Connection refused» при старті, потім працює після ручного рестарту

Симптом: додаток одразу падає з connection refused до DB/Redis, потім працює, якщо ви перезапустите контейнер додатка.

Корінь: залежність стартувала, але ще не слухає; у додатку немає повторів або вікно повторів замале.

Виправлення: додайте експоненційний backoff повторів у додаток; додайте gate готовності (healthcheck + gating або entrypoint wait) для залежностей.

2) Контейнер додатка «Up», але кожен запит падає

Симптом: стан контейнера — running; healthcheck зелений; користувачі бачать 500.

Корінь: healthcheck тестує лише liveness (порт відкритий), а не readiness (успішність залежностей).

Виправлення: реалізуйте /readyz, що перевіряє критичні залежності; направте healthcheck контейнера на нього.

3) «Unhealthy» під час нормального прогріву, що викликає цикли рестарту

Симптом: сервіс ніколи не стабілізується; влогах повторюються кроки старту.

Корінь: start_period healthcheck занадто короткий або відсутній; обгортка рестартить при unhealthy.

Виправлення: налаштуйте healthcheck start_period і розумний timeout; рестарт тільки при аварійному виході, а не через ранній unhealthy, хіба ви цього дійсно хочете.

4) Випадкові відмови, що зникають, якщо посеріалізувати запуск

Симптом: послідовний старт сервісів працює; одночасний — періодично падає.

Корінь: стадо запитів на спільну залежність (БД, auth, secrets) плюс агресивні повтори.

Виправлення: додайте jitter/backoff; рознесіть старт у часі; збільште можливості залежності; виконуйте міграції окремо.

5) «Authentication failed» при старті, потім потім нормально

Симптом: транзієнтні помилки автентифікації до БД або API відразу після старту.

Корінь: sidecar/агент інжекту секретів ще не готовий; файлові секрети ще не записано; IAM-токен ще недоступний.

Виправлення: готовність має включати «креденшали присутні й валідні»; допустіть запуск лише за цієї умови, а не лише коли процес стартував.

6) Додаток каже ready, але міграції ще тривають

Симптом: endpoint готовності повертає OK, а зміни схеми ще застосовуються; клієнти отримують SQL-помилки.

Корінь: додаток не вважає міграції залежністю готовності, або міграції запускаються паралельно на кількох репліках.

Виправлення: перемістіть міграції в окремий job; готовність додатка має бути false, доки версія схеми сумісна.

7) «Працює на моїй машині», не працює на повільному хості

Симптом: ноутбуки розробників ок, CI-ранери або маленькі ВМ падають при старті.

Корінь: таймінгові припущення в старті; відсутність backoff; healthchecks занадто жорсткі; сховище повільніше.

Виправлення: збільште tolerance windows; заміряйте реальний час до готовності; виправте найповільнішу залежність, замість того щоб її приховувати.

8) Контейнери показують healthy, але мережевий шлях для реальних клієнтів зламаний

Симптом: внутрішні healthchecks зелені; зовнішні клієнти часом таймаутяться.

Корінь: healthcheck тестує лише localhost; сервіс привʼязаний до неправильного інтерфейсу; публікація портів/ingress неправильно налаштовані.

Виправлення: тестуйте готовність тим самим шляхом, яким користуються клієнти, або додайте синтетичну перевірку ззовні контейнерної мережі.

Чеклісти / покроковий план

Покроковий план: виправлення хибних стартів у існуючому Compose-стеку

  1. Інвентаризація залежностей. Для кожного сервісу випишіть, що йому справді потрібно для обслуговування трафіку:
    підключення до БД, черги, кеш, секрети, записувана файлова система, версія схеми.
  2. Додайте endpoint готовності (або еквівалент) для кожного сервісу додатка. Якщо це не HTTP, реалізуйте невелику CLI-перевірку,
    що виконує мінімальну реальну операцію (наприклад, DB-запит).
  3. Визначте Docker healthchecks, що відображають готовність. Не хітіть vanity-endpoint; хітіть той, що перевіряє залежності.
  4. Реалістично налаштуйте таймінги healthcheck. Додайте start_period для відомих прогрівів, використайте таймаути, що відповідають найповільнішому нормальному старту.
  5. Оберіть стратегію gating. Якщо ваше середовище підтримує gating у Compose на основі health — використайте його. Інакше робіть gating в entrypoint або логіці додатка.
  6. Реалізуйте повтори в додатках з backoff і jitter. Це обовʼязково для систем, що мають пережити перезапуски і деплої.
  7. Розділіть міграції у виділений job. Виконуйте їх один раз, з локом, і явно фейліть, якщо завершити не вдається.
  8. Додайте синтетичну перевірку «стек готовий». Щось, що перевіряє шлях користувача: логін, отримання даних, запис запису.
  9. Замірте time-to-ready. Фіксуйте часові мітки в логах і слідкуйте за median/p95 часу старту; налаштовуйте healthchecks на підставі даних.
  10. Протестуйте найгірший сценарій. Перезавантажте хост, обмежте CPU, симулюйте повільний диск. Якщо модель готовності це витримає — витримає й у реальному житті.

Чекліст: як виглядає «добре»

  • Кожен сервіс має змістовний сигнал готовності.
  • Кожна залежність має healthcheck, що відповідає її придатності.
  • Жоден сервіс не виходить при першій невдачі зʼєднання; повтори обмежені й логуються.
  • Міграції виконуються разово, явно, а не як побічний ефект «старта апа».
  • Healthchecks толерантні до нормального прогріву; вони ловлять реальні deadlock-і та неправильні конфіги.
  • Стек має щонайменше одну end-to-end синтетичну перевірку, що використовується в CI або при деплої.

Чекліст: чого уникати (бо завжди кусає)

  • Жорстко зашиті sleep-и як «керування залежностями».
  • Healthchecks, що лише перевіряють «порт відкритий».
  • Цикли рестарту як стандартна стратегія відновлення.
  • Кілька реплік, що змагаються за міграції при старті.
  • Таймаути налаштовані під найшвидший ноутбука замість найповільнішого нормального середовища.

FAQ

1) Хіба depends_on не достатньо для більшості стеків?

Ні. Воно відповідає за послідовність запуску процесів, а не за придатність сервісу. Ви все ще отримаєте гонки на повільних дисках, після крашів
або коли залежності роблять внутрішнє відновлення.

2) Де краще робити gating: у Compose чи в додатку?

Всередині додатка це портативніше й правильніше. Gating у Compose — корисний шар, але ваш додаток працюватиме в більше місцях,
ніж Compose: різні хости, CI, Kubernetes, systemd, можливо bare metal. Повторювання залежностей — відповідальність додатка.

3) У чому різниця між liveness і readiness?

Liveness означає «процес живий». Readiness означає «сервіс може виконувати свою роботу для клієнтів». Їх не слід плутати.
Якщо ви змішаєте їх, то або вбʼєте здорові, але зайняті сервіси, або направите трафік до зламаних, але працюючих.

4) Якщо мій сервіс повторює спроби вічно, хіба це не надійно?

Це стійко, але не обовʼязково надійно. Нескінченні повтори можуть ховати відмови й створювати стабільне навантаження на залежності.
Використовуйте backoff, jitter і максимальний час очікування з ясною звітністю про помилку.

5) Що таке «хибний старт» у цьому контексті?

Хибний старт — це коли оркестратор звітує, що сервіси запущені (або навіть healthy), але система фактично не може коректно обслуговувати трафік.
Часто це вирішується «само собою», що робить проблему легкою для ігнорування, поки вона не спалить вас у продакшні.

6) Як написати хороший healthcheck для бази даних?

Віддавайте перевагу нативним інструментам готовності бази (наприклад, pg_isready) і, за потреби, мінімальному запиту,
що перевіряє автентифікацію й правильну базу. Будьте обережні: запит може бути дорогим, якщо виконувати його занадто часто.

7) Чому контейнери нормально стартять в dev, але падають у CI?

CI-ранери часто повільніші, більш завантажені і варіабельні. Таймінгові припущення першими ламаються там. Додайте start_period,
використайте endpoints готовності і заміряйте time-to-ready в обох середовищах.

8) Чи достатньо TCP-перевірки (nc) для готовності?

Це корисний перший крок, але не фінальна відповідь. Відкритий TCP означає, що сокет існує; це не гарантує, що автентифікація працює,
схема правильна або сервіс не повертає помилки.

9) Чи healthchecks можуть шкодити продуктивності?

Так. Частий важкий healthcheck (наприклад, повільний SQL-запит) може стати самошкоджуваним навантаженням. Тримайте перевірки легкими, знижуйте частоту
і використовуйте стартперіоди замість агресивного опитування.

10) Яке найпростіше покращення з найбільшим ефектом?

Додайте реальний endpoint готовності до додатка і націльте healthcheck на нього. Потім реалізуйте повтори з backoff для DB і черг.
Ці дві зміни усувають більшість нестабільностей при старті.

Висновок: наступні кроки, які можна зробити цього тижня

Ставтеся до «запущено» як до механічної події, а не умови успіху. Якщо ви хочете менше інцидентів, припиніть просити Docker виводити готовність
і почніть давати йому сигнали, що відповідають реальній придатності.

Практичні наступні кроки:

  1. Додайте endpoints готовності (або мінімальні перевірки реальних операцій) до сервісів ваших аплікацій.
  2. Покращіть healthchecks, щоб вони тестували готовність, а не лише «порт відкритий», і налаштуйте start_period відповідно до реальності.
  3. Реалізуйте в додатках повтори з backoff і jitter для кожної зовнішньої залежності.
  4. Розділіть схемні міграції на окремий, одноразовий крок з чіткою семантикою успіх/фейл.
  5. Опануйте плейбук швидкої діагностики і зробіть його «м’язовою памʼяттю»: health, логи, перевірки в контейнері, потім тиск ресурсів.

Хибні старти — не доля. Це прогалина в дизайні. Закрийте її, і ваша епоха «воно працює після рестарту» нарешті закінчиться.

← Попередня
SPF/DKIM проходять, але листи потрапляють у спам: приховані сигнали, які потрібно виправити
Наступна →
Debian 13: Відновлення доступу root через rescue mode — не погіршіть ситуацію

Залишити коментар