Главная/Dockerfile/Урок

5.8. HEALTHCHECK

Цели

После этого материала вы сможете:

  • настроить HEALTHCHECK и объяснить назначение каждого параметра;
  • объяснить, что Docker делает со статусом health, а чего не делает;
  • отличать liveness от readiness и понимать, какой из них выражает HEALTHCHECK;
  • написать проверку, не создающую ложных срабатываний;
  • объяснить, почему healthcheck не должен проверять внешние зависимости;
  • решить, где задавать проверку — в образе или в Compose.

Предварительные знания

Ключевые термины

ТерминОбъяснение
health statusСостояние проверки: starting, healthy, unhealthy
liveness«Жив ли процесс» — нужно ли его перезапустить
readiness«Готов ли принимать запросы» — нужно ли слать ему трафик
start periodВремя после старта, когда неудачи не считаются
failing streakЧисло неудач подряд
каскадный отказОтказ одного сервиса, вызывающий отказ зависимых

Теория

Синтаксис

dockerfile
HEALTHCHECK [опции] CMD команда
HEALTHCHECK NONE

Параметры:

ПараметрПо умолчаниюНазначение
--interval30sПериод между проверками
--timeout30sМаксимальная длительность одной проверки
--start-period0sВремя после старта, когда неудачи не учитываются
--start-interval5sПериод проверок во время start period
--retries3Число неудач подряд до статуса 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).

Жизненный цикл статуса

text
   container стартовал
          │
          ▼
   ┌──────────────┐  проверки идут с интервалом --start-interval
   │  starting    │  неудачи НЕ увеличивают счётчик
   └──────┬───────┘
          │  первая успешная проверка
          │  ИЛИ истёк --start-period
          ▼
   ┌──────────────┐  проверки с интервалом --interval
   │   healthy    │◄─────────────────┐
   └──────┬───────┘                  │ успешная проверка
          │ неудача                  │ сбрасывает счётчик
          ▼                          │
   счётчик неудач++ ─────────────────┘
          │
          │ счётчик достиг --retries
          ▼
   ┌──────────────┐
   │  unhealthy   │
   └──────────────┘

Важная деталь: одна успешная проверка сбрасывает счётчик неудач. Сервис, отвечающий через раз, останется healthy при --retries 3.

start-period решает проблему медленного старта

Без него приложение, которому нужно 40 секунд на инициализацию, будет помечено unhealthy ещё до готовности:

text
   без --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

Два разных вопроса, которые часто смешивают:

LivenessReadiness
ВопросПроцесс жив? Нужен ли перезапуск?Готов принимать трафик?
Отказ означаетПерезапуститьУбрать из балансировки
ПроверяетВнутреннее состояние процессаГотовность зависимостей
Должен проверять базу данныхнетда

Docker HEALTHCHECKодин механизм, и он ближе к liveness. Разделение на две пробы есть в Kubernetes (раздел 18) и разбирается подробно в разделе 11.

Почему нельзя проверять внешние зависимости

Соблазнительный вариант:

dockerfile
HEALTHCHECK CMD curl -f http://localhost:8000/health-with-db || exit 1

где endpoint обращается к базе данных. Последствие — каскадный отказ:

text
   база данных недоступна 30 секунд
        │
        ▼
   все 10 реплик API становятся unhealthy
        │
        ▼
   оркестратор перезапускает все 10 одновременно
        │
        ▼
   база поднялась, но получает лавину переподключений
        │
        ▼
   отказ усугубляется

Приложение при этом было полностью работоспособно — оно просто не могло обратиться к базе.

Правило: liveness-проверка касается только самого процесса. Проверка «отвечает ли мой HTTP-сервер» корректна. Проверка «доступна ли база» — нет.

Готовность зависимостей — задача readiness-пробы, и её отказ должен убирать экземпляр из балансировки, а не перезапускать его.

Чем проверять

ИнструментПлюсыМинусы
curlУниверсаленОбычно не установлен в slim-образах
wgetЕсть в Alpine (BusyBox)Другой синтаксис
python -cВсегда есть в Python-образеМедленнее: запуск интерпретатора
Скомпилированный healthcheckБыстрый, без зависимостейНужно собирать

Для Python-образов вариант без внешних зависимостей:

dockerfile
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-сервиса:

dockerfile
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 — по умолчанию последние пять записей с кодом возврата, временем и выводом.

Где смотреть результат

bash
docker inspect <container> --format '{{json .State.Health}}'

Структура:

ПолеСодержимое
Statusstarting, healthy, unhealthy
FailingStreakТекущее число неудач подряд
Log[]Последние проверки: Start, End, ExitCode, Output

Поле Output содержит вывод команды — главный источник информации при разборе ложных срабатываний.


Команды и примеры

Подготовка

bash
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

bash
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
text
наблюдаем переход статуса:
   3s: starting
   6s: healthy
   9s: healthy
  12s: healthy
  15s: healthy
  18s: healthy

Статус виден и в docker ps:

bash
docker ps --filter name=hc-basic --format 'table {{.Names}}\t{{.Status}}'
text
NAMES      STATUS
hc-basic   Up 20 seconds (healthy)

Подробности:

bash
docker inspect hc-basic --format '{{json .State.Health}}' | python3 -m json.tool | head -20
json
{
    "Status": "healthy",
    "FailingStreak": 0,
    "Log": [
        {
            "Start": "2026-07-30T14:22:10.481Z",
            "End": "2026-07-30T14:22:10.612Z",
            "ExitCode": 0,
            "Output": ""
        }
    ]
}
bash
docker rm -f hc-basic > /dev/null

Проблема медленного старта

bash
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
text
=== без --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:

bash
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
text
=== с --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

bash
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
text
=== приложение начинает отвечать 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 продолжает работать:

bash
docker ps --filter name=hc-fail --format '{{.Names}} {{.Status}}'
docker inspect hc-fail --format 'Running: {{.State.Running}}  RestartCount: {{.RestartCount}}'
text
hc-fail Up 45 seconds (unhealthy)
Running: true  RestartCount: 0

Docker пометил container как нездоровый и ничего не сделал. Ни перезапуска, ни остановки.

Проверим, что и restart policy не помогает:

bash
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
text
health=unhealthy restarts=0

Ноль перезапусков. Restart policy реагирует на завершение процесса, а не на статус health.

События health

bash
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
text
1785492141 health_status: healthy
1785492166 health_status: unhealthy

Именно на эти события подписываются внешние инструменты автоматического восстановления.

Отладка ложных срабатываний

Поле Output содержит вывод команды проверки:

bash
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
text
unhealthy
--- вывод последней проверки ---
127: /bin/sh: 1: curl: not found

Код 127 и сообщение curl: not found (урок 4.6). Приложение работало нормально — сломалась сама проверка.

Это самая частая причина ложного unhealthy: команда проверки использует инструмент, отсутствующий в образе.

Проверка без внешних зависимостей

bash
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
text
статус: healthy

Никаких дополнительных пакетов не потребовалось.

Альтернатива для образов без Python — проверка TCP-порта средствами оболочки:

bash
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
text
статус: healthy

Отмена унаследованной проверки

bash
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}}'
text
базовый: задан
после NONE: NONE

Нужно, когда базовый образ содержит проверку, не подходящую вашему приложению.

Переопределение при запуске

Проверку можно задать или отключить без пересборки:

bash
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
text
проверка отключена
своя проверка: healthy

Задание в Compose

yaml
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 с подстановкой переменных.

Уборка

bash
cd /tmp
docker rmi -f $(docker images -q --filter 'reference=hc:*') 2>/dev/null || true
rm -rf /tmp/healthcheck

Практическое упражнение

Задание. Настройте healthcheck для сервиса, который стартует 30 секунд и периодически теряет связь с базой данных.

Требования:

  1. Container не должен помечаться unhealthy во время старта.
  2. Готовность должна обнаруживаться в течение 3 секунд после её наступления.
  3. Проверка не должна зависеть от доступности базы данных — обоснуйте письменно.
  4. Проверка не должна требовать установки дополнительных пакетов.
  5. Отказ приложения должен обнаруживаться не дольше чем за 30 секунд.

Приведите подобранные значения параметров с обоснованием каждого и докажите выполнение требований измерением.

Подсказки

Подсказка 1

Требования 1 и 2 задаются парой --start-period и --start-interval.

Подсказка 2

Требование 5 определяет произведение interval × retries.

Подсказка 3

Для требования 3 приложение должно иметь два endpoint: один проверяет только себя, другой — зависимости.

Решение

Сначала выполните задание самостоятельно.

Показать решение
bash
#!/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

Ожидаемый вывод:

text
═══ Требования 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-period45sСтарт занимает 30 с; запас 1.5× покрывает замедление на нагруженной машине
--start-interval2sТребование 2: готовность обнаруживается не дольше чем за 2 с
--interval10sВместе с retries=3 даёт 30 с на обнаружение отказа
--retries3Одиночный сбой сети не переводит в unhealthy
--timeout3sБольше времени ответа здорового сервиса, меньше 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.

Проверка результата

bash
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, где известна конфигурация
Унаследованная проверка не подходитНе знают про NONEHEALTHCHECK NONE отменяет её
Не смотрят Output при разбореНе знают о поле.State.Health.Log[].Output содержит вывод команды

Контрольные вопросы

На понимание:

  1. Что Docker делает с container, получившим статус unhealthy?
  2. Чем starting отличается от healthy с точки зрения счётчика неудач?
  3. Почему healthcheck не должен проверять доступность базы данных?
  4. Чем liveness отличается от readiness и какой из них выражает HEALTHCHECK?
  5. Почему одна успешная проверка сбрасывает счётчик неудач?

На применение:

  1. Как настроить проверку для приложения, стартующего 40 секунд?
  2. Как написать проверку HTTP-endpoint без установки дополнительных пакетов?
  3. Как отменить healthcheck, унаследованный от базового образа?

На диагностику:

  1. Container помечен unhealthy, но приложение отвечает на запросы. Что проверить первым?
  2. Сервис с --restart=always находится в состоянии unhealthy уже час и не перезапускается. Почему?

Краткое резюме

  1. HEALTHCHECK задаёт команду; код 0 — здоров, 1 — нездоров, 2 зарезервирован.
  2. Docker только помечает статус и генерирует событие — перезапуск не выполняется.
  3. Главное применение в этом курсе — depends_on: condition: service_healthy в Compose.
  4. Статус проходит путь startinghealthyunhealthy; в starting неудачи не считаются.
  5. --start-period решает проблему медленного старта, --start-interval ускоряет обнаружение готовности.
  6. Отказ обнаруживается за interval × retries; одна успешная проверка сбрасывает счётчик.
  7. Проверка не должна касаться внешних зависимостей — это ведёт к каскадному отказу.
  8. HEALTHCHECK ближе к liveness; readiness требует отдельного endpoint.
  9. Для Python-образов проверка через urllib не требует дополнительных пакетов.
  10. Поле .State.Health.Log[].Output — основной источник при разборе ложных срабатываний.

Официальные источники

ИсточникСсылкаЧто подтверждает
Dockerfile reference: HEALTHCHECKhttps://docs.docker.com/reference/dockerfile/#healthcheckСинтаксис, все параметры, коды возврата, форма NONE
docker run referencehttps://docs.docker.com/reference/cli/docker/container/run/Флаги --health-cmd, --health-interval, --no-healthcheck
docker inspect referencehttps://docs.docker.com/reference/cli/docker/inspect/Поля .State.Health: Status, FailingStreak, Log
Compose services: healthcheckhttps://docs.docker.com/reference/compose-file/services/Задание проверки в Compose, CMD и CMD-SHELL, start_interval
Compose startup orderhttps://docs.docker.com/compose/how-tos/startup-order/depends_on с condition: service_healthy
docker events referencehttps://docs.docker.com/reference/cli/docker/system/events/Событие health_status
Kubernetes: probeshttps://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/Различие liveness и readiness

Навигация

← Предыдущий материал
Вернуться к разделу
Следующий материал → Воспроизводимые сборки
Главное оглавление

Markdown на GitHub ↗