Контейнер unhealthy, а Docker его не перезапускает. Почему?

Контейнер unhealthy, а Docker его не перезапускает. Почему?

В compose.yaml стоит restart: always. Сайт не открывается, контейнер помечен unhealthy, но его uptime продолжает расти. Можно менять interval, увеличивать retries и ждать следующей проверки. Перезапуска от этого не будет.

У обычных контейнеров Docker Engine, в том числе запущенных через docker compose, политика restart реагирует на остановку контейнера. Healthcheck запускает отдельную команду и записывает её результат. Эта команда может завершаться с ошибкой каждые пять секунд, пока основной процесс спокойно работает дальше. Поэтому running и unhealthy вполне уживаются в одном контейнере.

Проверить это проще на маленьком HTTP-сервере. Он будет отвечать 503 на любой запрос, но продолжит принимать соединения. Убирать сеть, ломать базу и ловить настоящее зависание для опыта не потребуется.

Проверим на одном контейнере

Сохраните такой compose.yaml в отдельном каталоге на тестовой машине. Нужен Compose v2. Имя проекта gig-health-demo должно быть свободно; порты на хост не публикуются, каталогов и томов в примере нет.

services:
  app:
    image: python:3.13-slim
    restart: unless-stopped
    command:
      - python
      - -u
      - -c
      - |
        from http.server import HTTPServer, BaseHTTPRequestHandler
        class Handler(BaseHTTPRequestHandler):
            def do_GET(self):
                self.send_error(503, "Demo: not ready")
        HTTPServer(("0.0.0.0", 8080), Handler).serve_forever()
    healthcheck:
      test:
        - CMD
        - python
        - -c
        - >-
          from urllib.request import urlopen;
          urlopen('http://127.0.0.1:8080/', timeout=1).close()
      interval: 5s
      timeout: 2s
      retries: 3
      start_period: 5s

Здесь всё на стандартной библиотеке Python, без установки curl. Сервер возвращает 503, urlopen выбрасывает исключение, проверочная команда завершается с кодом 1. Основной Python-процесс остаётся жив.

Из этого каталога запускаем демопроект. Docker создаст контейнер и сеть; образ скачает, если его ещё нет:

docker compose -p gig-health-demo up -d

Примерно через полминуты в том же Bash или sh читаем состояние:

cid="$(docker compose -p gig-health-demo ps -q app)"
docker inspect --format \
  '{{.State.Status}} {{.State.Health.Status}} {{.RestartCount}}' \
  "$cid"

Ожидаемый вывод нового контейнера:

running unhealthy 0

Если ещё starting, подождите несколько секунд. Ноль в конце означает, что автоматических перезапусков не было. После трёх подряд неудачных проверок Docker поставил unhealthy и продолжил проверять сервер. При успешном ответе статус вернётся в healthy.

В примере стоит unless-stopped. Замена на always результата не изменит. Даже on-failure здесь не поможет: ему нужен ненулевой код завершения основного процесса, а не команды healthcheck. Отличие always от unless-stopped важно после ручной остановки: always может поднять контейнер при следующем запуске Docker daemon, unless-stopped оставит его остановленным.

После опыта уберите только этот демопроект командой из того же каталога. Контейнер и его сеть будут удалены, образ останется:

docker compose -p gig-health-demo down

Что смотреть на рабочем сервере

Начните с вывода самой проверки. Для рабочего контейнера замените “$cid” на его точное имя или ID. Эти команды ничего не перезапускают:

docker inspect --format '{{json .State.Health}}' "$cid"
docker inspect --format '{{json .Config.Healthcheck}}' "$cid"
docker logs --tail 50 "$cid"

В State.Health.Log лежат результаты последних попыток, включая ExitCode и Output. В нашем примере там будет ошибка HTTP 503. Через docker logs виден stdout/stderr приложения; вывод проверочной команды нужно искать в inspect.

Если там curl: not found, сначала надо починить образ или команду проверки. Рестарт отсутствующий curl не установит. При Connection refused сверьте порт и адрес: localhost внутри контейнера ведёт в этот контейнер. Опубликованный порт хоста здесь может быть вообще ни при чём. Заодно посмотрите Config.Healthcheck: фактическая проверка могла прийти из образа и отличаться от той, которую вы ожидали увидеть.

С ответом 503 уже интереснее. Приложение могло потерять базу или исчерпать пул соединений. По одному unhealthy причину не отличить. До перезапуска полезно сохранить логи: после него симптом может исчезнуть, а объяснение так и останется неизвестным. Только не выкладывайте на форум полный inspect без просмотра: в конфигурации могут оказаться секреты.

Если отвалилась общая база, перезапуск всех приложений добавит к сбою новые подключения и прогрев. Здесь полезнее восстановление соединения с паузами между попытками. А если приложение умеет определить собственное невосстановимое состояние, оно может корректно завершиться с ненулевым кодом. Тогда restart policy действительно получит повод сработать.

Для зависаний, которые само приложение не замечает, понадобится внешний контроллер. У него должны быть ограничены число попыток и частота рестартов, иначе один неудачный healthcheck устроит бесконечный цикл. Доступ к /var/run/docker.sock тоже стоит оценить до установки autoheal: API обычного rootful Docker позволяет управлять контейнерами и потенциально получить root-доступ к хосту.

depends_on с condition: service_healthy эту задачу не решает: Compose ждёт здоровую зависимость при запуске сервиса. Постоянного наблюдения с восстановлением здесь нет. И restart: true внутри depends_on относится к явным операциям Compose над зависимостью, а не к её автоматическому перезапуску после сбоя.

Если встречали описание, где Docker сам заменяет unhealthy-контейнер, проверьте, не про Swarm ли оно. Там оркестратор действительно может завершить нездоровую задачу и создать замену согласно настройкам сервиса. У обычного docker compose up такого контроллера нет.

Как добавить проверку в свой контейнер и подключить Kuma

Через docker update добавить healthcheck в работающий контейнер нельзя. Если сервис уже описан в Compose, допишите блок healthcheck на уровне image и restart. Остальной конфиг менять не нужно. Этот фрагмент рассчитан на образ с sh и curl:

healthcheck:
  test:
    - CMD-SHELL
    - >-
      curl -fsS --max-time 3
      http://127.0.0.1:8080/health || exit 1
  interval: 30s
  timeout: 5s
  retries: 3
  start_period: 30s

Адрес и порт здесь условные. Сначала проверьте, что у приложения действительно есть такой endpoint и он отвечает изнутри контейнера. Если curl отсутствует, используйте штатную проверку приложения либо добавьте утилиту в образ при сборке. Установка вручную в живой контейнер потеряется при пересоздании.

up пересоздаст контейнер с изменённой конфигурацией. У единственного экземпляра будет перерыв в работе. Заранее проверьте, что важные данные вынесены в тома или bind mounts: записанное только в слой контейнера пропадёт. Обычный compose restart новую проверку не применит.

Из каталога проекта проверьте конфигурацию и примените изменение к одному сервису. Вместо app подставьте его имя; сохраните обычные для проекта параметры -f и -p, если используете их:

docker compose config -q && \
  docker compose up -d --no-deps app
docker compose ps app

После запуска дождитесь healthy.

В Uptime Kuma начните с монитора HTTP(s): укажите реальный внешний URL сервиса, ожидаемый код ответа, например 200, интервал 60 секунд и два повтора. Подключите канал уведомлений и отправьте тестовое сообщение. Такая проверка заодно заметит проблемы с доступом через домен и reverse proxy. Саму Kuma лучше держать на другой машине, чтобы падение сервера не выключило и мониторинг.

Для статуса самого контейнера есть монитор Docker Container: укажите Docker-хост и имя контейнера. В Kuma 2.5.5 unhealthy считается ошибкой и приводит к DOWN с учётом повторов; без healthcheck работающий контейнер считается UP. Этот вариант требует доступа к Docker API. Не выдавайте его через открытый порт или бесконтрольно подключённый docker.sock; для HTTP(s)-проверки он вообще не нужен. Kuma отправит уведомление, но сама контейнер не перезапустит.

Документация: HEALTHCHECK, restart policies, depends_on, задачи Swarm, доступ к Docker daemon, urllib.request, docker update, compose up, compose restart, Docker-монитор в Kuma, логика Kuma 2.5.5.