Контейнер упал: как разобраться
Код возврата и что он значит, логи и inspect упавшего контейнера, перезапуск по кругу, ошибка «no such file» при живом файле и отладка контейнера без shell.
Контейнер пропал из списка
Типичная картина: запускаешь сервис, проверяешь docker ps, а его там
нет. Повтори у себя, это безопасно:
docker run -d --name api alpine sh -c 'echo "Error: connect ECONNREFUSED 127.0.0.1:5432" >&2; exit 1'
docker ps
# CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
Список пустой, но контейнер никуда не делся. docker ps показывает
только запущенные, а этот стартовал и сразу завершился. Смотри все:
docker ps -a
# CONTAINER ID IMAGE STATUS NAMES
# 3f2a1c9e8b7d alpine Exited (1) 5 seconds ago api
Exited (1) уже подсказка: в скобках код возврата (exit code), с
которым завершился главный процесс контейнера. С него и начинай.
Что говорит код возврата
Процесс в Linux, завершаясь, отдаёт число от 0 до 255. Ноль означает успех, любое другое число означает ошибку.
Коды больше 128 особенные. В Unix так договорились: если процесс не
вышел сам, а его убили сигналом, код равен 128 плюс номер сигнала.
kill -9 шлёт сигнал 9, поэтому убитый так процесс даёт 137.
| Код | Что обычно значит |
|---|---|
0 | процесс отработал и вышел сам |
1 | ошибка в приложении, ищи в docker logs |
125 | не сработал сам docker run: опечатка во флаге, занятое имя |
126 | файл из CMD есть, но не запускается: нет права x |
127 | команды из CMD нет в образе |
137 | убит сигналом 9 (SIGKILL) |
143 | убит сигналом 15 (SIGTERM) и корректно завершился |
С кодами 125, 126 и 127 есть подвох. Их возвращает сам docker run,
потому что процесс внутри так и не стартовал. Контейнер остаётся в
статусе Created, а не Exited, и docker logs у него пустой.
Например, в alpine нет bash:
docker run --name nobash alpine bash
# docker: Error response from daemon: ... exec: "bash": executable file not found in $PATH
echo $?
# 127
Причину в таком случае показывает docker inspect, о нём ниже.
Код 137 бывает по трём причинам: контейнер упёрся в лимит памяти, его
убили через docker kill, или docker stop не дождался завершения за 10
секунд и добил процесс. Какая из трёх, покажет docker inspect. Почему
stop вообще приходится добивать, разберём в следующем уроке.
Про 0 отдельно. Контейнер живёт, пока живёт его главный процесс.
Запусти docker run -d alpine, и контейнер выйдет с кодом 0 через долю
секунды. Главная команда образа alpine это sh, а без подключённого
ввода (флага -i) ему нечего читать, и он завершается. Ошибки здесь нет.
Если так ведёт себя твой сервис, проверь, какая команда зашита в образ:
docker inspect alpine --format '{{.Config.Cmd}}'
# [/bin/sh]
Логи упавшего контейнера
docker logs работает и на остановленном контейнере. Всё, что процесс
успел написать в stdout и stderr, сохранилось:
docker logs api
# Error: connect ECONNREFUSED 127.0.0.1:5432
Приложение пыталось подключиться к базе на 127.0.0.1, и соединение
отклонили. Внутри контейнера 127.0.0.1 указывает на сам контейнер, а
не на хост и не на соседний контейнер с базой. Вместо него нужно имя
сервиса базы в общей сети, например db:5432. Эту ловушку подробно
разбирали в уроке про volumes и переменные окружения.
Если лог длинный, бери хвост или окно по времени:
docker logs --tail 50 api # последние 50 строк
docker logs --since 10m api # всё за последние 10 минут
docker logs -f api # и дальше следить в реальном времени
Логи живут, пока живёт контейнер. После docker rm они уходят вместе с
ним. Поэтому --rm на сервисе, который надо отлаживать, мешает: упал,
удалился, и смотреть уже нечего.
Если сервисы подняты через compose, команды те же, только через
docker compose: docker compose ps -a и docker compose logs --tail 50 api.
docker inspect: состояние контейнера
Если в логах пусто, иди в docker inspect. Он выдаёт большой JSON со
всем, что Docker знает о контейнере. Можно запустить его без флагов и
найти нужное глазами, но проще сразу вытащить раздел State. Возьмём контейнер, которому не
хватило памяти: ему дают 20 МБ, а tail /dev/zero съедает их за секунду.
docker run -d --name oom --memory 20m --memory-swap 20m alpine tail /dev/zero
docker inspect oom --format '{{json .State}}'
{
"Status": "exited",
"ExitCode": 137,
"OOMKilled": true,
"Error": "",
"StartedAt": "2026-09-29T10:14:02.51Z",
"FinishedAt": "2026-09-29T10:14:03.02Z"
}
Вывод сокращён, полей там больше. Главное видно сразу: код 137 и
OOMKilled: true. Контейнер упёрся в лимит памяти, и ядро убило процесс.
Если OOMKilled равен false, а код 137, значит процесс убили снаружи:
docker kill или docker stop по таймауту.
Лимит ты мог и не ставить. Тогда контейнер растёт, пока не кончится память всей машины, и OOM-killer ядра выбирает жертву уже на хосте. Механику лимитов разберём в уроке про ресурсы.
Поле Error заполняется, когда процесс вообще не стартовал. Для
контейнера nobash из прошлого раздела:
docker inspect nobash --format '{{.State.Status}} {{.State.ExitCode}} {{.State.Error}}'
# created 127 ... exec: "bash": executable file not found in $PATH
Контейнер перезапускается по кругу
Хуже, когда контейнер крутится в цикле. С политикой перезапуска
--restart unless-stopped Docker поднимает упавший контейнер снова.
Вот сервис, который падает через две секунды после старта:
docker run -d --name loop --restart unless-stopped alpine sh -c 'echo starting; sleep 2; exit 1'
Подожди секунд десять и посмотри:
docker ps
# STATUS
# Restarting (1) 1 second ago
docker inspect loop --format '{{.RestartCount}}'
# 4
В скобках у Restarting стоит код последнего падения, а RestartCount
считает попытки. Паузы между перезапусками растут: 100 мс, 200 мс, 400 мс
и дальше вдвое, до минуты. Так Docker не даёт сломанному сервису
перезапускаться тысячу раз в секунду.
Посмотреть хронологию падений помогает docker events. Это поток событий
Docker: создал, запустил, процесс умер. Без --since он показывает только
новые события, поэтому задай окно в прошлое и фильтр по событию die:
docker events --since 5m --until 0s --filter container=loop --filter event=die
# 2026-09-29T10:20:03.4+03:00 container die 3f2a1c9e8b7d (execDuration=2, exitCode=1, image=alpine, name=loop)
# 2026-09-29T10:20:06.1+03:00 container die 3f2a1c9e8b7d (execDuration=2, exitCode=1, image=alpine, name=loop)
# 2026-09-29T10:20:09.3+03:00 container die 3f2a1c9e8b7d (execDuration=2, exitCode=1, image=alpine, name=loop)
Вывод сокращён. --until 0s значит «до текущего момента», без него
команда повиснет и будет ждать новых событий. По времени видно, что
контейнер живёт пару секунд и падает с кодом 1. Значит, проблема при
старте приложения, а не под нагрузкой через час.
Цикл будет крутиться, пока не удалишь контейнер: docker rm -f loop.
Залезть внутрь образа, который не стартует
docker exec работает только на запущенном контейнере. Если
контейнер падает через секунду после старта, зайти в него не успеешь.
Выход: запустить тот же образ, но подменить точку входа на shell.
docker run --rm -it --entrypoint sh myapp:1.0
Почему не docker run -it myapp:1.0 sh? Так ты заменишь только CMD. Если
в образе задан ENTRYPOINT, sh станет его аргументом, и приложение всё
равно запустится. --entrypoint заменяет саму точку входа.
Приложение не стартует, и падать нечему. Внутри обычная проверка:
ls -la /app # код на месте?
cat /app/config.yml # конфиг тот?
env | grep DB_ # переменные дошли?
which node # бинарь есть?
Здесь ты видишь файлы образа. Если в боевом запуске сервис получает
переменные через -e или --env-file, а конфиг через -v, передай всё
это и сюда. Иначе будешь смотреть не на то, что видит приложение:
docker run --rm -it --env-file .env -v "$PWD/config.yml:/app/config.yml" --entrypoint sh myapp:1.0
Ещё один приём: вытащить файл из остановленного контейнера на хост.
docker cp работает даже на упавшем:
docker run --name crash alpine sh -c 'echo boom > /err.log; exit 1'
docker cp crash:/err.log ./err.log
cat err.log
# boom
Удобно, когда приложение пишет лог в файл, а не в stdout.
«no such file or directory», хотя файл на месте
Эта ошибка сбивает с толку: ls файл показывает, а Docker говорит,
что его нет. Соберём образ на alpine со скриптом, у которого в первой
строке #!/bin/bash:
docker build -t shebang-demo - <<'EOF'
FROM alpine
RUN printf '#!/bin/bash\necho hi\n' > /start.sh && chmod +x /start.sh
CMD ["/start.sh"]
EOF
docker run --name sb shebang-demo
# exec /start.sh: no such file or directory
Первая строка скрипта #!... называется шебанг. Она говорит ядру, какой
программой выполнять скрипт. Здесь это /bin/bash, а в alpine bash нет,
есть только /bin/sh. Ядро не находит интерпретатор и пишет «no such
file», хотя речь не о самом скрипте.
Проверь через --entrypoint:
docker run --rm --entrypoint sh shebang-demo -c 'head -1 /start.sh; ls /bin/bash'
# #!/bin/bash
# ls: /bin/bash: No such file or directory
Лечится заменой шебанга на #!/bin/sh или установкой bash через
apk add bash.
Обрати внимание на отличие от nobash. Когда bash стоит прямо в CMD,
контейнер даже не стартует: код 127, статус Created, причина в
.State.Error. Когда bash нужен только шебангу, процесс успевает
стартовать. Контейнер выходит с кодом 1, а ошибка лежит в docker logs.
У того же текста бывает и другая причина. Скрипт сохранили на Windows, и
строки заканчиваются на \r\n. Ядро ищет интерпретатор /bin/sh\r,
которого нет. Символ \r глазами не видно, но его покажет cat -A
внутри контейнера (на macOS локально используй cat -v):
cat -A start.sh
# #!/bin/sh^M$
# echo hi^M$
^M в конце строк и есть \r. Помогает dos2unix start.sh или
настройка редактора сохранять файлы с переводами строк LF.
exec format error
exec /app/server: exec format error
Файл есть, запускается, но ядро не понимает его формат. Чаще всего
бинарь собран под другую архитектуру процессора. Например, образ собрали
на Mac с M-процессором под arm64, а сервер работает на amd64. Быстрая
проверка: собрать заново с --platform linux/amd64. Подробно разберём в
конце трека.
Вторая причина: скрипт без шебанга в первой строке. Ядру нечем его выполнить.
Отладка работающего контейнера без shell
Бывает, что сервис запущен, но ведёт себя странно: не отвечает на
порту, отдаёт ошибки. Зайти через docker exec -it web sh не
получается, потому что в образе нет shell. Так устроены минимальные
образы: distroless содержит только приложение и нужные ему библиотеки, а
scratch вообще пустой.
Помогает соседний контейнер с инструментами. Возьмём busybox: это
крошечный образ с Docker Hub, где собраны sh, ps, wget, netstat и
другие базовые утилиты. Его можно подключить к namespace сервиса.
Namespace — механизм ядра, который даёт контейнеру свой список процессов
и свою сеть. Подключившись к ним, busybox увидит процессы и сеть сервиса
так, будто работает внутри него.
Для примера подойдёт обычный nginx:
docker run -d --name web nginx:alpine
docker run --rm -it --pid=container:web --network=container:web busybox sh
Внутри:
ps # процессы nginx
netstat -tlnp # какие порты слушает сервис
wget -qO- localhost:80 # запрос к нему изнутри его же сети
У netstat флаги значат: -t TCP, -l только слушающие сокеты, -n
числа вместо имён, -p какой процесс держит порт. Если сервис работает
не от root, колонка процесса будет пустой. Тогда добавь busybox флаг
--cap-add SYS_PTRACE.
Флаг --network=container:web сажает busybox в сетевой namespace
сервиса, и localhost для них теперь общий. --pid=container:web делает
то же с процессами. Сам сервис не трогаем и ничего в его образ не
доустанавливаем.
Приём работает только пока сервис запущен. К namespace остановленного
контейнера подключиться нельзя. Если образ без shell падает на старте,
остаются docker logs, docker cp для файлов и отладочный вариант
образа. У distroless есть теги :debug, в которых добавлен busybox:
временно собери сервис на таком образе и зайди через --entrypoint sh.
Порядок действий
Когда контейнер упал или не поднимается, иди по шагам:
docker ps -a: статус и код возврата.docker logs --tail 50 <name>: что приложение успело написать.docker inspect <name> --format '{{json .State}}':OOMKilledиError, если логи пустые.docker events --since 10m --until 0s --filter container=<name>: как часто он падает.docker run --rm -it --entrypoint sh <image>: заглянуть в образ, который не стартует.
Чаще всего ответ находится уже на втором шаге. Но при OOM и при незапустившемся процессе логи пустые, и тогда выручает третий.
Практика: найди причину
Запусти четыре сломанных контейнера и для каждого найди причину по шагам выше. Ответы спрятаны под спойлером.
docker run -d --name p1 alpine bash
docker run -d --name p2 --memory 20m --memory-swap 20m alpine tail /dev/zero
docker run -d --name p3 shebang-demo
docker run -d --name p4 --restart unless-stopped alpine sh -c 'sleep 2; exit 3'
Ответы
p1: статусCreated, код 127, в.State.Errorсказано, чтоbashне найден. В alpine нет bash.p2: код 137,OOMKilled: true. Не хватило памяти.p3: код 1, вdocker logsстрокаexec /start.sh: no such file or directory. Шебанг ссылается на bash, которого нет.p4: статусRestarting (3),RestartCountрастёт. Приложение выходит с кодом 3 через две секунды после старта.
Чтобы починить p3, пересобери shebang-demo с #!/bin/sh в
printf и запусти снова. В логах должно появиться hi.
Когда закончишь, удали всё, что создал за урок:
docker rm -f p1 p2 p3 p4 api nobash oom sb crash web
docker rmi shebang-demo
Что важно запомнить
Код больше 128 означает, что процесс убили сигналом, а номер сигнала
равен коду минус 128. Код 137 не всегда про память: смотри OOMKilled в
docker inspect.
Если контейнер остался в статусе Created с кодом 125, 126 или 127,
процесс не стартовал. Логов не будет, причина лежит в .State.Error.
--rm уничтожает логи вместе с контейнером. На сервисе, который
отлаживаешь, его лучше не ставить.
«No such file or directory» при живом файле почти всегда означает
шебанг или переводы строк от Windows. Соседний контейнер с
--pid=container: и --network=container: помогает только живому
сервису.
В следующем уроке разберём, почему docker stop иногда думает ровно 10
секунд и при чём тут PID 1.
Самопроверка
Впишите команду / ключевую строку. Регистр и пробелы не важны.
Дрилл · вопросы как на собесе
Проверь себя без подсказок
Сформулируй ответ вслух или письменно, потом раскрой и честно отметь: знал или нет.
Контейнер падает с `/bin/bash not found` — как разобраться?
DOC-015Прошли материал до конца? Отметьте урок пройденным.
Прогресс сохранён в этом браузере
Почистишь историю или откроешь с телефона, и отметки пропадут. Оставь почту, они переедут в аккаунт. Пароль придумывать не нужно.
Готово. теперь в аккаунте: прогресс виден с любого устройства. Мой прогресс