Короткий ответ
Цикл 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 после deploy | rolling, норма | логи одной ревизии |
| 137 и OOMKilled | «segfault» | лимиты |
Возможные причины
- Приложение падает на старте: нет файла конфига, неверный DSN, миграция.
restart: alwaysна one-shot (migrate,certbot) — контейнер обязан умереть с 0, Docker поднимает снова.- OOM:
mem_limitслишком мал или хост без лимита съел RAM. - Volume
app_dataне пишется: Permission denied → процесс exit 1. См. права volume. - Зависимость не готова, а
depends_onбезcondition: service_healthy— приложение стучится в пустой порт и падает. - Entrypoint ожидает TTY или интерактив (
-itв unit/cron). - Редко: 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 | tailOOMKilled: 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/healthStatus: 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.- Алерт на
RestartCountdelta за 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. Документируйте.