Тема кажется простой: есть приложение, есть база данных, приложение иногда стартует раньше базы и падает. Самое быстрое решение — вставить sleep 10, sleep 30 или даже sleep 60 перед запуском приложения. На локальной машине это часто «лечит» проблему, но на сервере, в CI/CD и при перезапусках после обновлений такой костыль начинает жить своей жизнью: сегодня 30 секунд хватает, завтра миграция базы заняла 45 секунд, послезавтра контейнер формально запустился, но ещё не принимает подключения.
В Docker Compose для этой задачи есть более управляемый механизм: docker compose healthcheck плюс длинная форма depends_on с condition. В этой статье разберём, что именно делает depends_on condition, как организовать wait for database без слепого ожидания, как связаны restart policy и healthcheck, а также где всё равно нужны ретраи на уровне приложения. Если вы разворачиваете такие проекты на сервере, удобнее делать это на отдельном VDS для Docker-проектов, где вы контролируете версии Docker, сети, диски и лимиты ресурсов.
Короткая версия:
depends_onзадаёт порядок запуска,healthcheckописывает готовность сервиса, аsleepпросто надеется, что мир за указанное время станет лучше.
Почему sleep в Docker Compose почти всегда плохой сигнал
Команда sleep не проверяет состояние сервиса. Она не знает, поднялся ли PostgreSQL, применились ли WAL-журналы, завершилась ли инициализация MySQL, доступен ли Redis после загрузки RDB/AOF, готов ли HTTP API отвечать не только на TCP-соединение, но и на реальный запрос. sleep просто ждёт фиксированное время.
Из-за этого появляются две типичные ошибки. Первая — ожидание слишком короткое: приложение стартует, получает connection refused, падает или уходит в бесконечный цикл ошибок. Вторая — ожидание слишком длинное: каждый деплой искусственно тормозит на 30–60 секунд, хотя база готова через 3 секунды. Особенно неприятно это в пайплайнах, где несколько сервисов запускаются последовательно.
Есть и третья проблема: фиксированная пауза маскирует настоящую причину. Например, база может быть недоступна не из-за долгого старта, а из-за неверного имени хоста, неправильного пароля, отсутствующей сети Compose или ошибки миграции. С sleep вы просто ждёте, а потом видите ту же ошибку. С healthcheck вы быстрее понимаете, какой именно компонент не стал здоровым.
Как Docker Compose на самом деле запускает зависимости
Важно различать «контейнер запущен» и «сервис готов». По умолчанию depends_on в короткой форме гарантирует только порядок запуска контейнеров. То есть Compose сначала создаст и запустит зависимость, а потом зависимый сервис. Но если база данных уже имеет статус running, это ещё не означает, что она принимает подключения и готова обслуживать запросы.
Короткая форма выглядит так:
services:
web:
build: .
depends_on:
- db
db:
image: postgres:16
Такой вариант полезен, но недостаточен для задач wait for database. Контейнер db может перейти в running почти сразу, а PostgreSQL внутри ещё будет выполнять инициализацию кластера. Для настоящего ожидания готовности нужна длинная форма depends_on и условие service_healthy.
В современной версии Docker Compose доступны несколько условий:
service_started— зависимость должна быть запущена, это примерно поведение короткой формы;service_healthy— зависимость должна иметь успешныйhealthcheck;service_completed_successfully— одноразовый сервис должен завершиться с кодом 0, удобно для миграций или подготовительных задач.
Ключевой момент: condition: service_healthy работает только если у зависимого сервиса действительно описан healthcheck. Если проверки здоровья нет, Compose не из чего понять, что сервис готов.

Минимальный рабочий пример: PostgreSQL и приложение
Начнём с базового Compose-файла. В нём приложение web стартует только после того, как PostgreSQL прошёл проверку pg_isready.
services:
db:
image: postgres:16
environment:
POSTGRES_DB: app
POSTGRES_USER: app
POSTGRES_PASSWORD: change-me
healthcheck:
test: ['CMD-SHELL', 'pg_isready -U app -d app']
interval: 5s
timeout: 3s
retries: 12
start_period: 20s
web:
build: .
depends_on:
db:
condition: service_healthy
command: ['python', 'app.py']
Разберём параметры. test — команда проверки. Для PostgreSQL лучше использовать pg_isready, потому что он проверяет именно готовность сервера принимать подключения. interval задаёт периодичность проверки, timeout — сколько ждать ответ каждой проверки, retries — сколько неудач подряд нужно для статуса unhealthy. start_period даёт сервису время на начальную инициализацию: ошибки внутри этого периода не будут сразу считаться фатальными для healthcheck.
Запускаем:
docker compose up -d
Смотрим состояние:
docker compose ps
Если всё хорошо, у базы будет состояние примерно healthy, а приложение стартует после этого. Если база не проходит healthcheck, web не должен стартовать как будто всё нормально: проблема становится видимой уже на уровне инфраструктурного описания.
Варианты healthcheck для популярных сервисов
Healthcheck должен проверять не абстрактное существование процесса, а именно то, что нужно зависимому сервису. Для базы данных это обычно подключение с нужными параметрами. Для HTTP-сервиса — запрос к endpoint готовности. Для очереди — команда диагностики брокера.
PostgreSQL
healthcheck:
test: ['CMD-SHELL', 'pg_isready -U app -d app']
interval: 5s
timeout: 3s
retries: 12
start_period: 20s
Если база слушает не стандартный порт или нужен конкретный хост, добавьте параметры -h и -p. Внутри одного Compose-проекта чаще всего достаточно имени сервиса и стандартного порта.
MySQL или MariaDB
healthcheck:
test: ['CMD-SHELL', 'mysqladmin ping -h 127.0.0.1 -uroot -p$$MYSQL_ROOT_PASSWORD']
interval: 5s
timeout: 3s
retries: 20
start_period: 30s
Обратите внимание на $$MYSQL_ROOT_PASSWORD. В Compose один знак доллара используется для подстановки переменных самим Compose, поэтому для передачи переменной внутрь контейнера часто нужен двойной доллар.
Redis
healthcheck:
test: ['CMD', 'redis-cli', 'ping']
interval: 5s
timeout: 3s
retries: 10
start_period: 5s
Для Redis простая проверка PING обычно достаточна. Если включена авторизация, healthcheck нужно адаптировать под вашу схему доступа, иначе контейнер будет считаться unhealthy при фактически рабочем Redis.
HTTP-приложение
healthcheck:
test: ['CMD-SHELL', 'wget -q -O /dev/null http://127.0.0.1:8080/health']
interval: 10s
timeout: 3s
retries: 6
start_period: 15s
В endpoint /health не стоит складывать тяжёлую бизнес-логику. Для readiness-проверки обычно достаточно понять, что процесс жив, слушает порт и может выполнить критически важные зависимости. Если endpoint делает сложные запросы, healthcheck сам может стать источником нагрузки.
depends_on condition: service_healthy в полном сценарии
Рассмотрим более реалистичный пример: база, миграции и веб-приложение. Нам нужно, чтобы база стала healthy, затем одноразовый контейнер применил миграции, и только после успешных миграций стартовал web.
services:
db:
image: postgres:16
environment:
POSTGRES_DB: app
POSTGRES_USER: app
POSTGRES_PASSWORD: change-me
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ['CMD-SHELL', 'pg_isready -U app -d app']
interval: 5s
timeout: 3s
retries: 12
start_period: 20s
migrate:
build: .
depends_on:
db:
condition: service_healthy
command: ['python', 'manage.py', 'migrate']
restart: 'no'
web:
build: .
depends_on:
migrate:
condition: service_completed_successfully
command: ['python', 'app.py']
restart: unless-stopped
volumes:
pgdata:
Такой подход лучше, чем запускать миграции внутри старта web-контейнера и надеяться на порядок событий. Если миграция падает, приложение не стартует как ни в чём не бывало. Администратор видит отдельный контейнер migrate и его логи.
Посмотреть логи миграций можно так:
docker compose logs migrate
Повторно запустить одноразовую задачу:
docker compose run --rm migrate
Для продакшена этот шаблон особенно полезен, если приложение не должно принимать трафик до завершения схемных изменений. Но важно помнить: миграции должны быть написаны безопасно, с учётом блокировок и обратной совместимости. Compose задаёт порядок, но не делает миграцию zero-downtime автоматически.
Что делает restart policy и чего она не делает
restart policy в Compose часто путают с healthcheck. Это разные механизмы. Параметр restart управляет перезапуском контейнера, когда основной процесс завершился или когда Docker daemon был перезапущен. Он не означает: «если healthcheck стал unhealthy, перезапусти контейнер».
Примеры:
restart: 'no'
restart: on-failure
restart: unless-stopped
restart: always
Для веб-приложений на сервере чаще всего используют restart: unless-stopped. Это значит, что контейнер поднимется после перезагрузки Docker или сервера, если вы явно не остановили его вручную. Для одноразовых задач, например миграций, обычно ставят restart: 'no' или аккуратно используют on-failure, если повтор безопасен.
Если контейнер стал unhealthy, но главный процесс продолжает работать, Docker Compose в обычном standalone-режиме не обязан перезапускать его. Это неожиданно для многих. Healthcheck — это индикатор состояния, а не самостоятельный watchdog. Если хотите отдельно разобраться в границах этих механизмов, посмотрите материал про Docker healthcheck и restart policy.
Если вам нужно автоматическое восстановление именно по состоянию приложения, есть несколько вариантов: научить приложение завершаться при критической деградации, использовать внешний мониторинг и оркестратор, либо отдельно реализовать контролируемый watchdog. Но бездумно перезапускать сервис при каждом временном сбое тоже опасно: можно получить restart loop и усугубить инцидент.
Параметр restart внутри depends_on: не путайте с restart policy
В длинной форме depends_on можно встретить ещё один параметр restart. Он относится не к политике рестарта контейнера, а к поведению зависимого сервиса при операциях Compose.
services:
web:
build: .
depends_on:
db:
condition: service_healthy
restart: true
restart: unless-stopped
db:
image: postgres:16
healthcheck:
test: ['CMD-SHELL', 'pg_isready -U postgres']
interval: 5s
timeout: 3s
retries: 12
Здесь depends_on.db.restart: true означает: если Compose явно пересоздаёт или перезапускает зависимость в рамках управляемой операции, зависимый сервис тоже может быть перезапущен. Это не то же самое, что restart: unless-stopped на уровне сервиса web. Второй параметр — это обычная restart policy контейнера.
Практическое правило простое: restart на уровне сервиса отвечает за жизненный цикл самого контейнера, а restart внутри depends_on — за реакцию на изменения зависимости при действиях Compose. Если сомневаетесь, явно разделяйте эти две идеи в комментариях к Compose-файлу.
Почему ретраи в приложении всё равно нужны
Даже идеальный docker compose healthcheck не отменяет ретраи подключения в приложении. depends_on condition помогает при первичном старте, но не решает все runtime-ситуации. База может перезапуститься позже, сеть может кратковременно моргнуть, контейнер с Redis может быть пересоздан, диск может дать задержку, а DNS внутри bridge-сети — обновиться не мгновенно.
Поэтому приложение должно уметь переживать временную недоступность зависимостей. Для веб-приложения это обычно означает пул соединений с ограниченными таймаутами, повтор подключения с backoff, корректную обработку ошибок и понятные логи. Для фоновых воркеров — повтор задачи, dead-letter очередь или отложенный retry в зависимости от стека.
Надёжная схема выглядит так: Compose не запускает приложение до готовности базы при первичном старте, а само приложение не падает безвозвратно при кратковременном сбое после запуска. Это не дублирование, а защита на разных уровнях.
Диагностика: как понять, почему сервис не стал healthy
Первый инструмент — docker compose ps. Он показывает, какие контейнеры запущены и какой у них health-статус.
docker compose ps
Второй — логи конкретного сервиса:
docker compose logs db
Можно смотреть последние строки и не тонуть в старых логах:
docker compose logs --tail 100 db
Если нужно увидеть подробности healthcheck, используйте inspect. Имя контейнера можно взять из docker compose ps.
docker inspect --format '{{json .State.Health}}' project-db-1
Частые причины unhealthy довольно приземлённые: команда проверки отсутствует в образе, неверные переменные окружения, пароль не передан внутрь healthcheck, указан localhost там, где нужно имя сервиса, порт слушается только на другом интерфейсе, база ещё выполняет восстановление после аварийного завершения.
Для быстрой ручной проверки можно зайти в контейнер:
docker compose exec db sh
И выполнить команду healthcheck вручную. Это помогает отличить проблему Compose-синтаксиса от реальной проблемы сервиса.

Когда sleep всё-таки допустим
Полностью запрещать sleep не стоит. В одноразовом локальном прототипе, учебном примере или временной отладке он может быть приемлем. Но в Compose-файле, который попадает на сервер, в репозиторий команды или в CI/CD, фиксированная задержка должна вызывать вопрос: что именно мы ждём и почему не можем проверить это явно?
Если хочется оставить sleep ради простоты, хотя бы не прячьте за ним ошибку. Например, лучше сделать небольшой скрипт ожидания, который реально проверяет TCP-порт или выполняет SQL-команду, чем просто спать минуту. Но если сервис поддерживает нормальную команду диагностики, healthcheck почти всегда чище и понятнее.
Плохой вариант:
services:
web:
build: .
command: ['sh', '-c', 'sleep 30; python app.py']
depends_on:
- db
db:
image: postgres:16
Лучший вариант — описать готовность базы и зависимость от неё:
services:
web:
build: .
command: ['python', 'app.py']
depends_on:
db:
condition: service_healthy
db:
image: postgres:16
healthcheck:
test: ['CMD-SHELL', 'pg_isready -U postgres']
interval: 5s
timeout: 3s
retries: 12
start_period: 20s
Практические настройки таймингов healthcheck
Универсальных значений нет, но есть рабочая логика. start_period должен покрывать нормальную начальную инициализацию сервиса. Для PostgreSQL без тяжёлого восстановления может хватить 10–20 секунд, для MySQL с первым созданием базы — 30 секунд и больше. interval обычно ставят 5–10 секунд: чаще не нужно, иначе проверка сама становится шумной. timeout должен быть меньше interval, чтобы проверки не накладывались по смыслу. retries задаёт, сколько последовательных ошибок нужно для unhealthy.
Не делайте healthcheck слишком агрессивным. Проверка каждую секунду с тяжёлым SQL-запросом — плохая идея. Не делайте его и слишком мягким: если сервис мёртв уже пять минут, статус healthy не должен сохраняться из-за огромных интервалов и повторов.
Для production-подхода полезно разделять liveness и readiness концептуально, даже если Compose даёт только один healthcheck. Проверка должна отвечать на вопрос: «можно ли сейчас направлять сюда зависимые сервисы или трафик?» Если ответ зависит от базы, кэша и миграций, отражайте это аккуратно, но не превращайте endpoint в полноценный нагрузочный тест.
Чек-лист для аккуратного Docker Compose
- Не используйте
sleepкак постоянное решение ожидания базы. - Добавляйте
healthcheckк сервисам, от готовности которых зависят другие контейнеры. - Используйте длинную форму
depends_onиcondition: service_healthyдля баз данных, брокеров и кэшей. - Для миграций применяйте
service_completed_successfully, если приложение не должно стартовать до их завершения. - Не ожидайте, что healthcheck сам перезапустит unhealthy-контейнер в обычном Compose.
- Подбирайте
restart policyпод тип сервиса: постоянный демон, одноразовая задача или воркер. - Обязательно добавляйте ретраи и таймауты на уровне приложения.
- Проверяйте health-статус через
docker compose ps, логи иdocker inspect.
Итог
docker compose healthcheck и depends_on condition закрывают типичную проблему старта сервисов в правильном месте: на уровне описания окружения. Вместо «подождать на всякий случай» вы явно говорите Compose: приложение зависит от базы, а база считается готовой только после успешной проверки.
Но не стоит ждать от Compose магии. Он помогает с порядком запуска и первичной готовностью, а не заменяет устойчивость приложения, мониторинг, резервное копирование и нормальные процедуры деплоя. Хороший Compose-файл — это не набор случайных контейнеров, а документированная карта зависимостей: кто кого ждёт, как проверяется готовность и что делать при сбоях.
Если вы сейчас видите в своём проекте sleep 30 перед запуском приложения, начните с малого: добавьте healthcheck к базе, замените короткий depends_on на длинный с condition: service_healthy, проверьте поведение при чистом старте и при перезапуске базы. Обычно уже этот шаг делает запуск предсказуемее, а диагностику — заметно проще.


