Docker healthcheck: когда запускается первая проверка
Почему контейнер с interval: 1h целый час висит в starting, как start_period и start_interval ускоряют первую проверку и как прочитать это в docker inspect.
Короткий ответ: первая проверка ждёт полный interval
Если у сервиса стоит healthcheck с interval: 1h и больше ничего, первая проверка запустится через час после старта контейнера. Всё это время контейнер будет в состоянии starting, а depends_on с condition: service_healthy будет ждать. Так устроен планировщик проверок в Docker Engine, и это поведение описано в документации. Отдельного параметра «когда запустить первую проверку» нет. Его роль выполняет пара start_period и start_interval.
Справочник Dockerfile описывает правило одной фразой:
The health check will first run interval seconds after the container is started, and then again interval seconds after each previous check completes.
То есть первая проверка запускается через interval секунд после старта контейнера, а каждая следующая через interval секунд после завершения предыдущей. Пока не завершилась ни одна проверка, у Docker нет данных о здоровье, и статус остаётся starting. С interval: 1h этот статус держится час.
Ниже разбор трёх параметров, порядок их применения по исходникам Docker Engine, команды для чтения состояния и два рабочих фрагмента Compose: для медленно стартующей базы и для дешёвой проверки раз в 10 или 30 секунд. Если секцию healthcheck в Compose вы настраиваете впервые, сначала посмотрите разбор healthcheck в Docker Compose, а здесь речь только о времени.
Три параметра: interval, start_period и start_interval
interval задаёт паузу между проверками в обычном режиме. По умолчанию 30 секунд. Отсчёт идёт от завершения предыдущей проверки, а не от её начала, поэтому долгая проверка растягивает реальный период.
start_period задаёт льготный период после старта контейнера. По умолчанию 0 секунд, то есть льготного периода нет. Справочник Dockerfile формулирует так:
start period provides initialization time for containers that need time to bootstrap. Probe failure during that period will not be counted towards the maximum number of retries. However, if a health check succeeds during the start period, the container is considered started and all consecutive failures will be counted towards the maximum number of retries.
Пока идёт start_period, неудачные проверки не увеличивают счётчик FailingStreak, и контейнер не станет unhealthy. Но одна удачная проверка внутри этого периода сразу переводит контейнер в healthy, и льгота заканчивается: после этого каждая неудача считается.
start_interval задаёт паузу между проверками внутри start_period. По умолчанию 5 секунд. Справочник:
start interval is the time between health checks during the start period.
Это и есть ответ на вопрос из заголовка. Пока действует start_period, планировщик ждёт start_interval вместо interval. Первая проверка запускается через start_interval после старта, а не через час.
Почему start_interval не работает без start_period
Это стоит проверить по коду. В файле daemon/health.go Docker Engine (репозиторий moby) функция monitor выбирает паузу перед следующей проверкой так (сокращённо, по состоянию исходников на сентябрь 2026):
getInterval := func() time.Duration {
sinceStart := time.Since(started)
if sinceStart >= startPeriod {
return probeInterval
}
// ...
if status == containertypes.Starting {
remaining := startPeriod - sinceStart
if startInterval > remaining {
return remaining
}
return startInterval
}
return probeInterval
}
intervalTimer := time.NewTimer(getInterval())Из этого кода следуют четыре правила.
- Первый таймер создаётся тем же
getInterval(), что и все последующие. Отдельной логики для первой проверки нет. - При
start_periodравном нулю условиеsinceStart >= startPeriodистинно всегда, и функция возвращаетprobeInterval.start_intervalв этом случае не используется никогда, какое бы значение вы ни указали. start_intervalприменяется только пока статусstarting. Как только проверка прошла и контейнер сталhealthy, планировщик переходит наinterval, даже еслиstart_periodещё не истёк.- Пауза внутри льготного периода обрезается по его остатку. Комментарий в коде: «Cap the interval so we don't sleep past the end of the start period». Если до конца
start_periodосталось 3 секунды, аstart_intervalравен 5, следующая проверка будет через 3.
Итого расписание для interval: 1h, start_period: 60s и start_interval: 5s по этой логике выглядит так: первая проверка через 5 секунд после старта, дальше каждые 5 секунд, пока сервис не ответит успехом или пока не пройдёт минута. После первого успеха следующая проверка через час. Если минута прошла, а успеха не было, проверки продолжаются раз в час, и теперь каждая неудача считается в retries.
Какие версии Docker и Compose это поддерживают
start_period старый: в справочнике docker run флаг --health-start-period помечен как API 1.29+. start_interval моложе: флаг --health-start-interval требует API 1.44+, а это Docker Engine 25.0 (январь 2024). Проверить версию API движка и версию Compose можно так:
docker version --format '{{.Server.APIVersion}}'
docker compose versionЕсли Docker на сервере ещё не установлен, начните с установки Docker и Compose на VPS и вернитесь сюда: всё ниже требует работающего движка.
С Compose история чуть длиннее, и её стоит знать, если сервер обновлялся давно. Поле start_interval в схеме файла Compose появилось в библиотеке compose-go 1.17.0, которую подхватил Compose v2.20.2 (июль 2023). Но по исходникам pkg/compose/convert.go функция ToMobyHealthCheck в версиях v2.20.2 и v2.23.0 это поле в движок не передавала: файл проходил валидацию, а значение молча терялось. Передача появилась в v2.24.0 (январь 2024) вместе с проверкой версии API. Compose v2.32 при движке ниже 25.0 отвечал ошибкой can't set healthcheck.start_interval as feature require Docker Engine v25 or later.
Актуальная ветка Compose на сентябрь 2026 добавила ещё одну защиту, и она прямо связана с кодом getInterval выше. Если в файле есть start_interval, но нет start_period, docker compose up не запустится и выведет:
healthcheck.start_interval requires healthcheck.start_period to be setБез start_period поле бесполезно, и Compose теперь говорит об этом сразу, а не оставляет вас ждать первой проверки час.
Как посмотреть состояние healthcheck
Самый быстрый взгляд даёт docker ps или docker compose ps: в колонке STATUS рядом с Up ... в скобках стоит health: starting, healthy или unhealthy. Для деталей нужен docker inspect:
docker inspect --format '{{json .State.Health}}' <container> | jq
docker inspect --format '{{.State.StartedAt}}' <container>В объекте Health три поля.
Status: одно изstarting,healthy,unhealthy.FailingStreak: сколько проверок подряд завершились неудачей. Сбрасывается в 0 после любого успеха. Когда значение достигаетretries, статус становитсяunhealthy. Внутриstart_periodнеудачи счётчик не двигают.Log: массив последних результатов. Движок хранит не больше пяти записей (константаmaxLogEntries = 5вdaemon/health.go), старые вытесняются. У каждой записи естьStart,End,ExitCodeиOutput.
Чтобы убедиться, когда именно запустилась первая проверка, сравните StartedAt контейнера с полем Start самой ранней записи в Log. Разница и есть реальная задержка первой проверки на вашей машине. Если start_period не задан, она будет близка к interval. Если задан, она будет близка к start_interval. Компактный вывод только времени и кодов возврата:
docker inspect --format '{{range .State.Health.Log}}{{.Start}} exit={{.ExitCode}}{{"\n"}}{{end}}' <container>Поле Output содержит stdout и stderr команды проверки. Именно туда попадают подсказки о том, что пошло не так: executable file not found in $PATH, если утилиты нет в образе, или Health check exceeded timeout (30s), если команда не уложилась в timeout. Во втором случае ExitCode равен -1.
Смену статусов удобно смотреть в реальном времени:
docker events --filter event=health_statusКаждый переход в healthy или unhealthy появится отдельной строкой с временной меткой. Так можно, ничего не считая, увидеть, сколько прошло от старта контейнера до первого healthy.
Пример 1: медленный старт, PostgreSQL
Базе на пустом томе нужно время на инициализацию, а приложению нельзя стартовать раньше неё. Такой сервис хочется проверять редко в штатном режиме, но часто на старте.
services:
db:
image: postgres:16
environment:
POSTGRES_PASSWORD: change-me
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -h 127.0.0.1 -U postgres"]
interval: 30s
timeout: 5s
retries: 5
start_period: 90s
start_interval: 3s
adminer:
image: adminer
ports:
- "8081:8080"
depends_on:
db:
condition: service_healthy
volumes:
pgdata:Что здесь происходит. Первые 90 секунд движок опрашивает pg_isready каждые 3 секунды, и неудачи не считаются. Как только база ответит, контейнер станет healthy, и Compose запустит adminer. После этого проверка идёт раз в 30 секунд, и пять неудач подряд переведут db в unhealthy. Если 90 секунд прошли без успеха, проверки продолжатся раз в 30 секунд, но уже со счётчиком.
-h 127.0.0.1 в команде не случаен. pg_isready без адреса ходит через unix-сокет, а приложение подключается по TCP. Проверка должна отвечать «да» ровно тогда, когда сможет подключиться ваше приложение, иначе service_healthy даст ложный старт. Помните правило из справочника: один успех внутри start_period заканчивает льготный период.
Без Compose то же самое задаётся флагами docker run:
docker run -d --name db \
-e POSTGRES_PASSWORD=change-me \
--health-cmd 'pg_isready -h 127.0.0.1 -U postgres' \
--health-interval 30s --health-timeout 5s --health-retries 5 \
--health-start-period 90s --health-start-interval 3s \
postgres:16Тот же подход годится для Java-приложений на Spring Boot или для Keycloak, где до готовности HTTP-порта проходят десятки секунд. Меняется только test, обычно на запрос к /actuator/health или аналогичному пути.
Пример 2: дешёвая проверка каждые 10 секунд
Для веб-сервера или лёгкого API проверка стоит миллисекунды, и её можно запускать часто. Здесь start_period тоже полезен, но короткий.
services:
web:
image: nginx:stable-alpine
ports:
- "8080:80"
healthcheck:
test: ["CMD", "wget", "-q", "-T", "3", "--spider", "http://127.0.0.1/"]
interval: 10s
timeout: 5s
retries: 3
start_period: 10s
start_interval: 2s
smoke:
image: busybox
command: ["wget", "-qO-", "http://web/"]
depends_on:
web:
condition: service_healthyКоманда проверки выполняется внутри контейнера web, поэтому утилита должна быть в образе. В Alpine-варианте nginx есть wget из BusyBox, и --spider делает запрос без скачивания тела. Флаг -T 3 ограничивает ожидание ответа тремя секундами, чтобы проверка сама не упиралась в timeout. Для образов на Debian с curl то же самое пишется как ["CMD", "curl", "-fsS", "http://127.0.0.1/"], а для Redis это ["CMD", "redis-cli", "ping"].
При interval: 10s и retries: 3 сервис будет объявлен unhealthy примерно через 30 секунд после того, как перестанет отвечать, плюс время самих проверок. С start_period: 10s и start_interval: 2s первая проверка запустится через 2 секунды, и одноразовый контейнер smoke выполнит свой запрос сразу после того, как nginx начнёт отвечать.
Стоит ли ставить interval: 5s? Каждая проверка запускает процесс внутри контейнера. Для wget это дёшево, для pg_isready тоже, но проверка, которая делает запрос к базе или прогоняет тяжёлый запрос к API, при частом запуске сама станет источником нагрузки. Правило простое: чем дороже команда, тем длиннее interval, а быстрый старт обеспечивайте через start_interval.
Что делает depends_on: condition: service_healthy
Compose при docker compose up не запускает зависимый сервис, пока контейнер зависимости не перейдёт в healthy. Сам Compose проверок не выполняет: он опрашивает состояние у движка (в исходниках v2.29 это тикер раз в 500 миллисекунд) и ждёт. Из этого следуют два практических вывода.
С примером 1 Compose ждёт первого успешного pg_isready. Без start_period и с interval: 30s он ждал бы минимум 30 секунд, а с interval: 1h час, хотя база могла быть готова гораздо раньше. С примером 2 ожидание ограничено первой успешной проверкой через 2 секунды.
Если зависимость станет unhealthy, Compose прервёт запуск. В v2.29 сообщение собиралось из строк dependency failed to start и container <имя> is unhealthy. Проверить, что именно не так, можно тем же docker inspect --format '{{json .State.Health}}': поле Output последней записи в Log покажет причину.
Если depends_on записан коротким синтаксисом (просто список имён), условие по умолчанию service_started, и healthcheck на порядок запуска не влияет. Оба синтаксиса с примерами собраны в шпаргалке по командам и полям Docker Compose.
Частые ошибки со временем проверок
start_interval без start_period. Разобрано выше: движок его не использует, а свежий Compose откажется запускать проект. Всегда задавайте оба поля.
Проверка дольше timeout. По умолчанию timeout 30 секунд. Если команда ждёт сеть или базу дольше, в Log появится Health check exceeded timeout с ExitCode -1, и это считается неудачей. Ограничивайте время внутри самой команды (wget -T или curl --max-time) значением меньше timeout.
Утилиты нет в образе. Минимальные образы (distroless или scratch) не содержат ни curl, ни wget. В Output будет executable file not found in $PATH, статус уйдёт в unhealthy. Решение: использовать то, что в образе есть, или встроить в приложение подкоманду вроде app healthcheck, которая делает запрос сама.
Ожидание, что interval отсчитывается от начала проверки. Пауза отсчитывается от завершения предыдущей проверки. Проверка на 4 секунды при interval: 10s даёт реальный период около 14 секунд. При расчёте времени до unhealthy учитывайте это.
Healthcheck из образа перекрывает ваш. Если в Dockerfile образа есть инструкция HEALTHCHECK, а в Compose секции healthcheck нет, действуют параметры из образа, включая его interval. Секция в Compose переопределяет их целиком. Отключить унаследованную проверку можно через test: ["NONE"] или disable: true.
Наконец, healthcheck отвечает на вопрос «жив ли процесс внутри контейнера» и ничего не знает о том, доступен ли сервис снаружи. Для внешнего мониторинга с уведомлениями поднимите Uptime Kuma со страницей статуса: он проверит порт с точки зрения пользователя и не зависит от того, что Docker знает о контейнере.
FAQ
Почему контейнер с interval: 1h целый час висит в статусе starting?
Потому что первая проверка запускается через interval после старта контейнера, и до неё у Docker нет ни одного результата. Справочник Dockerfile: «The health check will first run interval seconds after the container is started». Чтобы первая проверка пришла раньше, добавьте start_period и start_interval: внутри льготного периода планировщик ждёт start_interval вместо interval.
Работает ли start_interval без start_period?
Нет. В daemon/health.go пауза выбирается функцией, которая возвращает обычный interval, как только время с момента старта больше или равно start_period. При start_period равном нулю это условие истинно всегда. Свежие версии Compose отказываются запускать такой файл с ошибкой healthcheck.start_interval requires healthcheck.start_period to be set.
Как узнать, когда реально выполнилась первая проверка?
Выполните docker inspect --format '{{json .State.Health}}' <container> и docker inspect --format '{{.State.StartedAt}}' <container>. Разница между StartedAt и полем Start самой ранней записи в Log и есть задержка первой проверки. Учтите, что Log хранит только последние пять результатов, поэтому смотреть нужно вскоре после старта.
Что произойдёт с depends_on: condition: service_healthy, если проверка так и не пройдёт?
Compose будет ждать, пока зависимость находится в starting. Когда FailingStreak достигнет retries и контейнер станет unhealthy, docker compose up прервётся с ошибкой о нездоровой зависимости. Причина неудачи лежит в поле Output последней записи Log у контейнера зависимости.
Какая версия Docker нужна для start_interval?
Docker Engine 25.0 и новее (API 1.44), проверить можно командой docker version --format '{{.Server.APIVersion}}'. Со стороны Compose поле реально передаётся в движок начиная с v2.24.0; версии с v2.20.2 по v2.23 принимали его в файле, но в движок не передавали.