Главная/Containers и lifecycle/Урок

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 маскирует проблему вместо её решения.

Предварительные знания

Ключевые термины

ТерминОбъяснение
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. Оно проявляется в одном сценарии:

text
   1. Container работает с политикой always / unless-stopped
   2. Администратор выполняет docker stop  → container остановлен
   3. Сервер перезагружается, daemon стартует
        │
        ├─ always:          container ЗАПУСКАЕТСЯ снова
        └─ unless-stopped:  container ОСТАЁТСЯ остановленным

Практический смысл: unless-stopped уважает явное решение человека. Если вы остановили сервис намеренно — например, для обслуживания, — он не поднимется сам после перезагрузки.

Именно поэтому unless-stopped обычно предпочтительнее always для сервисов.

Выбор политики

Тип нагрузкиПолитикаОбоснование
Web-сервис, APIunless-stoppedДолжен работать постоянно, но уважать ручную остановку
Background workerunless-stoppedТо же
Databaseunless-stoppedТо же
Разовая миграцияnoДолжна выполниться один раз; перезапуск опасен
Задача, которую можно повторитьon-failure:3Перезапуск при сбое, но не бесконечно
Отладочный containernoПерезапуск мешает разбираться
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См. ниже
137SIGKILL (128+9)OOM, docker kill, истёк grace period
143SIGTERM (128+15)docker stop без обработчика в приложении
139SIGSEGV (128+11)Ошибка сегментации
130SIGINT (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:

  1. Записывает ExitCode, FinishedAt, OOMKilled.
  2. Проверяет политику и код.
  3. Если перезапуск нужен — увеличивает RestartCount, переводит container в restarting, ждёт backoff.
  4. Создаёт новую задачу.

Состояние restarting видно в docker ps как Restarting (N) M seconds ago, где N — код прошлого завершения.

Откуда берётся OOMKilled

При исчерпании памяти в cgroup ядро выбирает процесс и посылает ему SIGKILL. Событие фиксируется в файле memory.events cgroup в счётчике oom_kill.

Daemon читает этот счётчик и выставляет .State.OOMKilled. Именно поэтому поле надёжно различает OOM и внешний SIGKILL — источник данных разный.


Команды и примеры

Подготовка

bash
mkdir -p /tmp/restart && cd /tmp/restart

Политика no

bash
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
text
NAMES   STATUS
p-no    Exited (1) 3 seconds ago
перезапусков: 0

Container упал и остался лежать — политика по умолчанию.

Политика on-failure реагирует только на ошибки

bash
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
text
--- завершение с кодом 1 ---
код: 1  перезапусков: 3  статус: exited
--- завершение с кодом 0 ---
код: 0  перезапусков: 0  статус: exited

Первый перезапускался три раза и остановился — лимит достигнут. Второй не перезапускался вовсе: код 0 не считается сбоем.

Это делает on-failure правильным выбором для повторяемых задач и неправильным для сервисов, которые могут завершиться с кодом 0 по ошибке конфигурации.

always против unless-stopped

Различие проявляется только при перезапуске daemon. Воспроизведём.

bash
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}}'
text
NAMES       STATUS
r-unless    Exited (137) 1 second ago
r-always    Exited (137) 1 second ago

Оба остановлены. Теперь перезапустим daemon:

bash
sudo systemctl restart docker
sleep 5
docker ps -a --filter name=r- --format 'table {{.Names}}\t{{.Status}}'
text
NAMES       STATUS
r-unless    Exited (137) 22 seconds ago
r-always    Up 4 seconds

Вот оно, единственное различие: always поднялся сам, unless-stopped уважил ручную остановку.

bash
docker rm -f r-always r-unless > /dev/null

Именно поэтому для сервисов на серверах обычно выбирают unless-stopped. С always остановленный на обслуживание сервис поднимется после любой перезагрузки — иногда в самый неподходящий момент.

Наблюдение за restart loop

bash
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
text
наблюдаем 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 в действии.

Состояние:

bash
docker ps --filter name=looper --format 'table {{.Names}}\t{{.Status}}'
docker inspect looper --format 'перезапусков: {{.RestartCount}}  код: {{.State.ExitCode}}'
text
NAMES    STATUS
looper   Restarting (1) 2 seconds ago
text
перезапусков: 8  код: 1

Restarting (1) — в скобках код прошлого завершения. Счётчик перезапусков растёт.

Как отличить restart loop от нормальной работы: в docker ps статус Restarting вместо Up, а RestartCount увеличивается при повторных проверках.

bash
sleep 5
docker inspect looper --format 'перезапусков спустя 5 с: {{.RestartCount}}'
docker rm -f looper > /dev/null
text
перезапусков спустя 5 с: 10

Backoff и сброс счётчика

Приложение, живущее дольше 10 секунд, сбрасывает backoff:

bash
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
text
1785408101 start
1785408116 die
1785408116 start
1785408131 die
1785408131 start
1785408146 die

Задержка между die и start остаётся нулевой — приложение успевало проработать больше 10 секунд, и backoff сбрасывался каждый раз.

Практический вывод: отсутствие нарастающей задержки не означает отсутствия проблемы. Сервис, падающий раз в 15 секунд, перезапускается мгновенно и в docker ps часто выглядит как Up.

Коды 125, 126, 127

bash
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 "код: $?"
text
=== 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:

bash
docker run --rm alpine bash -c 'echo test' 2>&1 | tail -1
echo "код: $?"
text
... exec: "bash": executable file not found in $PATH
код: 127

В Alpine нет bash — только sh из BusyBox. Ошибка встречается постоянно при копировании команд из примеров для Debian.

Различение OOM и docker kill

bash
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
text
=== случай 1: OOM ===
код: 137  OOMKilled: true
=== случай 2: docker kill ===
код: 137  OOMKilled: false

Одинаковый код 137, разные причины. Поле OOMKilled — единственный надёжный различитель.

Подтверждение через события:

bash
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
text
create
start
oom
die

Событие oom присутствует явно. Отслеживание docker events --filter event=oom — практичный способ мониторинга нехватки памяти.

Опасная комбинация: always и разовая задача

bash
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
text
миграция выполнилась раз:
5
перезапусков: 4

Миграция отработала успешно и была перезапущена четыре раза. Для неидемпотентной миграции — «добавить колонку», «вставить начальные данные» — это ошибка или повреждение данных.

Правильно: --restart=no (по умолчанию) либо on-failure с ограничением, если повтор безопасен.

В Compose для таких задач используется зависимость по успешному завершению — раздел 09.

Изменение политики у работающего container

bash
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
text
политика: no
политика: unless-stopped
политика: no

docker update --restart=no — способ разорвать restart loop, не удаляя container. Container остановится после следующего падения, и вы сможете спокойно изучить его логи и состояние.

Уборка

bash
cd /tmp && rm -rf /tmp/restart

Практическое упражнение

Задание. Напишите скрипт diagnose-exit.sh <container>, который по завершившемуся или перезапускающемуся container выдаёт диагноз: что произошло и что делать.

Скрипт должен:

  1. Определить exit code и расшифровать его.
  2. Для кода 137 различить OOM, docker kill и истёкший grace period.
  3. Определить наличие restart loop по RestartCount и статусу.
  4. Показать последние события из docker events.
  5. Показать последние строки логов.
  6. Выдать конкретную рекомендацию для каждого случая.

Дополнительно: напишите функцию, которая по коду возврата определяет, был ли процесс завершён сигналом, и возвращает имя сигнала.

Подсказки

Подсказка 1

Код больше 128 обычно означает завершение сигналом: номер сигнала равен code - 128.

Подсказка 2

Исторические события доступны через docker events --since:

bash
docker events --since 10m --until 0s --filter "container=<name>" --format '{{.Action}}'
Подсказка 3

Отличить истёкший grace period от docker kill: в первом случае в событиях есть kill с сигналом 15 перед die, во втором — с сигналом 9.

Решение

Сначала выполните задание самостоятельно.

Показать решение
bash
#!/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 "  (пусто)"

Проверка на трёх сценариях:

bash
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:

text
═══ Состояние ═══
  Статус:          exited
  Exit code:       137
  Политика:        no
  Перезапусков:    0

═══ Расшифровка кода 137 ═══
  Завершён сигналом SIGKILL (9).

  ── Кто послал SIGKILL ──
  [!] OOM killer: исчерпана память cgroup.
      Лимит: 48 MB
      Действия: увеличить --memory либо найти утечку в приложении.

═══ Restart loop ═══
  [+] Признаков restart loop нет.
...

Вывод для сценария 3:

text
═══ Состояние ═══
  Статус:          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.

Проверка результата

bash
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 с, перезапускается мгновенно

Контрольные вопросы

На понимание:

  1. В чём единственное различие между always и unless-stopped?
  2. Почему on-failure не перезапускает container, завершившийся с кодом 0?
  3. Что означают коды 125, 126 и 127 и чем они различаются по причине?
  4. Почему 137 не всегда означает нехватку памяти?
  5. Почему backoff иногда не наблюдается при явном restart loop?

На применение:

  1. Как определить, был ли container убит OOM killer?
  2. Как разорвать restart loop, не удаляя container?
  3. Какую политику выбрать для сервиса миграций базы данных и почему?

На диагностику:

  1. docker ps показывает Up 3 seconds для сервиса, который должен работать сутками. Что проверить?
  2. Container завершается с кодом 127, хотя образ собирался успешно. Назовите две вероятные причины.

Краткое резюме

  1. Четыре политики: no, on-failure[:N], always, unless-stopped.
  2. on-failure реагирует только на ненулевой код возврата.
  3. Единственное различие always и unless-stopped — поведение после перезапуска daemon для вручную остановленного container.
  4. Для сервисов обычно предпочтителен unless-stopped, для разовых задач — no.
  5. Задержка между перезапусками удваивается и сбрасывается после 10 секунд работы.
  6. Коды 125, 126, 127 указывают на ошибку Docker, отсутствие прав и отсутствие файла соответственно.
  7. 128 + N означает завершение сигналом N: 137SIGKILL, 143SIGTERM.
  8. 137 различается по полю .State.OOMKilled: OOM или внешний SIGKILL.
  9. RestartCount больше нескольких единиц — признак того, что политика маскирует проблему.
  10. docker update --restart=no разрывает restart loop, не удаляя container.

Официальные источники

ИсточникСсылкаЧто подтверждает
Start containers automaticallyhttps://docs.docker.com/engine/containers/start-containers-automatically/Все четыре политики, различие always и unless-stopped, backoff и сброс счётчика
docker run referencehttps://docs.docker.com/reference/cli/docker/container/run/Флаг --restart, коды 125, 126, 127
docker update referencehttps://docs.docker.com/reference/cli/docker/container/update/Изменение restart policy у существующего container
docker inspect referencehttps://docs.docker.com/reference/cli/docker/inspect/Поля .State.ExitCode, .State.OOMKilled, .RestartCount
docker events referencehttps://docs.docker.com/reference/cli/docker/system/events/События die, kill, oom, restart, фильтр --since
Runtime options with Memory, CPUs and GPUshttps://docs.docker.com/engine/containers/resource_constraints/Поведение OOM killer при заданном лимите памяти
Compose services: restarthttps://docs.docker.com/reference/compose-file/services/Задание политики в Compose
signal(7) man pagehttps://man7.org/linux/man-pages/man7/signal.7.htmlНомера сигналов для расшифровки кодов 128 + N
Control Group v2https://docs.kernel.org/admin-guide/cgroup-v2.htmlСчётчик oom_kill в memory.events

Навигация

← Предыдущий материал
Вернуться к разделу
Следующий материал → Практические задания
Главное оглавление

Markdown на GitHub ↗