~/guides/docker-compose-healthcheck-race

Docker Compose

Docker Compose depends_on и healthcheck: почему сервис стартует раньше базы

Как условие service_healthy решает race condition при старте контейнеров, и почему просто depends_on без него не работает.

Приложение падает с ошибкой подключения к базе данных в первые секунды после docker compose up, хотя в файле честно прописан depends_on с именем сервиса базы. Через пару секунд, если перезапустить вручную, все работает. Проблема не в depends_on как таковом, а в том, что он по умолчанию означает совсем не то, что кажется на первый взгляд.

Что depends_on проверяет на самом деле по умолчанию

Базовый depends_on без дополнительных условий гарантирует только порядок запуска контейнера, а не готовность сервиса внутри него принимать соединения.

services:
  web:
    image: myapp:latest
    depends_on:
      - db
  db:
    image: postgres:16

Здесь Docker Compose запустит контейнер db раньше, чем web, это правда. Но само понятие “запущен” для Docker означает лишь то, что процесс postgres внутри контейнера начал стартовать, а не то, что PostgreSQL уже прошел инициализацию и готов принимать TCP соединения на своем порту. Между этими двумя моментами вполне может быть разрыв в несколько секунд, особенно при первом запуске с инициализацией пустой базы данных.

Явное условие через healthcheck

Решение — добавить в сервис базы данных healthcheck, реальную проверку готовности, а в зависимом сервисе указать явное условие ожидания именно здоровья, а не просто факта запуска контейнера.

services:
  web:
    image: myapp:latest
    depends_on:
      db:
        condition: service_healthy

  db:
    image: postgres:16
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 5s
      timeout: 3s
      retries: 5

Теперь Docker Compose не просто запустит db раньше web, а будет периодически выполнять команду pg_isready внутри контейнера db, и только когда эта проверка несколько раз подряд успешно отработает, контейнер получит статус healthy, и лишь после этого начнется запуск web.

Три доступных условия depends_on

Условие Что означает Когда уместно
service_started Контейнер запущен, процесс стартовал Сервисы без состояния готовности, простые скрипты
service_healthy Пройден healthcheck сервиса База данных, кеш, очередь сообщений
service_completed_successfully Контейнер завершился с кодом 0 Одноразовые задачи вроде миграций базы данных

Третье условие особенно удобно для сценария с миграциями: отдельный сервис-контейнер выполняет миграцию схемы базы и завершается, а основное приложение стартует только после его успешного завершения.

services:
  migrate:
    image: myapp:latest
    command: npm run migrate
    depends_on:
      db:
        condition: service_healthy

  web:
    image: myapp:latest
    depends_on:
      migrate:
        condition: service_completed_successfully

Частая ошибка: healthcheck без реальной проверки готовности

healthcheck:
  test: ["CMD", "true"]

Такой healthcheck технически существует и формально проходит мгновенно, но не проверяет вообще ничего осмысленного про готовность сервиса, это простая имитация проверки, которая ничем не отличается от полного отсутствия healthcheck по сути решаемой проблемы. Проверка должна реально стучаться в порт или выполнять реальную команду вроде pg_isready, redis-cli ping или простого curl на endpoint здоровья приложения.

Как быстро увидеть свою реальную проблему

Если сомневаетесь, есть ли в вашем стеке эта проблема, Docker Compose Service Graph покажет все связи depends_on в вашем файле, и по этому списку легко заметить сервисы, у которых нет условия healthy, хотя логически они точно должны быть готовы принимать соединения, прежде чем зависимый сервис начнет их использовать.

Итоговый чеклист

Базовый depends_on без условия гарантирует только порядок запуска контейнеров, а не готовность сервиса внутри принимать реальные соединения.

Для баз данных, кешей и очередей сообщений используйте condition service_healthy вместе с реальным healthcheck, который действительно проверяет готовность, а не имитирует проверку заглушкой.

Для одноразовых задач вроде миграций используйте condition service_completed_successfully вместо healthcheck, потому что такой контейнер не должен продолжать работать бесконечно.

Настраивайте interval, timeout и retries healthcheck разумно: слишком редкая проверка увеличит время ожидания старта, слишком частая создаст лишнюю нагрузку на сервис в момент его собственной инициализации.