HEALTHCHECK: проверка живости контейнера
Docker считает контейнер «живым», пока процесс PID 1 не упал. Но процесс может работать и при этом быть полусломан: Postgres запустился, но connections отказывает; HTTP-сервер слушает порт, но 500 на каждый запрос; Airflow worker процесс жив, но heartbeat не отправляется. Для таких случаев существует HEALTHCHECK — инструкция в Dockerfile (или в compose), которая периодически выполняется внутри контейнера и определяет реальное состояние сервиса.
В этом уроке: синтаксис HEALTHCHECK, exit codes, как HEALTHCHECK влияет на compose / depends_on / restart policy.
Синтаксис HEALTHCHECK в Dockerfile
HEALTHCHECK [OPTIONS] CMD command
Параметры:
--interval=30s— как часто запускать (default 30s, минимум 1s)--timeout=5s— таймаут на один прогон (если CMD виснет — считается unhealthy)--start-period=30s— initial grace period после запуска контейнера. В это время неудачные healthcheck’и не считаются «unhealthy». Полезно для slow-starting сервисов (Java, Postgres init).--start-interval=5s— интервал во время start-period (default 5s)--retries=3— сколько подряд неудач до пометки unhealthy (default 3)
CMD может быть в shell или exec form:
# Shell form
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
CMD curl -f http://localhost:8080/health || exit 1
# Exec form
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
CMD ["curl", "-f", "http://localhost:8080/health"]
CMD должен exit с одним из трёх кодов:
- 0 — healthy
- 1 — unhealthy
- 2 — reserved, не использовать (Docker считает это unhealthy, но это deprecated)
Стандартный паттерн || exit 1: если curl упал — exit 1; если успешен — exit 0.
Exit codes и состояния
Контейнер с HEALTHCHECK имеет дополнительный статус. docker ps показывает:
$ docker ps
CONTAINER ID IMAGE STATUS NAMES
abc123def456 my-app Up 5 minutes (healthy) app
Статусы:
starting— контейнер запущен, но ещё в start-period; первый healthcheck не отработалhealthy— последний healthcheck успешенunhealthy—retriesнеудач подряд
# Получить только healthcheck status
docker inspect --format '{{.State.Health.Status}}' app
# healthy
# Получить лог healthcheck'ов (последние 5)
docker inspect --format '{{json .State.Health.Log}}' app | jq
# [
# {
# "Start": "2026-05-15T10:00:00Z",
# "End": "2026-05-15T10:00:00.123Z",
# "ExitCode": 0,
# "Output": "OK"
# },
# ...
# ]
Реальный пример: HTTP-сервис
FROM python:3.13-slim
RUN apt-get update && apt-get install -y --no-install-recommends curl && \
rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 8080
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD curl -fs http://localhost:8080/health || exit 1
CMD ["python", "server.py"]
Что это значит:
- start-period=10s: первые 10 секунд после старта healthcheck’и не считаются failure (даже если падают). Это даёт серверу время инициализироваться.
- interval=30s: каждые 30 секунд проверка.
- timeout=5s: если /health долго не отвечает (>5s) — считается failure.
- retries=3: 3 подряд failure → unhealthy.
В Python-приложении нужно реализовать /health endpoint:
# server.py (упрощённо)
from fastapi import FastAPI
import psycopg2
app = FastAPI()
@app.get("/health")
def health():
try:
# Лёгкая проверка зависимостей
conn = psycopg2.connect("...")
conn.close()
return {"status": "ok"}
except Exception as e:
# FastAPI вернёт 500 по умолчанию через исключение
raise HTTPException(status_code=503, detail=str(e))
Healthcheck без curl: альтернативы
curl или wget нужно ставить через apt — +5MB к образу. Альтернативы:
Python:
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8080/health', timeout=3)"
Использует встроенный Python для HTTP-проверки. Не нужен curl.
exec через приложение:
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
CMD /app/healthcheck.sh
healthcheck.sh — скрипт, который знает специфику сервиса.
Для DB:
# Postgres
HEALTHCHECK --interval=10s --timeout=5s --start-period=10s --retries=5 \
CMD pg_isready -U postgres || exit 1
# Redis
HEALTHCHECK --interval=10s --timeout=3s --retries=3 \
CMD redis-cli ping | grep PONG || exit 1
HEALTHCHECK в Compose
Compose может объявить HEALTHCHECK на уровне сервиса (часто override от Dockerfile):
# docker-compose.yml
services:
app:
image: my-app:v1
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
interval: 30s
timeout: 5s
start_period: 10s
retries: 3
db:
image: postgres:16
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 10s
timeout: 5s
retries: 5
Compose имеет два формата test:
["CMD", "curl", "-f", "http://..."]— exec form (без shell)["CMD-SHELL", "pg_isready -U postgres"]— shell form (через /bin/sh -c)
Compose overrides Dockerfile HEALTHCHECK для этого сервиса. Если хочешь полностью отключить healthcheck:
services:
app:
healthcheck:
disable: true
depends_on с condition: service_healthy
Самый полезный use case HEALTHCHECK в compose — синхронизация старта сервисов:
services:
app:
image: my-app:v1
depends_on:
db:
condition: service_healthy # ждёт пока db не healthy
cache:
condition: service_started # ждёт только что cache запустился (default)
db:
image: postgres:16
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 3s
start_period: 30s
retries: 10
cache:
image: redis:7
condition: service_healthy блокирует старт app пока db не вернёт healthy. Это решает классическую гонку «app стартует, пытается connect к Postgres, который ещё в init» — без healthcheck app падает с ConnectionRefusedError.
Restart policy и healthcheck
Liveness и readiness probe в KubernetesDocker по дефолту НЕ перезапускает unhealthy контейнеры — только упавшие (exit code != 0). Чтобы был auto-restart по unhealthy, нужно использовать orchestrator (Swarm, Kubernetes) или внешнюю систему мониторинга.
services:
app:
image: my-app:v1
restart: unless-stopped # auto-restart при exit
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
interval: 30s
timeout: 5s
retries: 3
restart: unless-stopped рестартит, если процесс упал. Но unhealthy != crashed. Standalone Docker не рестартит unhealthy.
В Kubernetes аналог — liveness probe: периодически вызывает endpoint, при failure k8s kill’ит pod и заменяет его. Это полноценный self-healing.
# k8s pod spec
livenessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 30
periodSeconds: 30
timeoutSeconds: 5
failureThreshold: 3
HEALTHCHECK NONE: отключение
Иногда нужно отключить унаследованный HEALTHCHECK из базового образа:
FROM nginx:1.27 # nginx имеет HEALTHCHECK по умолчанию
HEALTHCHECK NONE # отключаем
Это полезно если параметры базового HEALTHCHECK не подходят (например, нужен другой interval).
Производительность HEALTHCHECK
Healthcheck выполняется внутри контейнера и тратит ресурсы (CPU, memory). На machine c 50+ контейнерами при interval=5s healthcheck’и могут заметно нагружать систему.
Best practices:
- Использовать realistic interval: 10-30s для большинства сервисов. 1s только для критичных, где нужен super-fast failover.
- Лёгкий healthcheck: проверять только liveness (процесс живой, базовые deps доступны). НЕ делать тяжёлые операции (
SELECT *из базы, full-cache scan). - Различать liveness и readiness: liveness — «процесс жив»; readiness — «готов принимать трафик». В k8s это два разных probe.
# Хороший /health (только liveness)
@app.get("/health")
def health():
return {"status": "ok"} # просто что-то ответил
# Хороший /ready (readiness, проверяет deps)
@app.get("/ready")
def ready():
db_ok = check_db_connection_quickly()
if not db_ok:
raise HTTPException(503, "db unavailable")
return {"status": "ready"}
Попробуй сам
Создай контейнер с healthcheck:
mkdir healthcheck-demo && cd healthcheck-demo
cat > server.py <<'EOF'
from http.server import HTTPServer, BaseHTTPRequestHandler
import time, random
start = time.time()
class H(BaseHTTPRequestHandler):
def do_GET(self):
# Симуляция slow start: первые 5 секунд /health возвращает 500
if time.time() - start < 5:
self.send_response(500); self.end_headers()
self.wfile.write(b"warming up")
return
# После 5 секунд: 80% успехов, 20% failure
if self.path == "/health":
if random.random() < 0.8:
self.send_response(200); self.end_headers()
self.wfile.write(b"OK")
else:
self.send_response(500); self.end_headers()
else:
self.send_response(200); self.end_headers()
self.wfile.write(b"hello")
HTTPServer(("0.0.0.0", 8080), H).serve_forever()
EOF
cat > Dockerfile <<'EOF'
FROM python:3.13-slim
RUN apt-get update && apt-get install -y --no-install-recommends curl && rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY server.py .
EXPOSE 8080
HEALTHCHECK --interval=5s --timeout=2s --start-period=10s --retries=3 \
CMD curl -fs http://localhost:8080/health || exit 1
CMD ["python", "server.py"]
EOF
docker build -t hc-demo .
docker run -d --name hc hc-demo
# Wait и проверяй статус каждые 5 секунд
for i in 1 2 3 4 5 6; do
sleep 5
docker inspect --format '{{.State.Health.Status}}' hc
done
# Получи лог healthcheck'ов
docker inspect --format '{{json .State.Health.Log}}' hc | jq
# Cleanup
docker rm -f hc
cd .. && rm -rf healthcheck-demo
docker rmi hc-demo