Перейти к содержимому
RootPoligon

Запуск и готовность

Ситуация

После docker compose up -d оба контейнера Up, а первые секунды сайт отвечает 502 Bad Gateway. Потом всё работает. А ночью приложение зависло: контейнер Up, процесс жив, но на запросы не отвечает. Docker ничего не сделал. Почему?

Главная мысль

Running (Up) значит одно: главный процесс существует. Ready — приложение действительно отвечает. Docker узнаёт о готовности только из проверки здоровья (healthcheck), которую вы ему дадите.

Дальше три разных механизма, и их легко спутать:

  • healthcheck — регулярно выполняет команду внутри контейнера и ставит статус starting → healthy или unhealthy. Сам ничего не чинит.
  • depends_on: condition: service_healthy — Compose запускает зависимый сервис после того, как зависимость стала healthy. Работает только при запуске.
  • restart policy (restart: unless-stopped) — перезапускает контейнер, когда главный процесс завершился. На unhealthy в обычном Docker Engine не реагирует.

Новые слова

  • Healthcheck — команда проверки и её расписание: interval (как часто), timeout (сколько ждать ответа), retries (сколько неудач подряд — уже unhealthy), start_period (время на запуск, неудачи в нём не считаются).
  • Политика перезапуска — no (по умолчанию), on-failure (только при ненулевом коде), always, unless-stopped (всегда, кроме случая, когда вы остановили контейнер сами).

Итоговый compose.yaml

Добавляем в файл главы 6 проверку готовности, условие запуска web и политику перезапуска (project/compose.yaml):

# «Заметки»: приложение и nginx перед ним
name: rpd

services:
  app:
    build: .
    image: rpd-notes:compose
    environment:
      GREETING: ${GREETING:-Заметки}
      STARTUP_DELAY: ${STARTUP_DELAY:-5}
    volumes:
      - notes-data:/data
    healthcheck:
      # Готово, если /health отвечает 200. Ответ 503 или ошибка соединения — код 1
      test: ["CMD", "python3", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/health', timeout=2)"]
      interval: 5s
      timeout: 3s
      retries: 3
      start_period: 15s
    restart: unless-stopped

  web:
    image: nginx:1.30-alpine
    ports:
      - "127.0.0.1:${WEB_PORT:-8080}:80"
    volumes:
      - ./nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
    depends_on:
      app:
        condition: service_healthy
    restart: unless-stopped

volumes:
  notes-data:
    name: rpd-notes-data

networks:
  default:
    name: rpd-net
  • Проверка запускает Python внутри контейнера: curl в образе нет. urlopen бросает исключение на ответ 503 и на ошибку соединения — Python завершается с кодом 1, Docker считает это неудачей.
  • STARTUP_DELAY: 5 — приложение «прогревается» 5 секунд: процесс уже работает, а /health отвечает 503. В настоящих приложениях так выглядит подключение к базе, загрузка кеша, миграции.
  • web запустится только после healthy у app.

Проверка выполняется каждые 5 секунд и пишет строку GET /health 200 в журнал приложения — это нормально.

Разбор

Папка ~/docker-course/rpd-notes, файл compose.yaml заменён на итоговый. Если проект поднят — docker compose down.

Запуск ждёт готовности

time docker compose up -d
 Container rpd-app-1 Started
 Container rpd-app-1 Waiting
 Container rpd-app-1 Healthy
 Container rpd-web-1 Starting
 Container rpd-web-1 Started

real	0m6.181s

Compose дождался Healthy и только потом запустил web — около 5 секунд прогрева плюс ожидание очередной проверки.

Запущен, но не готов

Перезапустим приложение и сразу посмотрим:

docker compose restart app
sleep 1
docker compose ps app --format '{{.Name}} {{.Status}}'
curl -sS -i http://127.0.0.1:8080/health
rpd-app-1 Up 1 second (health: starting)
HTTP/1.1 503 Service Unavailable
…
ещё прогревается

Контейнер Up, но ещё не готов. Обратите внимание: nginx продолжает отправлять ему запросы. Docker и Compose не убирают неготовый контейнер из обращения — статус здоровья только информирует. В самый момент перезапуска (пока процесс не начал слушать порт) nginx ответит 502 Bad Gateway. Через несколько секунд:

docker compose ps app --format '{{.Name}} {{.Status}}'
rpd-app-1 Up 13 seconds (healthy)

Последние проверки и их вывод — docker inspect -f '{{json .State.Health}}' rpd-app-1.

unhealthy — и ничего не происходит

Приложение умеет сообщать «на обслуживании»: если в папке данных есть файл maintenance, /health отвечает 503. Это учебный способ сымитировать зависание, не убивая процесс:

docker compose exec app touch /data/maintenance
sleep 18
docker compose ps --format 'table {{.Name}}\t{{.Status}}'
docker inspect -f '{{.State.Status}} {{.State.Health.Status}} restarts={{.RestartCount}}' rpd-app-1
NAME        STATUS
rpd-app-1   Up 40 seconds (unhealthy)
rpd-web-1   Up About a minute
running unhealthy restarts=0

Три неудачи подряд — unhealthy. Перезапусков — 0, и через минуту их тоже будет 0: restart: unless-stopped на unhealthy не реагирует. Перезапуск по нездоровью делают оркестраторы (Swarm, Kubernetes) или отдельный наблюдатель — в чистом Docker Engine этого нет. Вернём как было:

docker compose exec app rm /data/maintenance

Через одну-две проверки статус снова healthy.

Процесс завершился — перезапуск

Пошлём главному процессу SIGTERM изнутри — приложение штатно завершится с кодом 0:

docker compose exec app python3 -c "import os, signal; os.kill(1, signal.SIGTERM)"
sleep 3
docker inspect -f '{{.State.Status}} restarts={{.RestartCount}}' rpd-app-1
docker compose logs app | tail -3
running restarts=1
app-1  | notes: получен сигнал остановки, завершаю работу
app-1  | notes: остановлено
app-1  | notes: слушаю 0.0.0.0:8000, данные в /data

Процесс закончился — Docker запустил контейнер заново. А остановка вручную не перезапускается:

docker compose stop app
docker compose ps -a app --format '{{.Name}} {{.Status}}'
rpd-app-1 Exited (0) 3 seconds ago

docker compose start app — вернуть.

Перезапуск не чинит ошибку настройки

Одноразовый пример вне Compose — контейнер с неверным портом и политикой перезапуска:

docker run -d --name rpd-loop --restart unless-stopped -e PORT=80x1 rpd-notes:1
sleep 8
docker ps -a --filter name=rpd-loop --format '{{.Names}} {{.Status}}'
docker logs rpd-loop | head -2
docker rm -f rpd-loop
rpd-loop Restarting (2) 2 seconds ago
notes: неверный PORT: '80x1' (нужно число от 1 до 65535)
notes: неверный PORT: '80x1' (нужно число от 1 до 65535)

Контейнер бесконечно падает и перезапускается (с растущей паузой) — Restarting (2). Политика перезапуска спасает от случайного падения, но не от ошибки, которая повторяется при каждом запуске. Причину ищут в журнале.

Практика: долгий прогрев

Исходное состояние — проект поднят с итоговым compose.yaml.

  1. Добавьте в .env строку STARTUP_DELAY=20 и предскажите, сколько займёт docker compose up -d и в каком состоянии будет web в первые 15 секунд. Выполните time docker compose up -d, сравните.
  2. Подумайте: что будет при STARTUP_DELAY=60, если start_period — 15 секунд, проверка каждые 5 секунд, retries: 3? Проверьте: STARTUP_DELAY=60 docker compose up -d (переменная окружения важнее .env).
  3. Уберите строку из .env и верните проект в рабочее состояние.

Признаки успеха: для пункта 1 — время около 20 секунд и web запущен после Healthy; для пункта 2 — названо сообщение Compose и состояния обоих сервисов; в конце docker compose ps — app (healthy), web Up.

Подсказка 1

web ждёт healthy у app. Когда приложение начнёт отвечать 200 на /health?

Подсказка 2

Неудачные проверки в первые 15 секунд не считаются. После этого три неудачи подряд — unhealthy. Успеет ли приложение прогреться за это время?

Подсказка 3

Вернуть: удалите STARTUP_DELAY=… из .env (sed -i '/^STARTUP_DELAY=/d' .env) и выполните docker compose up -d.

Решение — в answers/07-health-restart.md.

Если результат другой

  • dependency failed to start: container rpd-app-1 is unhealthy. Приложение не стало готовым за отведённое время. Смотрите docker compose logs app и docker inspect -f '{{json .State.Health}}' rpd-app-1: что отвечает проверка. Увеличивать start_period — только если приложение действительно долго стартует.
  • Статус всегда starting или unhealthy, а сайт работает. Ошибка в самой команде проверки (опечатка в адресе, нет нужной программы в образе). Выполните её вручную: docker compose exec app python3 -c "…".
  • Контейнер в Restarting. Главный процесс падает сразу после старта. Журнал покажет причину; перезапуски её не исправят.

Проверка понимания

  1. Контейнер Up (unhealthy) уже десять минут, restart: unless-stopped. Перезапустит ли его Docker?
  2. Почему depends_on без condition: service_healthy не спасает от 502 при первом запуске?

Ответы — в answers/07-health-restart.md.

Дальше: 8. Диагностика: найти и починить.

Разбор и проверка понимания

Лист ученика: 07-health-restart.md.

Практика

1. Прогрев 20 секунд.

echo 'STARTUP_DELAY=20' >> .env
time docker compose up -d
 Container rpd-app-1 Waiting
 Container rpd-app-1 Healthy
 Container rpd-web-1 Started
real	0m21.121s

Около 20 секунд. Всё это время web в состоянии Created — Compose его не запускает, пока app не healthy. Неудачные проверки в первые 15 секунд (start_period) не считаются, а следующие две ещё не дотягивают до трёх подряд — к 20-й секунде приложение готово.

2. Прогрев 60 секунд.

STARTUP_DELAY=60 docker compose up -d
docker compose ps -a --format 'table {{.Name}}\t{{.Status}}'
dependency failed to start: container rpd-app-1 is unhealthy
NAME        STATUS
rpd-app-1   Up 26 seconds (unhealthy)
rpd-web-1   Created

15 секунд льготного периода плюс три неудачи по 5 секунд — около 25–30 секунд до unhealthy. app работает (и через 60 секунд станет готовым), но web так и не запущен: условие проверяется только при запуске.

3. Вернуть.

sed -i '/^STARTUP_DELAY=/d' .env
docker compose up -d
docker compose ps

app пересоздаётся с прогревом 5 секунд, после healthy запускается web.

Воспроизведено: Docker Engine 29.8.2, Compose 5.5.1, образы-заменители.

Проверка понимания

  1. Нет. restart: unless-stopped реагирует только на завершение главного процесса. Статус unhealthy в обычном Docker Engine — информация для людей и оркестраторов; перезапускать придётся самому (docker compose restart app) или отдельным наблюдателем.
  2. Без условия Compose ждёт только запуска контейнера app, то есть появления процесса. Приложение в этот момент может ещё не слушать порт или прогреваться — nginx уже принимает запросы и отвечает 502.

Практика без терминала

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

Прочитали и разобрались? Отметьте урок — прогресс сохранится в этом браузере.