5.8. HEALTHCHECK
Цели
После этого материала вы сможете:
- настроить
HEALTHCHECKи объяснить назначение каждого параметра; - объяснить, что Docker делает со статусом health, а чего не делает;
- отличать liveness от readiness и понимать, какой из них выражает
HEALTHCHECK; - написать проверку, не создающую ложных срабатываний;
- объяснить, почему healthcheck не должен проверять внешние зависимости;
- решить, где задавать проверку — в образе или в Compose.
Предварительные знания
- 5.2. Базовые инструкции;
- 4.1. Состояния и переходы;
- 4.3. Exec, logs, inspect — чтение полей
inspect.
Ключевые термины
| Термин | Объяснение |
|---|---|
health status | Состояние проверки: starting, healthy, unhealthy |
liveness | «Жив ли процесс» — нужно ли его перезапустить |
readiness | «Готов ли принимать запросы» — нужно ли слать ему трафик |
start period | Время после старта, когда неудачи не считаются |
failing streak | Число неудач подряд |
каскадный отказ | Отказ одного сервиса, вызывающий отказ зависимых |
Теория
Синтаксис
HEALTHCHECK [опции] CMD команда
HEALTHCHECK NONE
Параметры:
| Параметр | По умолчанию | Назначение |
|---|---|---|
--interval | 30s | Период между проверками |
--timeout | 30s | Максимальная длительность одной проверки |
--start-period | 0s | Время после старта, когда неудачи не учитываются |
--start-interval | 5s | Период проверок во время start period |
--retries | 3 | Число неудач подряд до статуса unhealthy |
Форма HEALTHCHECK NONE отменяет проверку, унаследованную от базового образа.
Команда интерпретируется по коду возврата:
| Код | Значение |
|---|---|
0 | Здоров |
1 | Нездоров |
2 | Зарезервирован — не использовать |
Что Docker делает со статусом
Здесь кроется главное недоразумение.
Docker сам по себе не перезапускает unhealthy container. Статус unhealthy — это метка, доступная через docker ps и docker inspect, и событие в docker events. Никакого автоматического действия за ним не следует.
Что статус реально даёт:
| Потребитель | Что делает |
|---|---|
docker ps | Показывает (healthy) или (unhealthy) |
docker events | Генерирует событие health_status |
Compose depends_on: condition: service_healthy | Ждёт готовности перед запуском зависимого сервиса |
| Swarm | Перезапускает задачу |
| Внешний мониторинг | Может опрашивать и реагировать |
Строка про Compose — основная практическая ценность healthcheck в рамках этого курса. Она разбирается в разделе 09.
Для автоматического перезапуска нужен внешний механизм: оркестратор, autoheal-container или мониторинг. Restart policy на статус health не реагирует (урок 4.6).
Жизненный цикл статуса
container стартовал
│
▼
┌──────────────┐ проверки идут с интервалом --start-interval
│ starting │ неудачи НЕ увеличивают счётчик
└──────┬───────┘
│ первая успешная проверка
│ ИЛИ истёк --start-period
▼
┌──────────────┐ проверки с интервалом --interval
│ healthy │◄─────────────────┐
└──────┬───────┘ │ успешная проверка
│ неудача │ сбрасывает счётчик
▼ │
счётчик неудач++ ─────────────────┘
│
│ счётчик достиг --retries
▼
┌──────────────┐
│ unhealthy │
└──────────────┘
Важная деталь: одна успешная проверка сбрасывает счётчик неудач. Сервис, отвечающий через раз, останется healthy при --retries 3.
start-period решает проблему медленного старта
Без него приложение, которому нужно 40 секунд на инициализацию, будет помечено unhealthy ещё до готовности:
без --start-period (retries=3, interval=10s):
0s старт
10s проверка 1 — неудача (приложение ещё грузится), счётчик 1
20s проверка 2 — неудача, счётчик 2
30s проверка 3 — неудача, счётчик 3 → UNHEALTHY
40s приложение готово, но статус уже испорчен
С --start-period=60s неудачи в первые 60 секунд не увеличивают счётчик, а --start-interval=2s заставляет проверять чаще — чтобы поймать момент готовности быстрее.
Liveness против readiness
Два разных вопроса, которые часто смешивают:
| Liveness | Readiness | |
|---|---|---|
| Вопрос | Процесс жив? Нужен ли перезапуск? | Готов принимать трафик? |
| Отказ означает | Перезапустить | Убрать из балансировки |
| Проверяет | Внутреннее состояние процесса | Готовность зависимостей |
| Должен проверять базу данных | нет | да |
Docker HEALTHCHECK — один механизм, и он ближе к liveness. Разделение на две пробы есть в Kubernetes (раздел 18) и разбирается подробно в разделе 11.
Почему нельзя проверять внешние зависимости
Соблазнительный вариант:
HEALTHCHECK CMD curl -f http://localhost:8000/health-with-db || exit 1
где endpoint обращается к базе данных. Последствие — каскадный отказ:
база данных недоступна 30 секунд
│
▼
все 10 реплик API становятся unhealthy
│
▼
оркестратор перезапускает все 10 одновременно
│
▼
база поднялась, но получает лавину переподключений
│
▼
отказ усугубляется
Приложение при этом было полностью работоспособно — оно просто не могло обратиться к базе.
Правило: liveness-проверка касается только самого процесса. Проверка «отвечает ли мой HTTP-сервер» корректна. Проверка «доступна ли база» — нет.
Готовность зависимостей — задача readiness-пробы, и её отказ должен убирать экземпляр из балансировки, а не перезапускать его.
Чем проверять
| Инструмент | Плюсы | Минусы |
|---|---|---|
curl | Универсален | Обычно не установлен в slim-образах |
wget | Есть в Alpine (BusyBox) | Другой синтаксис |
python -c | Всегда есть в Python-образе | Медленнее: запуск интерпретатора |
| Скомпилированный healthcheck | Быстрый, без зависимостей | Нужно собирать |
Для Python-образов вариант без внешних зависимостей:
HEALTHCHECK --interval=15s --timeout=3s --start-period=30s --retries=3 \
CMD python -c "import urllib.request,sys; sys.exit(0 if urllib.request.urlopen('http://127.0.0.1:8000/healthz', timeout=2).status==200 else 1)"
Он не требует ставить curl в образ, что важно для минимальных образов (раздел 11).
Стоимость проверки
Проверка выполняется внутри container и расходует его ресурсы. При --interval=5s запуск интерпретатора Python каждые 5 секунд заметен на нагруженном сервисе.
Разумные значения для типичного web-сервиса:
HEALTHCHECK --interval=30s --timeout=3s --start-period=40s --retries=3 CMD ...
Обнаружение отказа занимает до interval × retries = 90 секунд. Для более быстрой реакции уменьшайте interval, понимая цену.
Образ или Compose
| Где | Плюсы | Минусы |
|---|---|---|
В Dockerfile | Работает везде, где запущен образ | Требует пересборки для изменения; порт зашит |
В compose.yaml | Настраивается без пересборки; знает конфигурацию окружения | Действует только в этом Compose-проекте |
Практическая рекомендация: если порт и путь фиксированы — в образе. Если они настраиваются переменными окружения — в Compose, потому что Dockerfile не знает итоговых значений.
Внутренний механизм
Как выполняется проверка
Daemon периодически запускает указанную команду внутри container — фактически через тот же механизм, что и docker exec. Отсюда следствия:
- проверка выполняется в namespaces и cgroup container, расходуя его лимиты;
- она не является PID 1 и не влияет на главный процесс;
- при
--timeoutпроверка принудительно завершается и считается неудачной.
Результаты сохраняются в .State.Health.Log — по умолчанию последние пять записей с кодом возврата, временем и выводом.
Где смотреть результат
docker inspect <container> --format '{{json .State.Health}}'
Структура:
| Поле | Содержимое |
|---|---|
Status | starting, healthy, unhealthy |
FailingStreak | Текущее число неудач подряд |
Log[] | Последние проверки: Start, End, ExitCode, Output |
Поле Output содержит вывод команды — главный источник информации при разборе ложных срабатываний.
Команды и примеры
Подготовка
mkdir -p /tmp/healthcheck && cd /tmp/healthcheck
cat > server.py <<'PY'
"""Сервер с управляемым временем старта и состоянием здоровья."""
import http.server
import os
import socketserver
import threading
import time
STARTUP_DELAY = int(os.environ.get("STARTUP_DELAY", "0"))
FAIL_AFTER = int(os.environ.get("FAIL_AFTER", "0"))
ready = False
started_at = time.time()
class Handler(http.server.BaseHTTPRequestHandler):
def do_GET(self):
healthy = ready
if FAIL_AFTER and (time.time() - started_at) > FAIL_AFTER:
healthy = False
if self.path == "/healthz":
code = 200 if healthy else 503
body = b"ok" if healthy else b"unhealthy"
else:
code, body = 200, b"hello"
self.send_response(code)
self.send_header("Content-Type", "text/plain")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
def log_message(self, fmt, *args):
pass
def warmup():
global ready
if STARTUP_DELAY:
print(f"инициализация {STARTUP_DELAY} c...", flush=True)
time.sleep(STARTUP_DELAY)
ready = True
print("готов принимать запросы", flush=True)
threading.Thread(target=warmup, daemon=True).start()
with socketserver.TCPServer(("0.0.0.0", 8000), Handler) as httpd:
print("слушаю 8000", flush=True)
httpd.serve_forever()
PY
Базовый healthcheck
cat > Dockerfile.basic <<'EOF'
FROM python:3.13-slim
COPY server.py /server.py
EXPOSE 8000
HEALTHCHECK --interval=5s --timeout=3s --retries=3 \
CMD python -c "import urllib.request,sys; sys.exit(0 if urllib.request.urlopen('http://127.0.0.1:8000/healthz', timeout=2).status==200 else 1)"
CMD ["python", "-u", "/server.py"]
EOF
docker build -q -f Dockerfile.basic -t hc:basic . > /dev/null
docker run -d --name hc-basic hc:basic > /dev/null
echo "наблюдаем переход статуса:"
for i in $(seq 1 6); do
printf ' %2ds: %s\n' "$((i*3))" \
"$(docker inspect hc-basic --format '{{.State.Health.Status}}')"
sleep 3
done
наблюдаем переход статуса:
3s: starting
6s: healthy
9s: healthy
12s: healthy
15s: healthy
18s: healthy
Статус виден и в docker ps:
docker ps --filter name=hc-basic --format 'table {{.Names}}\t{{.Status}}'
NAMES STATUS
hc-basic Up 20 seconds (healthy)
Подробности:
docker inspect hc-basic --format '{{json .State.Health}}' | python3 -m json.tool | head -20
{
"Status": "healthy",
"FailingStreak": 0,
"Log": [
{
"Start": "2026-07-30T14:22:10.481Z",
"End": "2026-07-30T14:22:10.612Z",
"ExitCode": 0,
"Output": ""
}
]
}
docker rm -f hc-basic > /dev/null
Проблема медленного старта
echo "=== без --start-period, приложение стартует 25 секунд ==="
docker run -d --name hc-slow -e STARTUP_DELAY=25 hc:basic > /dev/null
for i in $(seq 1 7); do
printf ' %2ds: статус=%-10s неудач=%s\n' "$((i*5))" \
"$(docker inspect hc-slow --format '{{.State.Health.Status}}')" \
"$(docker inspect hc-slow --format '{{.State.Health.FailingStreak}}')"
sleep 5
done
docker rm -f hc-slow > /dev/null
=== без --start-period, приложение стартует 25 секунд ===
5s: статус=starting неудач=1
10s: статус=unhealthy неудач=3
15s: статус=unhealthy неудач=5
20s: статус=unhealthy неудач=7
25s: статус=unhealthy неудач=9
30s: статус=healthy неудач=0
35s: статус=healthy неудач=0
Container был помечен unhealthy на 10-й секунде, хотя приложение просто ещё не запустилось. В Compose с condition: service_healthy это привело бы к сбою запуска зависимых сервисов.
С --start-period:
cat > Dockerfile.startperiod <<'EOF'
FROM python:3.13-slim
COPY server.py /server.py
EXPOSE 8000
HEALTHCHECK --interval=5s --timeout=3s --start-period=40s --start-interval=2s --retries=3 \
CMD python -c "import urllib.request,sys; sys.exit(0 if urllib.request.urlopen('http://127.0.0.1:8000/healthz', timeout=2).status==200 else 1)"
CMD ["python", "-u", "/server.py"]
EOF
docker build -q -f Dockerfile.startperiod -t hc:sp . > /dev/null
docker run -d --name hc-sp -e STARTUP_DELAY=25 hc:sp > /dev/null
echo "=== с --start-period=40s ==="
for i in $(seq 1 7); do
printf ' %2ds: статус=%-10s неудач=%s\n' "$((i*5))" \
"$(docker inspect hc-sp --format '{{.State.Health.Status}}')" \
"$(docker inspect hc-sp --format '{{.State.Health.FailingStreak}}')"
sleep 5
done
docker rm -f hc-sp > /dev/null
=== с --start-period=40s ===
5s: статус=starting неудач=0
10s: статус=starting неудач=0
15s: статус=starting неудач=0
20s: статус=starting неудач=0
25s: статус=starting неудач=0
30s: статус=healthy неудач=0
35s: статус=healthy неудач=0
Счётчик неудач остался нулевым — в start period они не учитываются. Статус перешёл в healthy сразу, как только приложение стало отвечать.
Параметр --start-interval=2s заставил проверять каждые 2 секунды вместо 5, поэтому готовность обнаружена быстрее.
Переход в unhealthy
docker run -d --name hc-fail -e FAIL_AFTER=10 hc:basic > /dev/null
echo "=== приложение начинает отвечать 503 через 10 секунд ==="
for i in $(seq 1 8); do
printf ' %2ds: статус=%-10s неудач=%s\n' "$((i*5))" \
"$(docker inspect hc-fail --format '{{.State.Health.Status}}')" \
"$(docker inspect hc-fail --format '{{.State.Health.FailingStreak}}')"
sleep 5
done
=== приложение начинает отвечать 503 через 10 секунд ===
5s: статус=healthy неудач=0
10s: статус=healthy неудач=0
15s: статус=healthy неудач=1
20s: статус=healthy неудач=2
25s: статус=unhealthy неудач=3
30s: статус=unhealthy неудач=4
35s: статус=unhealthy неудач=5
40s: статус=unhealthy неудач=6
Потребовалось три неудачи подряд — как задано в --retries=3.
Ключевое наблюдение — container продолжает работать:
docker ps --filter name=hc-fail --format '{{.Names}} {{.Status}}'
docker inspect hc-fail --format 'Running: {{.State.Running}} RestartCount: {{.RestartCount}}'
hc-fail Up 45 seconds (unhealthy)
Running: true RestartCount: 0
Docker пометил container как нездоровый и ничего не сделал. Ни перезапуска, ни остановки.
Проверим, что и restart policy не помогает:
docker rm -f hc-fail > /dev/null
docker run -d --name hc-restart --restart=always -e FAIL_AFTER=8 hc:basic > /dev/null
sleep 35
docker inspect hc-restart --format 'health={{.State.Health.Status}} restarts={{.RestartCount}}'
docker rm -f hc-restart > /dev/null
health=unhealthy restarts=0
Ноль перезапусков. Restart policy реагирует на завершение процесса, а не на статус health.
События health
docker run -d --name hc-events -e FAIL_AFTER=8 hc:basic > /dev/null
timeout 30 docker events --filter 'container=hc-events' --filter 'event=health_status' \
--format '{{.Time}} {{.Status}}' 2>/dev/null
docker rm -f hc-events > /dev/null
1785492141 health_status: healthy
1785492166 health_status: unhealthy
Именно на эти события подписываются внешние инструменты автоматического восстановления.
Отладка ложных срабатываний
Поле Output содержит вывод команды проверки:
cat > Dockerfile.badcheck <<'EOF'
FROM python:3.13-slim
COPY server.py /server.py
# ОШИБКА: curl не установлен в slim-образе
HEALTHCHECK --interval=5s --retries=2 CMD curl -f http://localhost:8000/healthz || exit 1
CMD ["python", "-u", "/server.py"]
EOF
docker build -q -f Dockerfile.badcheck -t hc:bad . > /dev/null
docker run -d --name hc-bad hc:bad > /dev/null
sleep 14
docker inspect hc-bad --format '{{.State.Health.Status}}'
echo "--- вывод последней проверки ---"
docker inspect hc-bad --format '{{range .State.Health.Log}}{{.ExitCode}}: {{.Output}}{{end}}' | tail -2
docker rm -f hc-bad > /dev/null
unhealthy
--- вывод последней проверки ---
127: /bin/sh: 1: curl: not found
Код 127 и сообщение curl: not found (урок 4.6). Приложение работало нормально — сломалась сама проверка.
Это самая частая причина ложного unhealthy: команда проверки использует инструмент, отсутствующий в образе.
Проверка без внешних зависимостей
cat > Dockerfile.nodeps <<'EOF'
FROM python:3.13-slim
COPY server.py /server.py
EXPOSE 8000
# Вариант 1: urllib из стандартной библиотеки
HEALTHCHECK --interval=10s --timeout=3s --start-period=20s --retries=3 \
CMD python -c "import urllib.request,sys; sys.exit(0 if urllib.request.urlopen('http://127.0.0.1:8000/healthz', timeout=2).status==200 else 1)"
CMD ["python", "-u", "/server.py"]
EOF
docker build -q -f Dockerfile.nodeps -t hc:nodeps . > /dev/null
docker run -d --name hc-nodeps hc:nodeps > /dev/null
sleep 25
docker inspect hc-nodeps --format 'статус: {{.State.Health.Status}}'
docker rm -f hc-nodeps > /dev/null
статус: healthy
Никаких дополнительных пакетов не потребовалось.
Альтернатива для образов без Python — проверка TCP-порта средствами оболочки:
cat > Dockerfile.tcp <<'EOF'
FROM alpine:3.21
RUN apk add --no-cache python3
COPY server.py /server.py
EXPOSE 8000
# BusyBox wget есть в Alpine по умолчанию
HEALTHCHECK --interval=10s --timeout=3s --start-period=15s --retries=3 \
CMD wget -q -O /dev/null http://127.0.0.1:8000/healthz || exit 1
CMD ["python3", "-u", "/server.py"]
EOF
docker build -q -f Dockerfile.tcp -t hc:tcp . > /dev/null
docker run -d --name hc-tcp hc:tcp > /dev/null
sleep 20
docker inspect hc-tcp --format 'статус: {{.State.Health.Status}}'
docker rm -f hc-tcp > /dev/null
статус: healthy
Отмена унаследованной проверки
cat > Dockerfile.inherit <<'EOF'
FROM hc:basic
# Базовый образ имеет HEALTHCHECK; отменяем его
HEALTHCHECK NONE
EOF
docker build -q -f Dockerfile.inherit -t hc:none . > /dev/null
docker image inspect hc:basic --format 'базовый: {{if .Config.Healthcheck}}задан{{else}}нет{{end}}'
docker image inspect hc:none --format 'после NONE: {{index .Config.Healthcheck.Test 0}}'
базовый: задан
после NONE: NONE
Нужно, когда базовый образ содержит проверку, не подходящую вашему приложению.
Переопределение при запуске
Проверку можно задать или отключить без пересборки:
docker run -d --name hc-override --no-healthcheck hc:basic > /dev/null
sleep 3
docker inspect hc-override --format '{{if .State.Health}}{{.State.Health.Status}}{{else}}проверка отключена{{end}}'
docker rm -f hc-override > /dev/null
docker run -d --name hc-custom \
--health-cmd='python -c "import urllib.request,sys; sys.exit(0 if urllib.request.urlopen(\"http://127.0.0.1:8000/\", timeout=2).status==200 else 1)"' \
--health-interval=5s \
--health-start-period=15s \
--health-retries=2 \
hc:basic > /dev/null
sleep 18
docker inspect hc-custom --format 'своя проверка: {{.State.Health.Status}}'
docker rm -f hc-custom > /dev/null
проверка отключена
своя проверка: healthy
Задание в Compose
services:
api:
image: hc:basic
environment:
APP_PORT: "8000"
healthcheck:
test: ["CMD", "python", "-c", "import urllib.request,sys; sys.exit(0 if urllib.request.urlopen('http://127.0.0.1:8000/healthz', timeout=2).status==200 else 1)"]
interval: 10s
timeout: 3s
start_period: 30s
start_interval: 2s
retries: 3
worker:
image: myapp:worker
depends_on:
api:
condition: service_healthy
Ключевая строка — condition: service_healthy: worker не стартует, пока api не станет здоровым. Это и есть основное практическое применение healthcheck, разбираемое в разделе 09.
Форма test как массив с префиксом CMD эквивалентна exec form; префикс CMD-SHELL даёт shell form с подстановкой переменных.
Уборка
cd /tmp
docker rmi -f $(docker images -q --filter 'reference=hc:*') 2>/dev/null || true
rm -rf /tmp/healthcheck
Практическое упражнение
Задание. Настройте healthcheck для сервиса, который стартует 30 секунд и периодически теряет связь с базой данных.
Требования:
- Container не должен помечаться
unhealthyво время старта. - Готовность должна обнаруживаться в течение 3 секунд после её наступления.
- Проверка не должна зависеть от доступности базы данных — обоснуйте письменно.
- Проверка не должна требовать установки дополнительных пакетов.
- Отказ приложения должен обнаруживаться не дольше чем за 30 секунд.
Приведите подобранные значения параметров с обоснованием каждого и докажите выполнение требований измерением.
Подсказки
Подсказка 1
Требования 1 и 2 задаются парой --start-period и --start-interval.
Подсказка 2
Требование 5 определяет произведение interval × retries.
Подсказка 3
Для требования 3 приложение должно иметь два endpoint: один проверяет только себя, другой — зависимости.
Решение
Сначала выполните задание самостоятельно.
Показать решение
#!/usr/bin/env bash
# healthcheck-tune.sh — подбор параметров healthcheck.
set -uo pipefail
WORK="$(mktemp -d)"
trap 'docker rm -f $(docker ps -aq --filter "name=hct-") >/dev/null 2>&1 || true;
docker rmi -f hct:app >/dev/null 2>&1 || true;
rm -rf "$WORK"' EXIT
cd "$WORK"
cat > app.py <<'PY'
"""Сервис с раздельными endpoint для liveness и readiness."""
import http.server
import os
import socketserver
import threading
import time
STARTUP_DELAY = int(os.environ.get("STARTUP_DELAY", "30"))
DB_DOWN_AFTER = int(os.environ.get("DB_DOWN_AFTER", "0"))
APP_BROKEN_AFTER = int(os.environ.get("APP_BROKEN_AFTER", "0"))
started_at = time.time()
initialized = False
def elapsed() -> float:
return time.time() - started_at
def app_alive() -> bool:
"""Liveness: жив ли сам процесс. О базе не знает."""
if APP_BROKEN_AFTER and elapsed() > APP_BROKEN_AFTER:
return False
return initialized
def db_available() -> bool:
"""Readiness: доступна ли база."""
if DB_DOWN_AFTER and elapsed() > DB_DOWN_AFTER:
return False
return True
class Handler(http.server.BaseHTTPRequestHandler):
def do_GET(self):
if self.path == "/healthz": # liveness
ok = app_alive()
elif self.path == "/readyz": # readiness
ok = app_alive() and db_available()
else:
ok = True
body = b"ok" if ok else b"fail"
self.send_response(200 if ok else 503)
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
def log_message(self, fmt, *args):
pass
def warmup():
global initialized
print(f"инициализация {STARTUP_DELAY} c...", flush=True)
time.sleep(STARTUP_DELAY)
initialized = True
print(f"готов на {elapsed():.0f}-й секунде", flush=True)
threading.Thread(target=warmup, daemon=True).start()
with socketserver.TCPServer(("0.0.0.0", 8000), Handler) as httpd:
httpd.serve_forever()
PY
cat > Dockerfile <<'EOF'
FROM python:3.13-slim
COPY app.py /app.py
EXPOSE 8000
# --start-period=45s : старт занимает 30 c, берём запас 1.5x
# --start-interval=2s: готовность обнаруживается за <=2 c (требование 2)
# --interval=10s : период штатных проверок
# --retries=3 : отказ обнаруживается за 10*3 = 30 c (требование 5)
# --timeout=3s : проверка не должна висеть дольше 3 c
# /healthz, НЕ /readyz: проверка не зависит от базы (требование 3)
HEALTHCHECK --interval=10s --timeout=3s --start-period=45s --start-interval=2s --retries=3 \
CMD python -c "import urllib.request,sys; sys.exit(0 if urllib.request.urlopen('http://127.0.0.1:8000/healthz', timeout=2).status==200 else 1)"
CMD ["python", "-u", "/app.py"]
EOF
docker build -q -t hct:app . > /dev/null
watch_status() {
local name="$1" duration="$2" step="${3:-5}"
local t=0
while [ "$t" -lt "$duration" ]; do
printf ' %3ds: %-10s неудач=%s\n' "$t" \
"$(docker inspect "$name" --format '{{.State.Health.Status}}' 2>/dev/null)" \
"$(docker inspect "$name" --format '{{.State.Health.FailingStreak}}' 2>/dev/null)"
sleep "$step"
t=$((t + step))
done
}
echo "═══ Требования 1 и 2: медленный старт ═══"
docker run -d --name hct-start -e STARTUP_DELAY=30 hct:app > /dev/null
watch_status hct-start 45 5
docker rm -f hct-start > /dev/null
echo
echo "═══ Требование 3: база упала, приложение живо ═══"
docker run -d --name hct-db -e STARTUP_DELAY=2 -e DB_DOWN_AFTER=15 hct:app > /dev/null
sleep 40
printf ' статус healthcheck: %s\n' \
"$(docker inspect hct-db --format '{{.State.Health.Status}}')"
printf ' /healthz (liveness): %s\n' \
"$(docker exec hct-db python -c "import urllib.request;print(urllib.request.urlopen('http://127.0.0.1:8000/healthz').status)" 2>/dev/null)"
printf ' /readyz (readiness): %s\n' \
"$(docker exec hct-db python -c "
import urllib.request, urllib.error
try:
print(urllib.request.urlopen('http://127.0.0.1:8000/readyz').status)
except urllib.error.HTTPError as e:
print(e.code)
" 2>/dev/null)"
docker rm -f hct-db > /dev/null
echo
echo "═══ Требование 5: отказ приложения ═══"
docker run -d --name hct-broken -e STARTUP_DELAY=2 -e APP_BROKEN_AFTER=10 hct:app > /dev/null
watch_status hct-broken 50 10
docker rm -f hct-broken > /dev/null
Ожидаемый вывод:
═══ Требования 1 и 2: медленный старт ═══
0s: starting неудач=0
5s: starting неудач=0
10s: starting неудач=0
15s: starting неудач=0
20s: starting неудач=0
25s: starting неудач=0
30s: healthy неудач=0
35s: healthy неудач=0
40s: healthy неудач=0
═══ Требование 3: база упала, приложение живо ═══
статус healthcheck: healthy
/healthz (liveness): 200
/readyz (readiness): 503
═══ Требование 5: отказ приложения ═══
0s: starting неудач=0
10s: healthy неудач=0
20s: healthy неудач=1
30s: healthy неудач=2
40s: unhealthy неудач=3
Обоснование параметров.
| Параметр | Значение | Обоснование |
|---|---|---|
--start-period | 45s | Старт занимает 30 с; запас 1.5× покрывает замедление на нагруженной машине |
--start-interval | 2s | Требование 2: готовность обнаруживается не дольше чем за 2 с |
--interval | 10s | Вместе с retries=3 даёт 30 с на обнаружение отказа |
--retries | 3 | Одиночный сбой сети не переводит в unhealthy |
--timeout | 3s | Больше времени ответа здорового сервиса, меньше interval |
| endpoint | /healthz | Не касается базы — см. ниже |
Ключевой результат — блок «Требование 3». База недоступна, но healthcheck показывает healthy. Проверка обращается к /healthz, который отвечает только за состояние процесса.
Если бы проверка использовала /readyz, произошло бы следующее: все реплики сервиса стали бы unhealthy одновременно; в Compose с condition: service_healthy зависимые сервисы не запустились бы; в оркестраторе все реплики пошли бы на перезапуск, создав лавину переподключений к восстанавливающейся базе.
Приложение при этом полностью работоспособно — оно корректно возвращает 503 на запросы, требующие базы, и продолжает обслуживать остальные.
Разделение обязанностей:
| Endpoint | Проверяет | Кто использует | Реакция на отказ |
|---|---|---|---|
/healthz | Только себя | Docker HEALTHCHECK, Kubernetes liveness | Перезапуск |
/readyz | Себя и зависимости | Балансировщик, Kubernetes readiness | Убрать из ротации |
Docker HEALTHCHECK — один механизм, поэтому в нём используется liveness-проверка: она безопаснее. Readiness-проверка нужна балансировщику, а не Docker.
Подробно разделение разбирается в разделе 11.
Проверка результата
mkdir -p /tmp/vhc && cd /tmp/vhc
printf 'FROM python:3.13-slim\nHEALTHCHECK --interval=3s --start-period=5s --retries=2 CMD python -c "import sys; sys.exit(0)"\nCMD ["sleep","60"]\n' > Dockerfile
docker build -q -t vhc:1 . > /dev/null
docker run -d --name vhc-t vhc:1 > /dev/null
sleep 8
docker ps --filter name=vhc-t --format '{{.Status}}'
docker rm -f vhc-t > /dev/null; docker rmi -f vhc:1 > /dev/null; cd /tmp && rm -rf /tmp/vhc
Ожидается Up ... (healthy).
Типичные ошибки
| Ошибка | Причина | Исправление |
|---|---|---|
Ожидание, что Docker перезапустит unhealthy | Логичное предположение | Docker только помечает; нужен оркестратор или внешний механизм |
curl в проверке для slim-образа | Скопировано из примеров | Код 127, ложный unhealthy; использовать python -c или wget |
Нет --start-period | Не знают о параметре | Медленно стартующее приложение помечается unhealthy до готовности |
| Проверка обращается к базе данных | Кажется более полной | Каскадный отказ; liveness проверяет только себя |
Слишком короткий --interval | Хотят быстрее реагировать | Проверка расходует ресурсы container |
--retries=1 | Хотят быстрее реагировать | Одиночный сбой переводит в unhealthy |
Код возврата 2 в проверке | Кажется допустимым | Зарезервирован; использовать только 0 и 1 |
| Healthcheck в образе с настраиваемым портом | Порт зашит в Dockerfile | Задавать в Compose, где известна конфигурация |
| Унаследованная проверка не подходит | Не знают про NONE | HEALTHCHECK NONE отменяет её |
Не смотрят Output при разборе | Не знают о поле | .State.Health.Log[].Output содержит вывод команды |
Контрольные вопросы
На понимание:
- Что Docker делает с container, получившим статус
unhealthy? - Чем
startingотличается отhealthyс точки зрения счётчика неудач? - Почему healthcheck не должен проверять доступность базы данных?
- Чем liveness отличается от readiness и какой из них выражает
HEALTHCHECK? - Почему одна успешная проверка сбрасывает счётчик неудач?
На применение:
- Как настроить проверку для приложения, стартующего 40 секунд?
- Как написать проверку HTTP-endpoint без установки дополнительных пакетов?
- Как отменить healthcheck, унаследованный от базового образа?
На диагностику:
- Container помечен
unhealthy, но приложение отвечает на запросы. Что проверить первым? - Сервис с
--restart=alwaysнаходится в состоянииunhealthyуже час и не перезапускается. Почему?
Краткое резюме
HEALTHCHECKзадаёт команду; код0— здоров,1— нездоров,2зарезервирован.- Docker только помечает статус и генерирует событие — перезапуск не выполняется.
- Главное применение в этом курсе —
depends_on: condition: service_healthyв Compose. - Статус проходит путь
starting→healthy→unhealthy; вstartingнеудачи не считаются. --start-periodрешает проблему медленного старта,--start-intervalускоряет обнаружение готовности.- Отказ обнаруживается за
interval × retries; одна успешная проверка сбрасывает счётчик. - Проверка не должна касаться внешних зависимостей — это ведёт к каскадному отказу.
HEALTHCHECKближе к liveness; readiness требует отдельного endpoint.- Для Python-образов проверка через
urllibне требует дополнительных пакетов. - Поле
.State.Health.Log[].Output— основной источник при разборе ложных срабатываний.
Официальные источники
| Источник | Ссылка | Что подтверждает |
|---|---|---|
| Dockerfile reference: HEALTHCHECK | https://docs.docker.com/reference/dockerfile/#healthcheck | Синтаксис, все параметры, коды возврата, форма NONE |
| docker run reference | https://docs.docker.com/reference/cli/docker/container/run/ | Флаги --health-cmd, --health-interval, --no-healthcheck |
| docker inspect reference | https://docs.docker.com/reference/cli/docker/inspect/ | Поля .State.Health: Status, FailingStreak, Log |
| Compose services: healthcheck | https://docs.docker.com/reference/compose-file/services/ | Задание проверки в Compose, CMD и CMD-SHELL, start_interval |
| Compose startup order | https://docs.docker.com/compose/how-tos/startup-order/ | depends_on с condition: service_healthy |
| docker events reference | https://docs.docker.com/reference/cli/docker/system/events/ | Событие health_status |
| Kubernetes: probes | https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/ | Различие liveness и readiness |
Навигация
← Предыдущий материал
Вернуться к разделу
Следующий материал → Воспроизводимые сборки
Главное оглавление