4.6. Restart policies и exit codes
Цели
После этого материала вы сможете:
- выбрать restart policy под конкретный тип нагрузки и обосновать выбор;
- объяснить разницу между
alwaysиunless-stoppedпри перезапуске daemon; - интерпретировать любой exit code, включая
125,126,127,137,143; - отличить OOM от внешнего
SIGKILLпо даннымdocker inspect; - диагностировать restart loop и найти его причину;
- объяснить механизм экспоненциальной задержки между перезапусками;
- определить, когда restart policy маскирует проблему вместо её решения.
Предварительные знания
- 4.4. Сигналы и graceful shutdown — коды
137и143; - 4.5. PID 1 и init;
- 2.4. Cgroups — OOM killer.
Ключевые термины
| Термин | Объяснение |
|---|---|
restart policy | Правило автоматического перезапуска container при завершении |
exit code | Код возврата главного процесса, 0–255 |
restart loop | Цикл «запуск → падение → перезапуск», не приводящий к работоспособности |
backoff | Нарастающая задержка между попытками перезапуска |
OOM killer | Механизм ядра, завершающий процесс при исчерпании памяти в cgroup |
RestartCount | Счётчик выполненных автоматических перезапусков |
Теория
Четыре политики
| Политика | Перезапуск при ненулевом коде | При нулевом коде | После docker stop | После перезапуска daemon |
|---|---|---|---|---|
no (по умолчанию) | нет | нет | нет | нет |
on-failure[:N] | да, до N раз | нет | нет | да, если был запущен |
always | да | да | нет | да, всегда |
unless-stopped | да | да | нет | нет, если был остановлен вручную |
Две колонки требуют пояснения, потому что именно в них кроются практические различия.
Колонка «при нулевом коде». Политика on-failure реагирует только на ошибки. Container, завершившийся с кодом 0, считается успешно выполнившим задачу и не перезапускается. Политики always и unless-stopped перезапускают в любом случае.
Последняя колонка — единственное различие always и unless-stopped. Оно проявляется в одном сценарии:
1. Container работает с политикой always / unless-stopped
2. Администратор выполняет docker stop → container остановлен
3. Сервер перезагружается, daemon стартует
│
├─ always: container ЗАПУСКАЕТСЯ снова
└─ unless-stopped: container ОСТАЁТСЯ остановленным
Практический смысл: unless-stopped уважает явное решение человека. Если вы остановили сервис намеренно — например, для обслуживания, — он не поднимется сам после перезагрузки.
Именно поэтому unless-stopped обычно предпочтительнее always для сервисов.
Выбор политики
| Тип нагрузки | Политика | Обоснование |
|---|---|---|
| Web-сервис, API | unless-stopped | Должен работать постоянно, но уважать ручную остановку |
| Background worker | unless-stopped | То же |
| Database | unless-stopped | То же |
| Разовая миграция | no | Должна выполниться один раз; перезапуск опасен |
| Задача, которую можно повторить | on-failure:3 | Перезапуск при сбое, но не бесконечно |
| Отладочный container | no | Перезапуск мешает разбираться |
| CI-задача | no | Результат должен быть однозначным |
Отдельно про миграции: политика always для сервиса миграций — распространённая и опасная ошибка. Успешно выполнившаяся миграция завершается с кодом 0, always перезапускает её, она выполняется снова. При неидемпотентных миграциях это повреждает данные.
Backoff
Docker не перезапускает container немедленно и бесконечно часто. Задержка удваивается: 100 мс, 200 мс, 400 мс и так далее, с ограничением примерно в минуту.
Счётчик сбрасывается, если container проработал более 10 секунд. Отсюда следствие: приложение, падающее через 2 секунды после старта, попадает в нарастающий backoff; приложение, падающее через 30 секунд, перезапускается каждые 100 мс без задержки.
Наблюдать backoff можно через docker events.
Таблица exit codes
| Код | Значение | Типичная причина |
|---|---|---|
0 | Успех | Штатное завершение |
1 | Общая ошибка приложения | Необработанное исключение, sys.exit(1) |
2 | Неверное использование | Ошибка в аргументах командной строки |
125 | Ошибка самого Docker | Неверный флаг docker run, конфликт опций |
126 | Команда найдена, но не исполняема | Нет бита +x, неверный shebang |
127 | Команда не найдена | Опечатка, отсутствует в PATH, нет интерпретатора |
128+N | Завершён сигналом N | См. ниже |
137 | SIGKILL (128+9) | OOM, docker kill, истёк grace period |
143 | SIGTERM (128+15) | docker stop без обработчика в приложении |
139 | SIGSEGV (128+11) | Ошибка сегментации |
130 | SIGINT (128+2) | Ctrl+C |
Различать 125, 126 и 127 важно: они указывают на три разные проблемы, которые исправляются по-разному.
125— ошибка до запуска container. Виновата командаdocker, а не образ.126— файл найден, но не запустился. Обычно отсутствует бит исполнения или неверный shebang.127— файла нет. Опечатка в имени или отсутствует зависимость (например,bashв Alpine).
137 — код с двумя причинами
Самый частый код при диагностике и самый неоднозначный. 137 означает только «процесс получил SIGKILL». Кто его послал — вопрос отдельный:
| Отправитель | Как определить |
|---|---|
| OOM killer | .State.OOMKilled == true |
docker kill | В docker events есть событие kill |
Истёк grace period docker stop | Событию die предшествует kill с сигналом 15 |
| Внешний процесс на host | Ничего из перечисленного |
Поле .State.OOMKilled — главный различитель. Проверять его нужно до того, как строить гипотезы.
Когда restart policy маскирует проблему
Автоматический перезапуск полезен при временных сбоях: пропала сеть, база данных перезагружалась, кончилось место и освободилось.
Он вреден, когда скрывает постоянную проблему. Сервис, падающий каждые 40 секунд из-за утечки памяти и перезапускаемый политикой always, выглядит в docker ps как работающий. Пользователи получают периодические ошибки, а мониторинг по признаку «container запущен» ничего не показывает.
Индикатор — счётчик RestartCount. Значение больше нескольких единиц за время работы означает, что нужно разбираться, а не полагаться на перезапуск.
Внутренний механизм
Как daemon отслеживает завершение
Shim сообщает daemon код возврата процесса (урок 2.6). Daemon сверяет его с restart policy:
- Записывает
ExitCode,FinishedAt,OOMKilled. - Проверяет политику и код.
- Если перезапуск нужен — увеличивает
RestartCount, переводит container вrestarting, ждёт backoff. - Создаёт новую задачу.
Состояние restarting видно в docker ps как Restarting (N) M seconds ago, где N — код прошлого завершения.
Откуда берётся OOMKilled
При исчерпании памяти в cgroup ядро выбирает процесс и посылает ему SIGKILL. Событие фиксируется в файле memory.events cgroup в счётчике oom_kill.
Daemon читает этот счётчик и выставляет .State.OOMKilled. Именно поэтому поле надёжно различает OOM и внешний SIGKILL — источник данных разный.
Команды и примеры
Подготовка
mkdir -p /tmp/restart && cd /tmp/restart
Политика no
docker run -d --name p-no alpine sh -c 'sleep 2; exit 1' > /dev/null
sleep 5
docker ps -a --filter name=p-no --format 'table {{.Names}}\t{{.Status}}'
docker inspect p-no --format 'перезапусков: {{.RestartCount}}'
docker rm p-no > /dev/null
NAMES STATUS
p-no Exited (1) 3 seconds ago
перезапусков: 0
Container упал и остался лежать — политика по умолчанию.
Политика on-failure реагирует только на ошибки
echo "--- завершение с кодом 1 ---"
docker run -d --name of-fail --restart=on-failure:3 \
alpine sh -c 'sleep 1; exit 1' > /dev/null
sleep 12
docker inspect of-fail --format 'код: {{.State.ExitCode}} перезапусков: {{.RestartCount}} статус: {{.State.Status}}'
echo "--- завершение с кодом 0 ---"
docker run -d --name of-ok --restart=on-failure:3 \
alpine sh -c 'sleep 1; exit 0' > /dev/null
sleep 5
docker inspect of-ok --format 'код: {{.State.ExitCode}} перезапусков: {{.RestartCount}} статус: {{.State.Status}}'
docker rm -f of-fail of-ok > /dev/null
--- завершение с кодом 1 ---
код: 1 перезапусков: 3 статус: exited
--- завершение с кодом 0 ---
код: 0 перезапусков: 0 статус: exited
Первый перезапускался три раза и остановился — лимит достигнут. Второй не перезапускался вовсе: код 0 не считается сбоем.
Это делает on-failure правильным выбором для повторяемых задач и неправильным для сервисов, которые могут завершиться с кодом 0 по ошибке конфигурации.
always против unless-stopped
Различие проявляется только при перезапуске daemon. Воспроизведём.
docker run -d --name r-always --restart=always alpine sleep 3600 > /dev/null
docker run -d --name r-unless --restart=unless-stopped alpine sleep 3600 > /dev/null
sleep 2
echo "--- останавливаем оба вручную ---"
docker stop r-always r-unless > /dev/null
docker ps -a --filter name=r- --format 'table {{.Names}}\t{{.Status}}'
NAMES STATUS
r-unless Exited (137) 1 second ago
r-always Exited (137) 1 second ago
Оба остановлены. Теперь перезапустим daemon:
sudo systemctl restart docker
sleep 5
docker ps -a --filter name=r- --format 'table {{.Names}}\t{{.Status}}'
NAMES STATUS
r-unless Exited (137) 22 seconds ago
r-always Up 4 seconds
Вот оно, единственное различие: always поднялся сам, unless-stopped уважил ручную остановку.
docker rm -f r-always r-unless > /dev/null
Именно поэтому для сервисов на серверах обычно выбирают
unless-stopped. Сalwaysостановленный на обслуживание сервис поднимется после любой перезагрузки — иногда в самый неподходящий момент.
Наблюдение за restart loop
cat > crashing.py <<'PY'
import os
import sys
import time
n = int(os.environ.get("LIFETIME", "2"))
print(f"старт, проживу {n} с", flush=True)
time.sleep(n)
print("падаю с кодом 1", flush=True)
sys.exit(1)
PY
docker run -d --name looper --restart=always \
-e LIFETIME=2 \
-v "$PWD/crashing.py:/app.py:ro" \
python:3.13-slim python -u /app.py > /dev/null
echo "наблюдаем 25 секунд..."
timeout 25 docker events --filter 'container=looper' \
--format '{{.Time}} {{.Action}}' 2>/dev/null | head -20
наблюдаем 25 секунд...
1785408011 create
1785408011 start
1785408013 die
1785408013 start
1785408015 die
1785408015 start
1785408017 die
1785408018 start
1785408020 die
1785408021 start
1785408023 die
1785408024 start
Виден цикл start → die → start. Интервалы между die и следующим start растут: 0, 0, 1, 1, 1 — это backoff в действии.
Состояние:
docker ps --filter name=looper --format 'table {{.Names}}\t{{.Status}}'
docker inspect looper --format 'перезапусков: {{.RestartCount}} код: {{.State.ExitCode}}'
NAMES STATUS
looper Restarting (1) 2 seconds ago
перезапусков: 8 код: 1
Restarting (1) — в скобках код прошлого завершения. Счётчик перезапусков растёт.
Как отличить restart loop от нормальной работы: в docker ps статус Restarting вместо Up, а RestartCount увеличивается при повторных проверках.
sleep 5
docker inspect looper --format 'перезапусков спустя 5 с: {{.RestartCount}}'
docker rm -f looper > /dev/null
перезапусков спустя 5 с: 10
Backoff и сброс счётчика
Приложение, живущее дольше 10 секунд, сбрасывает backoff:
docker run -d --name long-lived --restart=always \
-e LIFETIME=15 \
-v "$PWD/crashing.py:/app.py:ro" \
python:3.13-slim python -u /app.py > /dev/null
echo "наблюдаем 50 секунд за container, живущим по 15 с..."
timeout 50 docker events --filter 'container=long-lived' \
--format '{{.Time}} {{.Action}}' 2>/dev/null | grep -E 'die|start'
docker rm -f long-lived > /dev/null
1785408101 start
1785408116 die
1785408116 start
1785408131 die
1785408131 start
1785408146 die
Задержка между die и start остаётся нулевой — приложение успевало проработать больше 10 секунд, и backoff сбрасывался каждый раз.
Практический вывод: отсутствие нарастающей задержки не означает отсутствия проблемы. Сервис, падающий раз в 15 секунд, перезапускается мгновенно и в docker ps часто выглядит как Up.
Коды 125, 126, 127
echo "=== 125: ошибка Docker ==="
docker run --rm --nonexistent-flag alpine echo test 2>&1 | tail -1
echo "код: $?"
echo
echo "=== 127: команда не найдена ==="
docker run --rm alpine no-such-command 2>&1 | tail -1
echo "код: $?"
echo
echo "=== 126: найдена, но не исполняема ==="
cat > notexec.sh <<'SH'
#!/bin/sh
echo "не должно выполниться"
SH
chmod 644 notexec.sh
docker run --rm -v "$PWD/notexec.sh:/s.sh:ro" alpine /s.sh 2>&1 | tail -1
echo "код: $?"
=== 125: ошибка Docker ===
unknown flag: --nonexistent-flag
код: 125
=== 127: команда не найдена ===
docker: Error response from daemon: failed to create task for container: failed to create shim task: OCI runtime create failed: ... exec: "no-such-command": executable file not found in $PATH
код: 127
=== 126: найдена, но не исполняема ===
docker: Error response from daemon: failed to create task for container: ... permission denied
код: 126
Различие важно при диагностике:
125— исправляйте командуdocker;126— исправляйте права:chmod +x;127— исправляйте путь или доустановите зависимость.
Классический случай 127 в Alpine:
docker run --rm alpine bash -c 'echo test' 2>&1 | tail -1
echo "код: $?"
... exec: "bash": executable file not found in $PATH
код: 127
В Alpine нет bash — только sh из BusyBox. Ошибка встречается постоянно при копировании команд из примеров для Debian.
Различение OOM и docker kill
cat > eater.py <<'PY'
data = []
try:
while True:
data.append(bytearray(1024 * 1024)) # по 1 MB
except MemoryError:
print("MemoryError внутри Python", flush=True)
PY
echo "=== случай 1: OOM ==="
docker run -d --name oom-case --memory=48m --memory-swap=48m \
-v "$PWD/eater.py:/app.py:ro" \
python:3.13-slim python -u /app.py > /dev/null
sleep 6
docker inspect oom-case --format \
'код: {{.State.ExitCode}} OOMKilled: {{.State.OOMKilled}}'
echo "=== случай 2: docker kill ==="
docker run -d --name kill-case alpine sleep 300 > /dev/null
sleep 1
docker kill kill-case > /dev/null
docker inspect kill-case --format \
'код: {{.State.ExitCode}} OOMKilled: {{.State.OOMKilled}}'
docker rm -f oom-case kill-case > /dev/null
=== случай 1: OOM ===
код: 137 OOMKilled: true
=== случай 2: docker kill ===
код: 137 OOMKilled: false
Одинаковый код 137, разные причины. Поле OOMKilled — единственный надёжный различитель.
Подтверждение через события:
docker run -d --name oom-ev --memory=48m --memory-swap=48m \
-v "$PWD/eater.py:/app.py:ro" \
python:3.13-slim python -u /app.py > /dev/null &
timeout 10 docker events --filter 'container=oom-ev' \
--format '{{.Action}}' 2>/dev/null | head -5
docker rm -f oom-ev > /dev/null 2>&1 || true
create
start
oom
die
Событие oom присутствует явно. Отслеживание docker events --filter event=oom — практичный способ мониторинга нехватки памяти.
Опасная комбинация: always и разовая задача
cat > migration.sh <<'SH'
#!/bin/sh
echo "выполняю миграцию $(date +%T)"
sleep 1
echo "миграция завершена"
exit 0
SH
chmod +x migration.sh
docker run -d --name bad-migration --restart=always \
-v "$PWD/migration.sh:/m.sh:ro" \
alpine /m.sh > /dev/null
sleep 12
echo "миграция выполнилась раз:"
docker logs bad-migration 2>&1 | grep -c 'выполняю миграцию'
docker inspect bad-migration --format 'перезапусков: {{.RestartCount}}'
docker rm -f bad-migration > /dev/null
миграция выполнилась раз:
5
перезапусков: 4
Миграция отработала успешно и была перезапущена четыре раза. Для неидемпотентной миграции — «добавить колонку», «вставить начальные данные» — это ошибка или повреждение данных.
Правильно: --restart=no (по умолчанию) либо on-failure с ограничением, если повтор безопасен.
В Compose для таких задач используется зависимость по успешному завершению — раздел 09.
Изменение политики у работающего container
docker run -d --name upd alpine sleep 300 > /dev/null
docker inspect upd --format 'политика: {{.HostConfig.RestartPolicy.Name}}'
docker update --restart=unless-stopped upd
docker inspect upd --format 'политика: {{.HostConfig.RestartPolicy.Name}}'
docker update --restart=no upd
docker inspect upd --format 'политика: {{.HostConfig.RestartPolicy.Name}}'
docker rm -f upd > /dev/null
политика: no
политика: unless-stopped
политика: no
docker update --restart=no — способ разорвать restart loop, не удаляя container. Container остановится после следующего падения, и вы сможете спокойно изучить его логи и состояние.
Уборка
cd /tmp && rm -rf /tmp/restart
Практическое упражнение
Задание. Напишите скрипт diagnose-exit.sh <container>, который по завершившемуся или перезапускающемуся container выдаёт диагноз: что произошло и что делать.
Скрипт должен:
- Определить exit code и расшифровать его.
- Для кода
137различить OOM,docker killи истёкший grace period. - Определить наличие restart loop по
RestartCountи статусу. - Показать последние события из
docker events. - Показать последние строки логов.
- Выдать конкретную рекомендацию для каждого случая.
Дополнительно: напишите функцию, которая по коду возврата определяет, был ли процесс завершён сигналом, и возвращает имя сигнала.
Подсказки
Подсказка 1
Код больше 128 обычно означает завершение сигналом: номер сигнала равен code - 128.
Подсказка 2
Исторические события доступны через docker events --since:
docker events --since 10m --until 0s --filter "container=<name>" --format '{{.Action}}'
Подсказка 3
Отличить истёкший grace period от docker kill: в первом случае в событиях есть kill с сигналом 15 перед die, во втором — с сигналом 9.
Решение
Сначала выполните задание самостоятельно.
Показать решение
#!/usr/bin/env bash
# diagnose-exit.sh — диагноз по завершившемуся container.
set -uo pipefail
C="${1:?Использование: $0 <container>}"
docker inspect "$C" > /dev/null 2>&1 || { echo "Container не найден: $C" >&2; exit 1; }
f() { docker inspect "$C" --format "$1" 2>/dev/null; }
signal_name() {
case "$1" in
1) echo SIGHUP ;; 2) echo SIGINT ;; 3) echo SIGQUIT ;;
6) echo SIGABRT ;; 9) echo SIGKILL ;; 11) echo SIGSEGV ;;
13) echo SIGPIPE ;; 15) echo SIGTERM ;; *) echo "сигнал $1" ;;
esac
}
STATUS="$(f '{{.State.Status}}')"
CODE="$(f '{{.State.ExitCode}}')"
OOM="$(f '{{.State.OOMKilled}}')"
RC="$(f '{{.RestartCount}}')"
POLICY="$(f '{{.HostConfig.RestartPolicy.Name}}')"
MEMLIMIT="$(f '{{.HostConfig.Memory}}')"
echo "═══ Состояние ═══"
printf ' %-16s %s\n' "Статус:" "$STATUS"
printf ' %-16s %s\n' "Exit code:" "$CODE"
printf ' %-16s %s\n' "Политика:" "$POLICY"
printf ' %-16s %s\n' "Перезапусков:" "$RC"
echo
echo "═══ Расшифровка кода $CODE ═══"
case "$CODE" in
0)
echo " Штатное завершение."
[ "$POLICY" = "always" ] && {
echo " [!] Политика 'always' перезапустит даже успешно завершившуюся задачу."
echo " Для разовых задач используйте 'no' или 'on-failure'."
}
;;
1) echo " Общая ошибка приложения. Смотрите логи и трассировку." ;;
2) echo " Неверное использование команды: проверьте аргументы." ;;
125) echo " Ошибка САМОГО Docker: неверный флаг или конфликт опций."
echo " Образ ни при чём — исправляйте команду 'docker run'." ;;
126) echo " Команда найдена, но не исполняема."
echo " Проверьте: chmod +x, корректность shebang, формат переводов строк (CRLF)." ;;
127) echo " Команда не найдена."
echo " Проверьте: опечатку, наличие в PATH, установлен ли интерпретатор."
echo " Частый случай: 'bash' в образе Alpine — там только 'sh'." ;;
*)
if [ "$CODE" -gt 128 ] 2>/dev/null; then
sig=$((CODE - 128))
echo " Завершён сигналом $(signal_name "$sig") ($sig)."
else
echo " Код приложения. Смотрите его документацию."
fi
;;
esac
# Отдельный разбор для 137
if [ "$CODE" = "137" ]; then
echo
echo " ── Кто послал SIGKILL ──"
if [ "$OOM" = "true" ]; then
echo " [!] OOM killer: исчерпана память cgroup."
if [ "$MEMLIMIT" != "0" ]; then
printf ' Лимит: %s MB\n' \
"$(awk -v b="$MEMLIMIT" 'BEGIN{printf "%.0f", b/1048576}')"
echo " Действия: увеличить --memory либо найти утечку в приложении."
else
echo " Лимит не задан — память исчерпана на уровне host."
fi
else
events="$(docker events --since 30m --until 0s \
--filter "container=$C" --format '{{.Action}}' 2>/dev/null || true)"
if grep -q '^kill' <<< "$events"; then
echo " Внешний docker kill (событие 'kill' в журнале)."
else
echo " Вероятно, истёк grace period команды 'docker stop'."
echo " Приложение не обработало SIGTERM за отведённое время."
echo " См. раздел 4.4: форма CMD и обработчик сигнала."
fi
fi
fi
if [ "$CODE" = "143" ]; then
echo
echo " Приложение получило SIGTERM и завершилось по действию по умолчанию."
echo " Штатно, но graceful shutdown НЕ выполнялся:"
echo " соединения не закрыты, буферы не сброшены."
echo " Добавьте обработчик SIGTERM — см. раздел 4.4."
fi
echo
echo "═══ Restart loop ═══"
if [ "$STATUS" = "restarting" ]; then
echo " [!] Container сейчас в состоянии restarting."
echo " Разорвать цикл для спокойного разбора:"
echo " docker update --restart=no $C"
elif [ "${RC:-0}" -gt 3 ]; then
echo " [!] Перезапусков: $RC — политика маскирует постоянную проблему."
echo " Автоперезапуск помогает при временных сбоях, но не при"
echo " ошибке конфигурации или утечке памяти."
else
echo " [+] Признаков restart loop нет."
fi
echo
echo "═══ Последние события ═══"
docker events --since 15m --until 0s --filter "container=$C" \
--format ' {{.Time}} {{.Action}}' 2>/dev/null | tail -8 \
|| echo " (недоступны)"
echo
echo "═══ Последние строки логов ═══"
docker logs --tail 8 "$C" 2>&1 | sed 's/^/ /' || echo " (пусто)"
Проверка на трёх сценариях:
chmod +x diagnose-exit.sh
# сценарий 1: OOM
docker run -d --name s-oom --memory=48m --memory-swap=48m python:3.13-slim \
python -c "d=[]
while True: d.append(bytearray(1024*1024))" > /dev/null
sleep 6
./diagnose-exit.sh s-oom
docker rm -f s-oom > /dev/null
# сценарий 2: команда не найдена
docker run -d --name s-127 alpine bash -c 'echo hi' > /dev/null 2>&1 || true
./diagnose-exit.sh s-127 2>/dev/null || true
docker rm -f s-127 > /dev/null 2>&1 || true
# сценарий 3: restart loop
docker run -d --name s-loop --restart=always alpine sh -c 'sleep 1; exit 1' > /dev/null
sleep 12
./diagnose-exit.sh s-loop
docker update --restart=no s-loop > /dev/null
docker rm -f s-loop > /dev/null
Вывод для сценария 1:
═══ Состояние ═══
Статус: exited
Exit code: 137
Политика: no
Перезапусков: 0
═══ Расшифровка кода 137 ═══
Завершён сигналом SIGKILL (9).
── Кто послал SIGKILL ──
[!] OOM killer: исчерпана память cgroup.
Лимит: 48 MB
Действия: увеличить --memory либо найти утечку в приложении.
═══ Restart loop ═══
[+] Признаков restart loop нет.
...
Вывод для сценария 3:
═══ Состояние ═══
Статус: restarting
Exit code: 1
Политика: always
Перезапусков: 9
═══ Расшифровка кода 1 ═══
Общая ошибка приложения. Смотрите логи и трассировку.
═══ Restart loop ═══
[!] Container сейчас в состоянии restarting.
Разорвать цикл для спокойного разбора:
docker update --restart=no s-loop
Что делает скрипт полезным.
Он не просто печатает код, а отвечает на вопрос «что делать». Ключевой пример — код 137: сам по себе он означает лишь SIGKILL, и без различения OOM от docker kill диагностика уходит в неверном направлении. Проверка OOMKilled занимает одну строку и экономит часы.
Второй практически ценный момент — рекомендация docker update --restart=no при активном restart loop. Без неё container продолжает перезапускаться во время разбора, логи затираются новыми, и понять причину трудно.
Функция signal_name покрывает распространённые сигналы; полный список — в man 7 signal.
Проверка результата
docker run -d --name v1 --memory=48m --memory-swap=48m python:3.13-slim \
python -c "d=[]
while True: d.append(bytearray(1024*1024))" > /dev/null
sleep 6
docker inspect v1 --format 'код={{.State.ExitCode}} OOM={{.State.OOMKilled}}'
docker rm -f v1 > /dev/null
Ожидается код=137 OOM=true. Если вы можете объяснить, чем это отличается от код=137 OOM=false, материал усвоен.
Типичные ошибки
| Ошибка | Причина | Исправление |
|---|---|---|
--restart=always для разовой задачи | Кажется «надёжнее» | Успешная задача перезапустится. Использовать no или on-failure |
always вместо unless-stopped для сервиса | Различие неочевидно | always поднимет сервис, остановленный вручную, после перезагрузки |
Толкование 137 только как OOM | Частая ассоциация | Проверять .State.OOMKilled |
Ожидание 143 после docker stop | Так пишут в статьях | Обработавшее сигнал приложение даёт 0; 143 — признак --init |
| Restart policy вместо исправления причины | Сервис «работает» | Смотреть RestartCount; перезапуск для временных сбоев, не постоянных |
| Мониторинг только по «container запущен» | Простой признак | При restart loop статус часто Up. Следить за RestartCount |
Путаница 126 и 127 | Похожие ситуации | 126 — нет прав; 127 — нет файла |
bash в образе Alpine | Скопировано из примеров для Debian | В Alpine только sh; либо доустановить bash |
| Разбор логов при активном restart loop | Логи затираются перезапусками | Сначала docker update --restart=no |
| Ожидание нарастающей задержки всегда | Backoff сбрасывается через 10 с работы | Сервис, падающий раз в 15 с, перезапускается мгновенно |
Контрольные вопросы
На понимание:
- В чём единственное различие между
alwaysиunless-stopped? - Почему
on-failureне перезапускает container, завершившийся с кодом0? - Что означают коды
125,126и127и чем они различаются по причине? - Почему
137не всегда означает нехватку памяти? - Почему backoff иногда не наблюдается при явном restart loop?
На применение:
- Как определить, был ли container убит OOM killer?
- Как разорвать restart loop, не удаляя container?
- Какую политику выбрать для сервиса миграций базы данных и почему?
На диагностику:
docker psпоказываетUp 3 secondsдля сервиса, который должен работать сутками. Что проверить?- Container завершается с кодом
127, хотя образ собирался успешно. Назовите две вероятные причины.
Краткое резюме
- Четыре политики:
no,on-failure[:N],always,unless-stopped. on-failureреагирует только на ненулевой код возврата.- Единственное различие
alwaysиunless-stopped— поведение после перезапуска daemon для вручную остановленного container. - Для сервисов обычно предпочтителен
unless-stopped, для разовых задач —no. - Задержка между перезапусками удваивается и сбрасывается после 10 секунд работы.
- Коды
125,126,127указывают на ошибку Docker, отсутствие прав и отсутствие файла соответственно. 128 + Nозначает завершение сигналом N:137—SIGKILL,143—SIGTERM.137различается по полю.State.OOMKilled: OOM или внешнийSIGKILL.RestartCountбольше нескольких единиц — признак того, что политика маскирует проблему.docker update --restart=noразрывает restart loop, не удаляя container.
Официальные источники
| Источник | Ссылка | Что подтверждает |
|---|---|---|
| Start containers automatically | https://docs.docker.com/engine/containers/start-containers-automatically/ | Все четыре политики, различие always и unless-stopped, backoff и сброс счётчика |
| docker run reference | https://docs.docker.com/reference/cli/docker/container/run/ | Флаг --restart, коды 125, 126, 127 |
| docker update reference | https://docs.docker.com/reference/cli/docker/container/update/ | Изменение restart policy у существующего container |
| docker inspect reference | https://docs.docker.com/reference/cli/docker/inspect/ | Поля .State.ExitCode, .State.OOMKilled, .RestartCount |
| docker events reference | https://docs.docker.com/reference/cli/docker/system/events/ | События die, kill, oom, restart, фильтр --since |
| Runtime options with Memory, CPUs and GPUs | https://docs.docker.com/engine/containers/resource_constraints/ | Поведение OOM killer при заданном лимите памяти |
| Compose services: restart | https://docs.docker.com/reference/compose-file/services/ | Задание политики в Compose |
signal(7) man page | https://man7.org/linux/man-pages/man7/signal.7.html | Номера сигналов для расшифровки кодов 128 + N |
| Control Group v2 | https://docs.kernel.org/admin-guide/cgroup-v2.html | Счётчик oom_kill в memory.events |
Навигация
← Предыдущий материал
Вернуться к разделу
Следующий материал → Практические задания
Главное оглавление