← Docker: продвинутое
16 мин · просто · Урок 3 из 3

Контейнер упал: как разобраться

Код возврата и что он значит, логи и 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.

Порядок действий

Когда контейнер упал или не поднимается, иди по шагам:

  1. docker ps -a: статус и код возврата.
  2. docker logs --tail 50 <name>: что приложение успело написать.
  3. docker inspect <name> --format '{{json .State}}': OOMKilled и Error, если логи пустые.
  4. docker events --since 10m --until 0s --filter container=<name>: как часто он падает.
  5. 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.

Самопроверка

Впишите команду / ключевую строку. Регистр и пробелы не важны.

1. Покажи все контейнеры, включая остановленные и упавшие.
2. Выведи последние 50 строк лога контейнера `api`.
3. Контейнер вышел с кодом 137. Каким сигналом его убили? Впиши номер сигнала.
4. Выведи через `docker inspect` только код возврата контейнера `api`.
5. Запусти образ `myapp` (без тега) интерактивно с `sh` вместо его ENTRYPOINT, контейнер удалить после выхода.

Дрилл · вопросы как на собесе

Проверь себя без подсказок

Сформулируй ответ вслух или письменно, потом раскрой и честно отметь: знал или нет.

01

Контейнер падает с `/bin/bash not found` — как разобраться?

DOC-015

Прошли материал до конца? Отметьте урок пройденным.