SSD Nodes Learn 8GB RAM — $66/рік
Посібники Matt ConnorВід Matt Connor · Оновлено 2026-08-01

Як правильно налаштувати healthcheck у Docker Compose

Дізнайтеся, як Compose оцінює healthcheck, чому depends_on не перевіряє готовність і як написати надійні перевірки для Postgres та застосунку.

Що насправді робить healthcheck у Docker Compose

healthcheck у Docker Compose — це одна команда, яку Docker запускає всередині контейнера за розкладом. Docker не читає ваші журнали, не відстежує порт і не перевіряє список процесів. Він запускає команду, зчитує код завершення та зберігає для контейнера один стан: starting, healthy або unhealthy. Код завершення 0 означає, що контейнер справний. Будь-який інший код означає несправний стан, а код завершення 2 зарезервований Docker, тому ніколи не повертайте його навмисно.

Це весь механізм. Майже кожна проблема з healthcheck має одну причину: написана вами команда відповідає на інше запитання, ніж те, яке ви хотіли поставити. Цей посібник передбачає, що ви вже знаєте як написати compose-файл на VPS, і продовжує з моменту, коли стек запускається в неправильному порядку.

services:
  api:
    image: ghcr.io/example/api:1.4.0
    healthcheck:
      test: ["CMD", "curl", "-fsS", "http://localhost:8080/healthz"]
      interval: 10s
      timeout: 3s
      retries: 5
      start_period: 30s

Значення test має дві корисні форми. Список, що починається з CMD, запускає команду безпосередньо, без оболонки, тому конвеєри, && і розгортання змінних не працюють. Список, що починається з CMD-SHELL, передає решту як один рядок до /bin/sh -c усередині контейнера. Саме цей варіант потрібен, коли перевірка використовує синтаксис оболонки. Рядок без списку обробляється як CMD-SHELL. Список, що містить лише ["NONE"], видаляє healthcheck, вбудований в образ через його Dockerfile.

Перевірка виконується всередині контейнера, тому кожен двійковий файл, указаний у команді, має існувати в цьому образі. Спочатку перевірте це, оскільки slim-образ без curl створює контейнер, який постійно має стан «несправний», і ця причина ніколи не з’являється в журналі застосунку. Перевірте команду вручну:

docker compose exec api curl --version

Якщо двійковий файл відсутній, команда повертає OCI runtime exec failed: exec: "curl": executable file not found in $PATH: unknown. В образах на основі Alpine зазвичай використовується BusyBox wget, тому перевірка набуває вигляду ["CMD", "wget", "-q", "-O", "-", "http://localhost:8080/healthz"].

Як поєднуються interval, retries і start_period

П’ять параметрів керують часом виконання перевірок. Їхні значення за замовчуванням надходять із Docker Engine, а не з Compose.

  • interval: інтервал між двома перевірками після завершення start period контейнера. Значення за замовчуванням — 30s.
  • timeout: максимальна тривалість одного запуску перевірки, після якої Docker примусово завершує його та зараховує як помилку. Значення за замовчуванням — 30s.
  • retries: кількість послідовних помилок, потрібна для переходу стану до unhealthy. Значення за замовчуванням — 3.
  • start_period: період очікування після запуску контейнера. Значення за замовчуванням — 0s.
  • start_interval: частота запуску перевірки протягом start period. Значення за замовчуванням — 5s; для цього потрібен Docker Engine версії 25.0 або новішої.

Важливе правило: протягом start period невдала перевірка не зараховується до retries, а контейнер залишається у стані starting. Коли перевірка вперше завершується успішно, контейнер переходить у стан healthy, а start period негайно завершується, навіть якщо більша частина цього періоду ще не минула. Якщо start period завершується, коли перевірка все ще неуспішна, запускається звичайний відлік, і контейнеру потрібно retries послідовних помилок, перш ніж його буде позначено як unhealthy.

Отже, максимальний час від запуску контейнера до unhealthy становить start_period плюс retries, помножене на interval, плюс timeout. Для значень із наведеного вище файлу це 30 плюс 5, помножене на 13, тобто 95 секунд. Запишіть це число перед встановленням тайм-ауту розгортання, оскільки розгортання, яке припиняється через 60 секунд, ніколи не дочекається переходу цього контейнера до кінцевого стану.

Поширена помилка — збільшити retries, щоб врахувати повільний запуск. Це спрацює один раз, а потім назавжди погіршить контроль: служба, якій для запуску було потрібно 8 повторних спроб, тепер у production допускатиме 8 послідовних помилок, перш ніж це стане помітно. Натомість використовуйте start_period, оскільки він застосовується лише до першої успішної перевірки.

Чому depends_on сам по собі нічого не гарантує

Скорочена форма depends_on є основним джерелом плутанини.

  api:
    depends_on:
      - db

Це означає лише одне: запустити контейнер db перед контейнером api. Compose очікує, поки контейнер буде створено та запущено. Він не очікує завершення першої ініціалізації PostgreSQL і не очікує, поки порт 5432 почне приймати з'єднання. Ваш застосунок запускається приблизно через секунду, підключається до порту, на якому ще нічого не прослуховується, і завершує роботу. У журналі ви бачите Connection refused або FATAL: the database system is starting up, якщо сервер уже запущено, але він ще відновлюється.

Розширена форма відповідає реальній потребі:

  api:
    depends_on:
      db:
        condition: service_healthy
        restart: true
      migrate:
        condition: service_completed_successfully

condition має три значення. service_started означає те саме, що й скорочена форма. service_healthy затримує запуск залежного сервісу, доки залежність не повідомить про стан healthy. Це має сенс лише тоді, коли для цієї залежності визначено healthcheck — у compose-файлі або в її образі. service_completed_successfully очікує завершення однократного контейнера, наприклад контейнера міграції бази даних, зі статусом 0.

Поруч із condition є ще два поля. restart: true вказує Compose перезапустити цей сервіс після оновлення залежного сервісу. required: false перетворює відсутню залежність із помилки на попередження.

Тепер про обмеження, яке часто стає несподіванкою. Ці умови перевіряються під час запуску стека. Вони визначають порядок запуску, а не виконують функцію нагляду. Якщо база даних перезапуститься о третій годині ночі, service_healthy не буде повторно перевірено, і застосунок не буде перезапущено для повторного виконання цієї умови. Код застосунку все одно має самостійно повторно встановлювати з'єднання. docker compose up --no-deps api навмисно обходить увесь цей механізм. Те саме відбувається під час безпосереднього запуску контейнера за допомогою docker start.

Напишіть перевірку готовності, а не наявності процесу

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

Доручіть контейнеру виконати роботу, для якої його запущено:

  • Для HTTP-служби надішліть запит до реальної кінцевої точки. curl -fsS завершується з ненульовим кодом для будь-якого статусу 400 або вище через -f, тому код 500 від несправного застосунку означає невдалу перевірку.
  • Для PostgreSQL використовуйте pg_isready. Команда завершується з кодом 0, коли сервер приймає підключення, з кодом 1, коли він їх відхиляє, з кодом 2, коли взагалі не відповідає, і з кодом 3, коли передані параметри неправильні.
  • Для Redis використовуйте redis-cli ping. Команда виводить PONG і завершується з кодом 0.
  • Для MariaDB офіційний образ містить скрипт healthcheck.sh, а healthcheck.sh --connect --innodb_initialized — це форма, яку документують його розробники.

У pg_isready є одна особливість, про яку варто знати. Під час першого запуску з порожнім каталогом даних офіційний образ postgres виконує ініціалізацію на тимчасовому сервері, який прослуховує лише Unix-сокет. pg_isready без аргументу host використовує цей сокет, тому може повідомити «підключення приймаються», хоча TCP-порт 5432 ще закритий для вашого застосунку. Явно вкажіть TCP для перевірки, і проблема зникне, оскільки тимчасовий сервер не відповідає через нього.

    healthcheck:
      test: ["CMD-SHELL", "pg_isready -h 127.0.0.1 -p 5432 -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
      interval: 5s
      timeout: 5s
      retries: 10
      start_period: 30s

Подвійні знаки долара не є помилкою. Compose сам розгортає $VAR під час читання файлу, через що значення з оточення вашого хоста потрапило б у перевірку. $$ екранує його до одинарного $, тому оболонка всередині контейнера розгортає його на основі власного оточення контейнера.

Стек PostgreSQL і застосунку, який запускається у правильному порядку

services:
  db:
    image: postgres:17.5
    environment:
      POSTGRES_USER: appuser
      POSTGRES_PASSWORD: ${DB_PASSWORD:?set DB_PASSWORD in .env}
      POSTGRES_DB: appdb
    volumes:
      - pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -h 127.0.0.1 -p 5432 -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
      interval: 5s
      timeout: 5s
      retries: 10
      start_period: 30s
    restart: unless-stopped

  api:
    image: ghcr.io/example/api:1.4.0
    environment:
      DATABASE_URL: postgres://appuser:${DB_PASSWORD}@db:5432/appdb
    depends_on:
      db:
        condition: service_healthy
    healthcheck:
      test: ["CMD", "curl", "-fsS", "http://localhost:8080/healthz"]
      interval: 10s
      timeout: 3s
      retries: 5
      start_period: 30s
    ports:
      - "127.0.0.1:8080:8080"
    restart: unless-stopped

volumes:
  pgdata:

Запустіть його та спостерігайте за зміною станів:

docker compose up -d
docker compose ps

Стовпець STATUS містить стан готовності в дужках. Для справної пари в обох рядках має бути Up 41 seconds (healthy). Поки база даних ще ініціалізується, у db відображається Up 4 seconds (health: starting), а api відсутній у списку, оскільки Compose ще не створив його.

Щоб з’ясувати, чому перевірка завершилася успішно або невдало, перегляньте журнал стану готовності:

docker inspect --format '{{json .State.Health}}' "$(docker compose ps -q db)"

Docker зберігає кілька останніх результатів. Кожен результат містить час початку, час завершення, ExitCode і Output команди. Збережені вихідні дані обрізаються, тому перевірка, яка виводить великий вміст сторінки, створює непридатний запис у журналі. Перевірки мають виводити мінімум даних.

Що робить Docker, коли контейнер переходить у стан unhealthy

Нічого. Саме ця відповідь найбільше дивує людей.

Docker Engine на одному хості не перезапускає контейнер у стані unhealthy. Політика restart: unless-stopped реагує на завершення головного процесу, а контейнер у стані unhealthy не завершив роботу. Він може залишатися у стані unhealthy протягом тижня, поки Compose не вживає жодних дій. Режим Swarm замінює нездорові завдання, але звичайний стек Compose на одному сервері — ні.

Тут є два практичні варіанти. Завершуйте роботу процесу, коли він виявляє несправність, щоб політика перезапуску могла відреагувати. Або відстежуйте стан ззовні та налаштуйте сповіщення для цього стану. Якщо спрямувати монітор Uptime Kuma на ту саму кінцеву точку, яку викликає healthcheck, несправна залежність буде видимою в обох місцях, і про неї повідомить монітор, а не користувач. Якщо трафік надходить до застосунку через зворотний проксі Traefik, пам’ятайте, що власне уявлення проксі про backend не залежить від стану Docker health. Тому один механізм не замінює інший.

Налагодження перевірки, яка ніколи не переходить у стан healthy

Виконайте точну команду самостійно в тому самому контейнері та перевірте код завершення:

docker compose exec api curl -fsS http://localhost:8080/healthz; echo "exit=$?"

exit=0 тут, коли контейнер і далі повідомляє про стан unhealthy, означає, що ваш compose test відрізняється від щойно введеної команди. Зазвичай це відбувається через використання CMD там, де потрібен синтаксис shell.

Більшість інших випадків спричиняють дві помилки. Перша — неправильний порт. Перевірка стану виконується всередині контейнера, тому потрібно використовувати порт контейнера, а не опублікований порт хоста. У ports: - "8080:3000" застосунок прослуховує порт 3000, тому перевірка через http://localhost:8080 постійно завершується помилкою, хоча сайт нормально працює у браузері. Друга — неправильний хост. Під час перевірки localhost — це той самий контейнер. Це правильно для перевірки самого контейнера, але неправильно для перевірки сусіднього контейнера. У такому разі потрібно використовувати ім’я сервісу, наприклад db.

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

FAQ

Чому мій застосунок не може підключитися, хоча в depends_on зазначено, що база даних працездатна?

Тому що condition: service_healthy оцінюється один раз під час запуску стека. Після цього він нічого не контролює. Якщо контейнер бази даних пізніше перезапуститься, Compose не перезапустить застосунок, щоб знову виконати цю умову. Тому код застосунку має містити власну логіку повторного підключення та повторних спроб. Ця умова також не працює, коли ви запускаєте окремий контейнер за допомогою docker start або docker compose up --no-deps.

Чи потрібен healthcheck, якщо образ уже визначає його?

Зазвичай ні. Перевизначення часто погіршує ситуацію, оскільки супроводжувач образу знає, що означає готовність саме для цього програмного забезпечення. Додавайте власну перевірку лише тоді, коли перевірка в образі не відповідає вашій конфігурації, наприклад перевіряє порт, який ви змінили. Щоб вимкнути healthcheck образу, установіть test: ["NONE"] або disable: true для сервісу.

Чи має healthcheck використовувати curl або wget?

Використовуйте той інструмент, який уже є в образі, і перевірте його наявність за допомогою docker compose exec <service> curl --version, перш ніж покладатися на нього. У багатьох образах на основі Debian немає жодного з них. В образах на основі Alpine є wget з BusyBox. Не додавайте пакет до образу лише для запуску healthcheck, якщо програмне забезпечення постачає власний клієнт, наприклад pg_isready або redis-cli.

Чи перезапускається непрацездатний контейнер автоматично?

Docker Engine не робить цього на одному хості. Політики перезапуску реагують на завершення процесу, а не на стан healthcheck. Тому непрацездатний контейнер продовжує працювати й залишається несправним, доки інший компонент не виконає певну дію. Або завершіть процес, коли він виявляє помилку, або запустіть зовнішній моніторинг, який надсилатиме сповіщення про цей стан.

Якою має бути тривалість start_period?

Вона має бути достатньою для найдовшого виміряного вами коректного першого запуску, із додатковим запасом. Виміряйте цей час за допомогою docker compose up на порожньому томі, оскільки перший запуск бази даних значно довший за всі наступні. Надто тривалий період запуску лише затримує перший вердикт unhealthy. Надто велика кількість повторних спроб послаблює перевірку протягом усього часу роботи контейнера. Це гірший варіант помилки.

#docker-compose#healthcheck#depends-on#docker#reliability