Короткий ответ

Цикл Restarting — это не «Docker сломался». Это политика always/unless-stopped/on-failure поднимает процесс, который каждый раз выходит. На host.example снимите ExitCode, OOMKilled, Error и RestartCount через docker inspect, прочитайте последние логи без -f на час, сопоставьте с restart: в compose проекта app. Код 137 чаще OOM или SIGKILL, 1 — приложение, 127 — нет бинаря, 143 — SIGTERM. Не ставьте restart: "no" как лечение продакшена: вы просто перестанете видеть цикл, сервис останется мёртвым.

Одноразовый выход без restart — соседняя тема: сразу завершается.

Симптомы и как отличить

  • docker compose -f /opt/app/compose.yaml ps показывает Restarting (1) 2 seconds ago;
  • docker events --filter container=app-web-1 сыплет die/start;
  • в мониторинге флапает порт, healthcheck красный;
  • dmesg содержит Out of memory: Killed process.
КартинаНе путать сСмотреть
Restarting каждые 1–2 сdaemon падаетdaemon
Up, но unhealthyпроцесс жив, проверка врётhealthcheck
Restart после deployrolling, нормалоги одной ревизии
137 и OOMKilled«segfault»лимиты

Возможные причины

  1. Приложение падает на старте: нет файла конфига, неверный DSN, миграция.
  2. restart: always на one-shot (migrate, certbot) — контейнер обязан умереть с 0, Docker поднимает снова.
  3. OOM: mem_limit слишком мал или хост без лимита съел RAM.
  4. Volume app_data не пишется: Permission denied → процесс exit 1. См. права volume.
  5. Зависимость не готова, а depends_on без condition: service_healthy — приложение стучится в пустой порт и падает.
  6. Entrypoint ожидает TTY или интерактив (-it в unit/cron).
  7. Редко: seccomp/AppArmor убивает syscall, в логе Operation not permitted.

Диагностика

Каталог стека /opt/app, проект app, сервис web.

1. Код выхода и политика

cd /opt/app
docker compose ps -a
docker inspect app-web-1 --format '{{.State.Status}} exit={{.State.ExitCode}} oom={{.State.OOMKilled}} err={{.State.Error}} restarts={{.RestartCount}} finished={{.State.FinishedAt}}'
docker inspect app-web-1 --format '{{.HostConfig.RestartPolicy.Name}} max={{.HostConfig.RestartPolicy.MaximumRetryCount}}'

RestartCount в сотнях за час — crash loop, не «долгий старт».

2. Логи одного цикла

Не docker logs -f на час. Снимите хвост и предыдущий контейнер, если compose уже пересоздал имя.

docker logs --tail 200 app-web-1
docker compose logs --tail 200 web

Ищите Permission denied, connection refused к db:5432, exec: not found, killed.

3. OOM хоста vs лимит контейнера

docker inspect app-web-1 --format 'mem={{.HostConfig.Memory}} swap={{.HostConfig.MemorySwap}} pids={{.HostConfig.PidsLimit}}'
dmesg -T | grep -i -E 'oom|killed process' | tail
grep -E 'web|app' /var/log/kern.log | tail

OOMKilled: true при Memory: 0 — убил хост. При Memory: 134217728 — лимит 128 MiB, см. лимиты.

4. Restart в YAML, не в голове

docker compose config | sed -n '/web:/,/^  [a-z]/p'

Сверьте restart: и deploy.restart_policy (последнее в чистом Compose v2 на одном узле не заменяет restart:).

Решение

Сценарий A. Exit 1, в логе ошибка приложения

Чините конфиг, секрет, строку к БД. Для проверки одного старта временно:

docker update --restart=no app-web-1
docker start -a app-web-1

-a покажет stdout в текущем TTY. Верните политику после фикса:

docker update --restart=unless-stopped app-web-1

В compose оставьте unless-stopped, не no.

Сценарий B. One-shot с always

Сервисы migrate/seed должны быть restart: "no" и запускаться через docker compose run --rm migrate, либо профилем, который не входит в up -d. Иначе цикл вечен при коде 0 — wait, при 0 on-failure не рестартит, а always рестартит даже успешный exit. Это классика certbot-контейнера.

Сценарий C. 137 / OOMKilled

Не снимайте лимит «чтобы жило». Поднимите осознанно или найдите утечку. Порядок: логи приложения → heap → лимит. Команды смены лимита — в ограничении ресурсов.

docker stats --no-stream app-web-1

Сценарий D. depends_on без готовности

В Compose v2:

depends_on:
  db:
    condition: service_healthy

И рабочий healthcheck у db, не exit 0 из cron. Пока БД unhealthy, web не должен входить в always-crash. Не заменяйте это restart: always и sleep 30 в entrypoint без таймаута.

Сценарий E. Бинаря нет (127) или формат (139/exec format)

docker compose run --rm --entrypoint sh web -c 'ls -l /usr/local/bin; uname -m'

exec format error — образ не той архитектуры (arm64 на amd64 хосте). Не restart: always как маскировка.

Как проверить, что проблема устранена

sleep 20
docker inspect app-web-1 --format '{{.State.Status}} restarts={{.RestartCount}} exit={{.State.ExitCode}} oom={{.State.OOMKilled}}'
docker compose ps
curl -sS -o /dev/null -w '%{http_code}\n' --max-time 5 http://127.0.0.1:8080/health

Status: running, RestartCount не растёт на глазах, HTTP не 000. Подождите 5 минут: короткий «Up» обманывает.

Если не помогло

  • RestartCount растёт, логов нет: процесс пишет не в stdout; переведите на journal/json-file или PYTHONUNBUFFERED=1.
  • Падает только под нагрузкой: это OOM/ulimit, не YAML.
  • Падает после SIGTERM от healthcheck unhealthy + on-failure в оркестраторе; на чистом Compose healthcheck не убивает контейнер сам по себе — ищите внешний watchdog.
  • Error: "OCI runtime create failed" — это не restart приложения, а runtime; journal docker.
  • Секрет в env пустой после rotate — контейнер честно падает; не always без проверки docker compose config.

Профилактика

  • unless-stopped для долгих сервисов, no для migrate.
  • Алерт на RestartCount delta за 5 минут.
  • Healthcheck, который проверяет зависимость, плюс condition: service_healthy.
  • Лимиты памяти с запасом и мониторинг OOMKilled.
  • Не глотать исключение в entrypoint || true — получите «здоровый» цикл с кодом 0 при always.

FAQ

always или unless-stopped?

unless-stopped не поднимет контейнер после ручного stop (удобно в окне). always поднимет даже после reboot и после вашего stop, когда daemon стартует — чаще мешает. Для продакшена на systemd-хосте обычно unless-stopped.

Зачем MaximumRetryCount?

Только с on-failure. Снимает бесконечный цикл после N падений. Имеет смысл на one-shot, не на nginx.

Почему 137, но OOMKilled false?

SIGKILL с хоста (kill -9, docker kill) тоже 128+9=137. Смотрите audit/dmesg. Не всё 137 — память.

Можно ли restart: no на проде, чтобы «перестало дёргать диск»?

Нет. Вы отключите автоподъём после падения. Чините причину. Временно no только на время docker start -a в окне.

Compose restart vs systemd Restart=always на unit с docker compose up?

Два сторожа дерутся. Выберите одно: либо unit запускает compose и сам не рестартит контейнеры сверх политики compose, либо один контейнер в unit. Документируйте.