Контейнер 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.

