Короткий ответ
unhealthy значит: процесс жив, а HEALTHCHECK/healthcheck.test возвращает ненулевой код. На host.example запустите ту же команду через docker compose exec, почините URL/порт/утилиту/start_period. Не ставьте disable: true и не удаляйте healthcheck «чтобы стек поднялся»: тогда depends_on: condition: service_healthy врёт зелёным. Docker Engine сам не убивает unhealthy в чистом Compose (в Swarm/k8s — да); у вас сломается оркестрация зависимостей и мониторинг.
Симптомы и как отличить
docker compose ps→Up (unhealthy);webне стартует: ждётdbhealthy;health: startingминутами (длинныйstart_periodили вечный fail);- контейнер Restarting — это не health, это exit code.
| ps | Смысл |
|---|---|
| Up (healthy) | test exit 0 |
| Up (unhealthy) | test ≠ 0 |
| Up | нет HEALTHCHECK |
| Restarting | PID 1 падает |
Возможные причины
wget/curlнет в distroless/alpine.- Порт 8080 в YAML, test стучится в 80.
- HTTPS self-signed, curl без
-k(лучше http://127.0.0.1 внутренний). start_periodменьше прогрева JVM/миграций.- Health бьёт в
http://db:5432из db-контейнера вместоpg_isready. CMDvsCMD-SHELLи переменные не раскрылись.- Редко: IPv6
localhost→::1, слушает только v4.
Диагностика
cd /opt/app
docker compose ps
docker inspect app-web-1 --format '{{json .State.Health}}' | python3 -m json.tool
docker inspect app-web-1 --format '{{json .Config.Healthcheck}}' | python3 -m json.toolПоследние Log в .State.Health — stdout/stderr теста, не приложения.
Повторите test вручную:
docker compose exec web sh -c 'wget -qO- http://127.0.0.1:8080/health || curl -sfS http://127.0.0.1:8080/health; echo exit:$?'Если exec ок, а health нет — смотрите оболочку (CMD json без shell), рабочий каталог, USER без права на бинарь wget.
docker compose config | grep -n -A20 'healthcheck'Решение
Сценарий A. Нет клиента HTTP в образе
Плохо: test: ["CMD", "curl", "-f", "http://localhost/health"] в scratch.
Варианты: поставить curl в runtime-образе; использовать CMD-SHELL с wget busybox; для Go — сам бинарь -healthcheck; для postgres:
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app -d app"]
interval: 5s
timeout: 3s
retries: 20
start_period: 20sНе exit 0 заглушка.
Сценарий B. Неверный URL/порт
Выровняйте с EXPOSE и command. Внутри сети это 127.0.0.1:8080, не публичный DNS и не имя соседнего сервиса, если проверяете себя.
test: ["CMD-SHELL", "curl -sfS http://127.0.0.1:8080/health || exit 1"]Сценарий C. Медленный старт
start_period: 60s
retries: 10
interval: 10sstarting в этот период не считает unhealthy. Не раздувайте до 30 минут вместо depends_on.
Сценарий D. Хотели отключить
Не healthcheck: { disable: true }. Если тест врал — чините. Временно в окне можно поднять сервис без condition, не оставляйте в git.
Сценарий E. Health бьёт в соседа и падает из-за DNS
Проверка своего процесса. Связность web→db — отдельный readiness в приложении, плюс healthy у db. Иначе циклические ложные unhealthy: DNS, сети.
После правки YAML:
docker compose up -d --force-recreate web
sleep 15
docker inspect app-web-1 --format '{{.State.Health.Status}}'start_period не отключает проверки, а не считает неудачи в этот интервал как unhealthy. Если JVM греется 90 с, а period 10 с и retries 3, статус станет unhealthy ещё на старте и depends_on не отпустит web. Увеличьте period по факту, не disable. Лог Health.Log покажет connection refused — это прогрев, не «Docker сломал health».
Distroless/scratch: нет shell, нет curl. JSON CMD должен указывать реальный бинарь, например сам сервис с флагом --health. Добавление apt install curl в финальный stage ради health раздувает образ и поверхность. Лучше крошечный static wget или встроенный endpoint.
Не проверяйте внешний https://registry.example из health web: получите unhealthy при сбое DNS, хотя процесс жив. Liveness — сам процесс и локальный порт. Readiness — зависимости; в Compose это healthy у db плюс логика приложения, не один test на всё.
interval: 1s на тяжёлом SQL-health убьёт БД. 5–10 с достаточно. timeout меньше времени ответа даст ложные единицы. Сверьте с curl -w time_total. После правки нужен recreate: healthcheck вшит в конфиг контейнера при create, не live-reload.
Как проверить, что проблема устранена
docker compose ps
docker inspect app-web-1 --format '{{.State.Health.Status}} {{.State.Health.FailingStreak}}'
docker compose exec web sh -c 'curl -sfS http://127.0.0.1:8080/health'healthy, FailingStreak 0. Зависимый сервис стартовал по service_healthy. Статус стабилен 2–3 минуты, не мигает.
Если не помогло
Healthnull — YAML не применился, нет recreate / другой-f.- exec ок, inspect Log Permission denied — test от другого USER чем exec (вы exec под override).
- Distroless: добавьте статический health бинарь, не apt в проде.
- HTTP 401 на /health — либо публичный путь без auth, либо
-H Authorizationв секрете не в YAML plaintext; лучше отдельный/healthzбез секретов.
Код 401 на /health из-за глобального basic-auth — вынесите /healthz без пароля на loopback, не disable healthcheck и не кладите пароль в YAML test.
Профилактика
/healthzбез зависимостей от далёких API (liveness) vs/ready(readiness) — не смешивать.- Обязательный healthcheck у БД перед depends_on.
- CI:
compose pshealthy timeout 90s. - Не disable в шаблонах.
- Логи test в inspect при алёрте unhealthy.
depends_on с service_healthy без рабочего test у db зависнет web в created. Смотрите compose ps обоих. Если test pg_isready требует пользователя, которого нет на холодном томе, первый старт будет unhealthy до инициализации — увеличьте start_period у db, не отключайте проверку у web.
FAQ
disable: true быстрее чем чинить curl?
Быстрее и слепее. Зависимые сервисы стартанут на неготовый Postgres.
CMD vs CMD-SHELL
JSON CMD без shell не знает $VARS и ||. Для pg_isready удобен CMD-SHELL. Для одного бинаря — CMD.
Swarm перезапускает unhealthy, Compose нет
Да, разная семантика. На Compose unhealthy — сигнал вам и depends_on.
Можно ли test: ["CMD", "true"]?
Это отключение под видом галочки. Нет.
Healthcheck нагружает CPU
Увеличьте interval, облегчите endpoint. Не disable.