MAATRIX / Блог / Docker healthcheck: настройка

Docker healthcheck: настройка

Docker healthcheck: настройка

MAATRIX

Docker показывает статус контейнера как running, даже если приложение внутри уже зависло, упало в бесконечный цикл или перестало отвечать на запросы, — сам по себе контейнер этого не замечает и не сообщит о проблеме. docker ps продолжит рапортовать «всё хорошо», пока пользователи получают таймауты. HEALTHCHECK решает именно эту проблему: он запускает внутри контейнера проверку по расписанию и честно показывает, живо ли приложение, а не только жив ли процесс. Разберём, как настроить HEALTHCHECK в Dockerfile и docker-compose и связать его с автоматическим перезапуском.

Почему статус running не гарантирует работоспособность

Docker считает контейнер живым, пока жив его главный процесс (PID 1). Это узкое определение: процесс может быть запущен, но зависнуть в дедлоке, исчерпать пул соединений к базе, застрять в бесконечном ожидании ответа от внешнего API — и при этом ничего не писать в логи и не завершаться. Для Docker это по-прежнему running, потому что процесс формально существует.

Типичный пример — Node.js или Java-приложение, у которого event loop заблокирован синхронной операцией, или веб-сервер, у которого исчерпаны воркеры и новые запросы просто копятся в очереди без ответа. Балансировщик или пользователь видит зависший сервис, а инфраструктура — «здоровый» контейнер. HEALTHCHECK закрывает этот разрыв: вместо вопроса «жив ли процесс» он задаёт вопрос «отвечает ли приложение корректно», выполняя реальную проверку — HTTP-запрос, TCP-соединение или скрипт — с заданной периодичностью.

Синтаксис директивы HEALTHCHECK в Dockerfile

Директива добавляется в Dockerfile и описывает, как и с какой частотой проверять контейнер:

FROM node:20-alpine
WORKDIR /app
COPY . .
RUN npm ci --production

HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
  CMD curl -f http://localhost:3000/health || exit 1

CMD ["node", "server.js"]

Ключевые моменты синтаксиса:

  • HEALTHCHECK [опции] CMD команда — сама проверка, её код возврата определяет статус.
  • Команда должна завершаться кодом 0 (healthy), 1 (unhealthy) или ничем не выполняться дольше timeout (тогда попытка тоже считается неудачной).
  • HEALTHCHECK NONE отключает healthcheck, унаследованный от базового образа — полезно, если базовый образ уже содержит свою проверку, которая вам не подходит.
  • В образе может быть только одна директива HEALTHCHECK — если их несколько, действует последняя.

Проверка выполняется процессом внутри самого контейнера, поэтому в команде используется localhost, а не имя сервиса или внешний IP — с точки зрения healthcheck-команды вы обращаетесь к собственному процессу.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать VPS

Параметры interval, timeout, retries, start-period

Четыре параметра управляют тем, как часто и как строго Docker проверяет контейнер:

ПараметрПо умолчаниюЧто делает
--interval30sПауза между проверками
--timeout30sСколько ждать ответа от команды проверки, прежде чем засчитать неудачу
--retries3Сколько подряд неудачных проверок нужно, чтобы статус стал unhealthy
--start-period0sВремя после старта контейнера, в течение которого неудачные проверки не считаются в счётчик retries

start-period — параметр, который чаще всего забывают, а зря. Приложению нужно время на инициализацию: прогрев соединения к базе, чтение конфигов, JIT-компиляцию. Без start-period первые несколько неудачных проверок сразу начнут копиться в счётчик retries, и контейнер может улететь в unhealthy ещё до того, как реально стартовал. Ставьте start-period чуть больше, чем реальное время холодного старта приложения — для тяжёлых Java-сервисов это может быть 30-60 секунд, для лёгкого HTTP-сервиса на Go — 5-10 секунд. Ориентируйтесь на своё приложение, эти цифры сильно зависят от стека и ресурсов сервера.

Логика проста: Docker ждёт interval секунд, выполняет проверку, ждёт от неё ответа не дольше timeout. Если проверка провалилась (ненулевой код или не уложилась в timeout) retries раз подряд, контейнер помечается unhealthy. Одна успешная проверка сбрасывает счётчик обратно в ноль.

Практический пример: HTTP-эндпоинт и проверка процесса

Самый надёжный вариант — свой health-эндпоинт, который реально проверяет работоспособность приложения, а не просто отвечает 200 OK на любой запрос:

HEALTHCHECK --interval=15s --timeout=3s --retries=3 \
  CMD curl -sf http://localhost:8080/health || exit 1

Флаг -f у curl важен: без него curl вернёт код 0 даже на ответ 500, потому что сам HTTP-запрос прошёл успешно — а -f заставляет curl завершиться с ошибкой при статусах 4xx/5xx. Хороший /health-эндпоинт стоит делать «умным»: проверять не только то, что веб-сервер отвечает, но и доступность зависимостей — соединение с базой, доступность кэша. Но не переусердствуйте: если /health дергает пять внешних сервисов, любой сбой одного из них будет валить весь healthcheck без необходимости.

Если в образе нет curl (например, минималистичный Alpine или distroless), варианты:

# через wget, часто уже есть в alpine
HEALTHCHECK CMD wget -q --spider http://localhost:8080/health || exit 1

# без HTTP вообще — проверка, что процесс слушает порт
HEALTHCHECK CMD nc -z localhost 8080 || exit 1

# проверка процесса по имени, если HTTP-эндпоинта нет
HEALTHCHECK CMD pgrep -f "node server.js" || exit 1

Проверка через pgrep слабее HTTP-проверки — она подтверждает, что процесс запущен, но не что он отвечает на запросы. Используйте её только когда у приложения в принципе нет сетевого интерфейса для проверки (воркеры очередей, cron-подобные процессы).

HEALTHCHECK в docker-compose

В docker-compose.yml та же логика описывается ключом healthcheck на уровне сервиса — это удобно, когда вы не хотите пересобирать образ ради изменения параметров проверки:

services:
  web:
    image: myapp:latest
    ports:
      - "8080:8080"
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
      interval: 15s
      timeout: 3s
      retries: 3
      start_period: 10s
    restart: unless-stopped

  db:
    image: postgres:16
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 10s
      timeout: 5s
      retries: 5
    restart: unless-stopped

Формат test бывает двух видов: ["CMD", "команда", "аргумент1", ...] выполняет команду напрямую без обращения к shell, а ["CMD-SHELL", "команда"] запускает её через /bin/sh -c, что нужно, если используете пайпы, переменные окружения или составные условия. Значения в compose переопределяют то, что задано в Dockerfile образа, — это удобно для тестового окружения, где вы хотите более частые проверки, чем в проде.

Отдельный практический плюс healthcheck в compose — директива depends_on с условием:

services:
  web:
    depends_on:
      db:
        condition: service_healthy

Так web стартует не просто после запуска контейнера db, а только после того, как db реально пройдёт healthcheck и станет готов принимать соединения. Это надёжнее, чем depends_on без условий, который просто дожидается, пока контейнер запустится — база может быть ещё не готова принимать подключения к моменту старта зависимого сервиса.

Как посмотреть статус healthcheck

Статус здоровья виден прямо в выводе docker ps — в колонке STATUS появляется пометка (healthy), (unhealthy) или (health: starting) в период start-period:

docker ps
CONTAINER ID   IMAGE       STATUS                    PORTS
a1b2c3d4e5f6   myapp       Up 2 minutes (healthy)    0.0.0.0:8080->8080/tcp

Для детальной картины — история последних проверок, их вывод и коды возврата — используйте docker inspect:

docker inspect --format='{{json .State.Health}}' имя_контейнера | jq

Вывод покажет текущий статус (Status), число неудач подряд (FailingStreak) и массив Log с последними попытками — временем запуска, кодом выхода и выводом команды проверки. Это первое место, куда стоит смотреть, если контейнер помечен unhealthy, а логи приложения ничего подозрительного не показывают: часто причина как раз в самой команде проверки — неверный порт, эндпоинт ещё не готов, curl не установлен в образе.

Если нужен только статус без лишнего:

docker inspect --format='{{.State.Health.Status}}' имя_контейнера

Связка с restart policy для автоперезапуска

Сам по себе HEALTHCHECK ничего не перезапускает — статус unhealthy показывается в docker ps, но Docker не убивает и не перезапускает контейнер только на основании него. Автоматический перезапуск зависшего контейнера строится через связку с restart policy, но не напрямую, а через прокладку: контейнеры с политикой restart: always или unless-stopped перезапускаются, только когда сам процесс завершается — то есть healthcheck должен либо привести к падению процесса, либо вы добавляете внешний наблюдатель.

Практический вариант — контейнер сам завершает свой главный процесс, если понимает, что завис (watchdog-логика внутри приложения), и restart policy его поднимает:

services:
  web:
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
      interval: 15s
      timeout: 3s
      retries: 3

Второй вариант — внешний наблюдатель, который следит за статусом unhealthy и перезапускает контейнер сам. Простейший вариант — легковесный проект docker-autoheal: он слушает события Docker, и при переходе контейнера в unhealthy выполняет docker restart. Поднимается как ещё один контейнер с доступом к /var/run/docker.sock:

services:
  autoheal:
    image: willfarrell/autoheal
    restart: always
    environment:
      - AUTOHEAL_CONTAINER_LABEL=all
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock

Третий вариант, если вы уже используете оркестрацию поверх обычного Docker — например, Docker Swarm, где healthcheck встроен глубже: Swarm сам подменяет unhealthy-задачи новыми репликами без сторонних инструментов. Для одиночного docker-compose на одном сервере связка healthcheck + docker-autoheal — самый простой рабочий вариант без лишней инфраструктуры.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать VPS

Нужны сами нейросети для контента?

Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.

Частые вопросы

Healthcheck замедляет работу контейнера?

Нет, если проверка лёгкая (простой HTTP-запрос к своему же процессу занимает миллисекунды). Не делайте проверку тяжёлой — не гоняйте в ней полные интеграционные тесты, это лишняя нагрузка на каждый интервал.

Можно ли использовать healthcheck без HTTP-эндпоинта?

Да, подойдёт проверка через nc -z на нужный порт или pgrep по имени процесса — это грубее, но лучше, чем ничего, для сервисов без веб-интерфейса.

Почему контейнер сразу помечается unhealthy после запуска?

Скорее всего не задан или занижен start-period — приложению не хватает времени на инициализацию, и первые проверки проваливаются ещё до полного старта.

Чем HEALTHCHECK отличается от простого restart: always?

restart: always перезапускает контейнер, только когда падает его главный процесс. HEALTHCHECK ловит случай, когда процесс жив, но не отвечает, — это разные сценарии сбоя, и для полного покрытия нужны оба механизма вместе.

Нужен ли healthcheck для баз данных вроде Postgres или Redis?

Да, особенно если от их готовности зависят другие сервисы через depends_on: condition: service_healthy — это защищает от гонки при старте, когда приложение подключается раньше, чем база готова принимать соединения.