Learning Platform
Глоссарий Troubleshooting
Урок 12.04 · 24 мин
Средний
dockercomposedepends_onhealthcheckrace-condition

depends_on + healthcheck: правильная синхронизация

«У меня иногда работает, иногда нет». На 80% это про race condition при старте: app коннектится к Postgres, но Postgres ещё не готов. Корректное решение — depends_on с condition: service_healthy + правильно написанный healthcheck. В этом уроке разберём всё, что нужно: семантику depends_on, типичные healthcheck’и для Postgres, Redis, Kafka, и как Compose это оркестрирует.


Lifecycle процесса — ready, running, blocked, zombie

depends_on без condition — устаревшая семантика

services:
  postgres:
    image: postgres:17
    environment: { POSTGRES_PASSWORD: secret }

  app:
    image: myapp
    depends_on: [postgres]

Что это значит:

  • Compose стартует postgres до app.
  • Дождётся state running контейнера postgres (= главный процесс запустился).
  • Запустит app.

Что НЕ значит:

  • Не значит, что Postgres готов принимать соединения.
  • Не значит, что в app можно сразу делать psycopg.connect().

Между «контейнер running» и «приложение готово» — секунды (для Postgres до 5-10 секунд при первом запуске на новом volume из-за initdb). Если app сразу пытается подключиться — connection refused.

WARNING

depends_on без condition: service_healthy — это false sense of security. Junior смотрит в YAML, видит depends_on, успокаивается, потом удивляется почему «у меня иногда не работает». Всегда добавляй condition.


Правильно: condition: service_healthy

services:
  postgres:
    image: postgres:17
    environment: { POSTGRES_PASSWORD: secret }
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 5s
      timeout: 3s
      retries: 5
      start_period: 10s

  app:
    image: myapp
    depends_on:
      postgres:
        condition: service_healthy

Теперь compose:

  • Стартует postgres.
  • Ждёт start_period (10s) грейс.
  • Начинает гонять healthcheck pg_isready каждые 5 секунд.
  • Когда pg_isready возвращает exit code 0 -> state = healthy.
  • Только тогда стартует app.

app гарантированно поднимается после того, как Postgres готов.


Возможные condition

conditionСемантика
service_startedDefault. Контейнер в state running
service_healthyHealthcheck перешёл в healthy
service_completed_successfullyКонтейнер завершился с exit 0

Последний полезен для one-shot init-jobs:

services:
  migrate:
    image: myapp
    command: ["alembic", "upgrade", "head"]
    depends_on:
      postgres:
        condition: service_healthy

  app:
    image: myapp
    depends_on:
      migrate:
        condition: service_completed_successfully

app запускается только когда migrate отработал успешно. Если миграция упала — app не стартует.


Healthcheck в compose

Полная форма:

healthcheck:
  test: ["CMD-SHELL", "pg_isready -U postgres -d mydb"]
  interval: 5s
  timeout: 3s
  retries: 5
  start_period: 10s
  start_interval: 1s
ПараметрDefaultЧто значит
test-Команда. CMD-SHELL "..." через sh -c, CMD ["a","b"] exec
interval30sИнтервал между проверками после старта
timeout30sМаксимум на одну проверку
retries3Сколько fail подряд для unhealthy
start_period0sGrace period в начале. Fail здесь не считается
start_interval5sИнтервал в start_period (Docker 25+)

Форматы test:

test: ["CMD-SHELL", "pg_isready -U postgres"]   # рекомендую: shell-форма
test: ["CMD", "pg_isready", "-U", "postgres"]   # exec-форма, без shell
test: ["NONE"]                                   # отключить healthcheck (даже если есть в образе)

CMD-SHELL запускает в sh -c. Удобно для пайпов, env vars, переменных. CMD запускает напрямую.


Реальные healthcheck’и

Postgres

healthcheck:
  test: ["CMD-SHELL", "pg_isready -U postgres -d mydb"]
  interval: 5s
  timeout: 3s
  retries: 5
  start_period: 10s

pg_isready встроен в Postgres-образ, проверяет, что демон слушает и готов. Если у тебя нестандартный port или socket — добавь -h localhost -p $PORT.

Более строгая проверка — реальный SQL-запрос:

test: ["CMD-SHELL", "PGPASSWORD=$$POSTGRES_PASSWORD psql -U postgres -d mydb -c 'SELECT 1'"]

$$ экранирует $ в YAML, чтобы compose не пытался интерполировать переменную.

Redis

healthcheck:
  test: ["CMD", "redis-cli", "ping"]
  interval: 5s
  timeout: 3s
  retries: 5

redis-cli ping возвращает PONG и exit 0, если демон отвечает.

Kafka

Сложнее, потому что Kafka стартует долго (Zookeeper/KRaft + контроллер).

healthcheck:
  test: ["CMD-SHELL", "kafka-topics --bootstrap-server localhost:9092 --list || exit 1"]
  interval: 10s
  timeout: 10s
  retries: 10
  start_period: 30s

Запрос списка топиков работает только когда брокер reachable. start_period 30s — Kafka просто не успевает за меньшее.

HTTP-сервис

healthcheck:
  test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
  interval: 10s
  timeout: 5s
  retries: 3
  start_period: 15s

-f делает curl exit 22 при HTTP-ошибке. Endpoint /health приложение должно реализовать само — это не магия compose.

TIP

Если в образе нет curl, не добавляй его ради healthcheck — раздуешь образ. Используй wget (часто в alpine есть), либо nc -z localhost 8000, либо собственный mini-healthcheck-binary в Go (10 МБ vs 30 МБ для curl с зависимостями).


Как compose использует healthcheck

docker compose ps
# NAME              STATUS                  PORTS
# myproj-postgres-1 Up 2 minutes (healthy)  127.0.0.1:5432->5432/tcp
# myproj-app-1      Up 1 minute             0.0.0.0:8000->8000/tcp

(healthy) — состояние после успешных healthcheck’ов. (starting) — пока в start_period или ещё не было retries проверок. (unhealthy) — несколько fail подряд.

docker compose ps --format json | jq '.[] | {Name, Health}'

— programmatic-вариант для CI.

Lifecycle сервиса с healthcheck
docker compose upCompose стартует postgres-контейнер. Главный процесс postgres запустился. Health = starting
start_period
Grace 10sCompose не запускает healthcheck эти 10 секунд. Failures здесь не считаются. Дать сервису время на initdb
Healthcheck loopПосле grace начинаются проверки каждые 5 секунд: pg_isready -U postgres. Exit 0 = healthy, exit !=0 = fail
3 успеха подряд: healthyПосле trips подряд успехов state становится healthy. После N подряд fail (retries=5) — unhealthy
Compose запускает appdepends_on: condition: service_healthy выполнено — теперь стартует app, который зависит от postgres
App работаетApp открывает connection к postgres:5432 уверенно — БД точно готова

Что если healthcheck падает

docker compose ps
# myproj-postgres-1 Up 30 seconds (unhealthy)

Compose не убивает unhealthy-контейнер сам — это работа restart policy. Но app, который ждал condition: service_healthy, не стартует, пока БД unhealthy. Если так и осталось — compose в какой-то момент сдастся и отменит up:

Error response from daemon: dependency failed to start: container postgres is unhealthy

Дебаг:

docker compose logs postgres
# Видишь сами логи postgres — почему не стартует.

docker inspect <container> | jq '.[0].State.Health'
# История healthcheck-результатов с exit code и output.

start_period: про что чаще всего забывают

Без start_period healthcheck начинается сразу. Если сервис стартует медленно (Kafka, Elasticsearch, тяжёлые JVM-апликейшены), первые retries-проверок упадут, и compose может объявить unhealthy раньше, чем сервис реально готов.

healthcheck:
  test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
  start_period: 30s   # дай 30 секунд тишины перед первой проверкой

retries * interval + start_period — это максимальное время до признания unhealthy. Для тяжёлых сервисов запас в start_period полезен.


Попробуй сам

mkdir -p ./hc-demo && cd ./hc-demo

cat > compose.yml <<'YAML'
services:
  postgres:
    image: postgres:17
    environment: { POSTGRES_PASSWORD: secret }
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 3s
      timeout: 2s
      retries: 5
      start_period: 5s

  client:
    image: postgres:17
    depends_on:
      postgres:
        condition: service_healthy
    command: |
      sh -c "echo Postgres now healthy at $$(date); psql -h postgres -U postgres -c 'SELECT now();' "
    environment:
      PGPASSWORD: secret
YAML

# 1. Запусти и засеки время.
date && docker compose up
# Видишь, как postgres сначала starting, потом healthy, потом client сразу делает SELECT.

# 2. Сравни с broken-вариантом (без condition).
cat > compose.yml <<'YAML'
services:
  postgres:
    image: postgres:17
    environment: { POSTGRES_PASSWORD: secret }
  client:
    image: postgres:17
    depends_on: [postgres]
    command: |
      sh -c "echo Trying at $$(date); psql -h postgres -U postgres -c 'SELECT now();' || echo 'FAIL: postgres not ready'"
    environment:
      PGPASSWORD: secret
YAML

docker compose down -v
docker compose up
# Скорее всего увидишь FAIL: postgres not ready — race condition.

# 3. Cleanup.
docker compose down -v
cd ..
rm -rf hc-demo
NOTE

Healthcheck можно объявить и в Dockerfile через HEALTHCHECK CMD ... (модуль 8). Compose-уровень имеет приоритет: если есть и там, и там — compose использует своё. Но если в compose не указан — берёт из образа.

В следующем уроке — собираем реальный compose-стенд: Python ETL + Postgres с init.sql и работающим healthy-pattern.


Проверка знанийKnowledge check
У тебя в compose: app имеет depends_on: [postgres] без condition. Иногда стенд стартует чисто, иногда app падает с connection refused. Объясни почему так бывает (race condition), какой правильный compose-фрагмент это лечит, и какой healthcheck для postgres ты бы написал.
ОтветAnswer
Race condition: compose-семантика depends_on без condition ждёт только state running контейнера postgres — это значит главный процесс запустился. Внутри postgres-контейнера entrypoint ещё делает initdb (на пустом volume 2-10s), затем стартует daemon, который начинает слушать 5432. App стартует параллельно с этим: открывает TCP-сокет на postgres:5432, попадает в ConnectionRefused (никто не слушает) или ConnectionReset (TCP-стек ядра уже принимает, но процесс не accept'ит). На быстрой машине с уже init'еженным volume — postgres готов мгновенно, app проходит. На медленной — fail. Лечение: depends_on: postgres: condition: service_healthy + healthcheck в postgres-сервисе. Корректный healthcheck: test: ["CMD-SHELL", "pg_isready -U postgres"] + interval: 5s, timeout: 3s, retries: 5, start_period: 10s. pg_isready проверяет, что демон слушает и готов принимать соединения; start_period даёт initdb отработать без false-fail-проверок; retries: 5 даёт буфер на временные icmpы. С этим набором app стартует только когда Postgres реально готов.

Проверьте понимание

Результат: 0 из 0
Концептуальный
Вопрос 1 из 4. Что фундаментально отличает depends_on: [postgres] от depends_on: postgres: { condition: service_healthy }?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 5