SSD Nodes Learn Hosting plans →
Руководства Matt ConnorАвтор: Matt Connor

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 принимали его в файле, но в движок не передавали.