Health Check завершується помилкою, хоча застосунок працює
Якщо під час deployment health check завершується помилкою, хоча локально застосунок працює без проблем, зазвичай причина одна з п’яти. Перевірте binding, path, port, timing і dependencies у порядку, який найшвидше допоможе знайти проблему.
Є особливий різновид зависання: health check завершується помилкою під час deployment, блокуючи кожен release, хоча застосунок за всіма доступними вам ознаками повністю справний. Локально він працює. У локальному Docker він працює. У логах видно, що він слухає порт. А платформа раз за разом повідомляє про помилку — іноді двадцять разів поспіль, — і при цьому в access log не з’являється жодного запиту.
Остання деталь особливо важлива, адже вона одразу звужує коло пошуку. Якщо застосунок жодного разу не записав запит у лог, перевірка не дійшла до застосунку — отже, у коді застосунку не знайти пояснення цієї проблеми.
Ось п’ять причин у порядку, який найшвидше допоможе знайти проблему.
1. Ви прив’язалися до localhost
Це найпоширеніша причина, і саме вона пояснює симптом «трафік узагалі не надходить».
Усередині контейнера 127.0.0.1 означає власний loopback цього контейнера. Health check іззовні контейнера не може до нього дістатися. Процес слухає порт, у логах це видно, але socket недоступний з будь-якого місця, яке має значення.
// Unreachable from outside the container
app.listen(3000, '127.0.0.1')
// Correct
app.listen(3000, '0.0.0.0')
У різних framework значення за замовчуванням відрізняються, а деякі з них змінили його між major-версіями. Перевірте, до якої адреси насправді прив’язується ваш framework, а не ту, яку ви пам’ятаєте.
# Confirm from inside the running container
dockup exec "ss -ltn || netstat -ltn" my-project/my-api
Якщо адреса, на якій прослуховується порт, — 127.0.0.1:3000, а не 0.0.0.0:3000, ви знайшли причину, і решта пунктів у цьому списку вже не має значення.
2. Порт, який перевіряє платформа, відрізняється від порту, на якому ви обслуговуєте запити
Тут задіяні два порти, і їх легко переплутати: порт, на якому процес слухає запити всередині контейнера, і порт, на який спрямовує трафік платформа. Якщо застосунок читає PORT із середовища, а десь у Dockerfile ви жорстко вказали 3000, ці значення можуть непомітно розійтися.
Надійний підхід — дозволити платформі повідомити вам потрібне значення:
const port = process.env.PORT || 3000
app.listen(port, '0.0.0.0')
Після цього один раз задайте порт сервісу на платформі й не підтримуйте це число у двох різних місцях.
3. Відповідь на path не є успішною
Path для health check перевіряється точно, і напрочуд часто причина помилки — redirect. Якщо застосунок перенаправляє /healthz на /healthz/ або примусово вмикає HTTPS за допомогою 301, checker, який вважає успішними лише відповіді 2xx, щоразу завершуватиме роботу з помилкою, тоді як браузер перейде за redirect і покаже вам робочу сторінку.
Ось три конкретні пастки:
- Redirect через кінцевий слеш.
/healthz→/healthz/— це 301. - Примусовий HTTPS. Внутрішня перевірка зазвичай надходить через звичайний HTTP на loopback. Безумовний redirect на HTTPS призведе до помилки.
- Auth middleware. Глобальний authentication guard, який запускається до routing, також поверне 401 для health path.
Явно виключіть health path з auth і з примусового HTTPS. Це єдиний route, який має бути максимально простим.
4. Перевірка завершується швидше, ніж ваш cold start
Якщо перевірка кілька разів завершується помилкою, а потім проходить, або завершується помилкою під час deploy, але проходить після повторної спроби, причина в timing, а не в конфігурації.
Вам потрібен бюджет не на одну спробу, а на interval × retries. Застосунку, якому потрібно дванадцять секунд, щоб під’єднатися до бази даних і прогріти cache, потрібен загальний бюджет понад дванадцять секунд. Інакше кожен release завершуватиметься помилкою, а зрештою ви вимкнете gate — і приберете єдиний захист між зламаною збіркою та вашими користувачами.
dockup info my-project/my-api --json | grep -A6 healthCheck
Встановіть timeout, більший за найдовшу допустиму окрему спробу, а кількість retries — так, щоб interval × retries із запасом перевищував найдовший допустимий boot. Не вгадуйте час запуску — виміряйте його: у логах є timestamps.
5. Застосунок справді не готовий
Останній випадок — саме той, для якого існує check: застосунок запустився, не зміг під’єднатися до dependency і повторює спроби. Він не впав, тому нічого не перезапускається. Він не може обслуговувати запити, тому check завершується помилкою. Система працює саме так, як задумано, і повідомляє, що цей release не має отримувати трафік.
Відрізнити цей випадок від чотирьох інших можна за тим, що застосунок записав запит у лог і відповів кодом, відмінним від 2xx. Якщо запит є в логах, причини 1–3 можна виключити.
Порядок діагностики, який заощаджує час
# 1. Did the request reach the app at all?
dockup logs my-project/my-api --follow
# 2. What is the process actually bound to?
dockup exec "ss -ltn || netstat -ltn" my-project/my-api
# 3. Does the path answer from inside the container?
dockup exec "curl -si localhost:3000/healthz" my-project/my-api
# 4. What is the gate configured to expect?
dockup info my-project/my-api --json
Саме крок 3 найчастіше допомагає розв’язати проблему. curl зсередини контейнера одночасно усуває всі мережеві змінні: якщо там він повертає 200, а платформа все одно повідомляє про помилку, проблема в address або port, а не в застосунку. Якщо він повертає 301 або 401, ви знайшли причину, навіть не змінюючи налаштувань платформи.
Чому gate варто залишити увімкненим
Після четвертого невдалого deploy дуже легко піддатися спокусі вимкнути health check і випустити release. Варто пам’ятати, що саме ви вимикаєте.
У Dockup health gate не дає зламаному release дістатися до ваших користувачів. Нова версія збирається і запускається, поки поточна продовжує обслуговувати запити; трафік переходить на нову версію лише після того, як вона починає відповідати. Вимкнувши gate, ви знову дозволяєте сценарій, за якого контейнер, що запускається, але не може працювати, замінює справний контейнер.
Check, який чотири release поспіль завершується помилкою, — це дратує. Check, який безумовно проходить, не зупинить той deploy, який справді має значення.
Поширені запитання
Чому health check завершується помилкою, хоча застосунок працює локально?
Майже завжди тому, що контейнер прив’язаний до 127.0.0.1, а не до 0.0.0.0. Локально ви під’єднуєтеся через той самий loopback, а ззовні контейнера ця адреса недоступна.
Чи має health endpoint вимагати authentication? Ні. Виключіть його з global auth middleware, інакше checker отримає 401, а deploy завершиться помилкою, хоча застосунок працює.
Який timeout слід використовувати? Він має бути довшим за найдовшу допустиму окрему спробу, а retries повинні охоплювати найдовший допустимий cold start. Визначте час запуску за логами, а не вгадуйте його.
Чи безпечно вимкнути health check, щоб розблокувати release? Це розблокує release, але прибере захист, який не дає зламаній версії отримати трафік. Натомість виправте check — найчастіше причина в bind address або redirect, і це займає лише кілька хвилин.
