Перейти к содержимому
RootPoligon

Жизненный цикл контейнера

Чтение и практика ~18 мин

Ситуация

Вы запустили контейнер, а через минуту docker ps его не показывает. Он удалён? Упал? Его кто-то остановил? Где теперь искать причину?

Главная мысль

Контейнер живёт, пока работает его главный процесс (PID 1). Процесс завершился — контейнер переходит в состояние Exited с кодом завершения этого процесса. Остановленный контейнер не исчезает: его журнал, файлы и код завершения остаются, пока вы его не удалите.

docker run ──▶ Up (работает) ──процесс завершился / docker stop──▶ Exited (код)
                    ▲                                                │
                    └───────────────── docker start ─────────────────┘
                                                      docker rm ──▶ удалён

Новые слова

  • docker ps — работающие контейнеры; docker ps -a — все, включая остановленные.
  • docker logs — журнал контейнера: всё, что главный процесс написал в stdout и stderr.
  • docker exec — выполнить ещё одну команду в работающем контейнере.
  • Код завершения — число, с которым закончил главный процесс: 0 — успех, другое — ошибка или сигнал. 137 = 128 + 9: процесс убит сигналом SIGKILL; 143 = 128 + 15: завершён по SIGTERM.

Разбор: учебный веб-сервер

Команды выполняются на учебной машине от root. Все имена — с префиксом rpd-.

Запуск и список

В Python есть простой веб-сервер — модуль http.server. Запустим его в фоне (-d) с именем rpd-web. Ключ -u отключает буферизацию вывода Python — зачем, станет ясно в разделе про журнал.

docker run -d --name rpd-web python:3.12-slim python3 -u -m http.server 8000
sleep 1
docker ps --filter name=rpd-
CONTAINER ID   IMAGE              COMMAND                  CREATED         STATUS        PORTS     NAMES
842d741fbb22   python:3.12-slim   "python3 -u -m http.…"   2 seconds ago   Up 1 second             rpd-web

--filter name=rpd- показывает только учебные контейнеры. Столбец PORTS пуст: снаружи к серверу пока не подключиться — это тема главы 4.

Журнал и команда внутри

docker logs rpd-web
Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...

Сделаем запрос изнутри контейнера. В образе нет curl, но есть Python:

docker exec rpd-web python3 -c "import urllib.request; print(urllib.request.urlopen('http://localhost:8000/').status)"
200
docker logs rpd-web
Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...
127.0.0.1 - - [16/Oct/2026 11:51:21] "GET / HTTP/1.1" 200 -

Запрос виден в журнале. localhost внутри контейнера — это сам контейнер.

Следить за журналом в реальном времени — docker logs -f rpd-web, выход — Ctrl+C (контейнер при этом продолжит работать). Последние строки — docker logs --tail 20 rpd-web.

Остановка и код завершения

Предскажите, сколько займёт остановка.

time docker stop rpd-web
rpd-web

real	0m10.160s

Десять секунд — не случайность. docker stop посылает главному процессу SIGTERM и ждёт 10 секунд. Процесс с номером 1 по правилам ядра игнорирует сигналы, для которых у программы нет обработчика, — а у http.server его нет. Через 10 секунд Docker посылает SIGKILL, который не игнорируется:

docker ps -a --filter name=rpd-
CONTAINER ID   IMAGE              COMMAND                  CREATED          STATUS                                PORTS     NAMES
842d741fbb22   python:3.12-slim   "python3 -u -m http.…"   12 seconds ago   Exited (137) Less than a second ago             rpd-web

Exited (137) — убит SIGKILL. Приложение курса (глава 3) обрабатывает SIGTERM и останавливается сразу с кодом 0. Код можно получить и отдельно:

docker inspect -f '{{.State.Status}} {{.State.ExitCode}}' rpd-web
exited 137

Снова запустить — тот же контейнер

docker start rpd-web
sleep 1
docker logs rpd-web

start запускает тот же контейнер с той же командой. В журнале остались строки прошлого запуска — журнал принадлежит контейнеру, а не запуску. docker run каждый раз создал бы новый контейнер.

Контейнер, который сразу завершается

docker run --name rpd-fail python:3.12-slim python3 -c "import sys; print('нет файла настроек /etc/app.conf'); sys.exit(3)"
нет файла настроек /etc/app.conf

Без -d вывод идёт прямо в терминал, а команда docker run сама завершается с кодом процесса (echo $? → 3).

docker ps -a --filter name=rpd- --format '{{.Names}} {{.Status}}'
rpd-fail Exited (3) Less than a second ago
rpd-web Up Less than a second

В остановленном контейнере exec не работает:

docker exec rpd-fail ls
Error response from daemon: container df2aa82c148f… is not running

Но журнал читается: docker logs rpd-fail покажет то же сообщение.

Уборка

docker rm -f rpd-web rpd-fail

Без -f работающий контейнер не удалится: cannot remove container "rpd-web": container is running.

Практика: почему контейнер не работает

Запустите «сломанный» контейнер:

docker run -d --name rpd-broken python:3.12-slim python3 -c "open('/etc/app.conf')"

Команда напечатала идентификатор — кажется, всё в порядке. Выясните, ничего не меняя:

  1. Работает ли контейнер сейчас?
  2. С каким кодом он завершился?
  3. Какая строка журнала называет причину?

Затем удалите его.

Признак успеха: три ответа, для каждого — команда, которая его показала; docker ps -a --filter name=rpd- пуст.

Подсказка 1

В обычном docker ps его нет. Какой ключ показывает и остановленные контейнеры?

Подсказка 2

Код виден в столбце STATUS у docker ps -a и в docker inspect -f '{{.State.ExitCode}}' ….

Подсказка 3

Причину ищут в docker logs rpd-broken — последняя строка трассировки Python.

Решение — в answers/02-lifecycle.md.

Если результат другой

  • docker logs пустой, хотя программа точно что-то печатает. Python буферизует вывод, когда пишет не в терминал, и сбрасывает его поздно. Запустите с python3 -u или задайте переменную PYTHONUNBUFFERED=1. Приложение курса печатает с немедленным сбросом.
  • Conflict. The container name "/rpd-web" is already in use. Контейнер с этим именем уже есть, может быть остановленный: docker ps -a --filter name=rpd-web. Нужен прежний — docker start rpd-web; не нужен — удалите.
  • docker stop ждёт 10 секунд. Главный процесс не обрабатывает SIGTERM (см. разбор). Это не ошибка Docker; лечится в самом приложении или запуском с --init.

Проверка понимания

  1. Контейнер в docker ps -a со статусом Exited (0). Он упал?
  2. Чем docker start rpd-web отличается от повторного docker run --name rpd-web …?

Ответы — в answers/02-lifecycle.md.

Дальше: 3. Dockerfile и контекст сборки.

Проверка автора и источники

  • Воспроизведено: все команды разбора и практики — Docker Engine 29.8.2, образ-заменитель python:3.12-slim с Python 3.12.3 (см. карту курса). Задержка docker stop 10 секунд и код 137, буферизация вывода без -u (журнал без строки Serving HTTP…), сохранение журнала после start — проверены. Идентификаторы и время — из этого запуска. Таблицы docker ps показаны как есть; на узком экране они переносятся.
  • Источники: docker container stop (SIGTERM, затем SIGKILL через 10 секунд), docker container logs, docker container exec, docker run: --init, Python -u.
Разбор и ответы — откройте после своей попытки

Практика

1. Работает ли?

docker ps --filter name=rpd-broken

Таблица пустая — не работает. Остановленные показывает -a:

docker ps -a --filter name=rpd-broken --format '{{.Names}} {{.Status}}'
rpd-broken Exited (1) 1 second ago

2. Код завершения — 1 (в столбце STATUS или docker inspect -f '{{.State.ExitCode}}' rpd-broken).

3. Причина:

docker logs rpd-broken
Traceback (most recent call last):
  File "<string>", line 1, in <module>
FileNotFoundError: [Errno 2] No such file or directory: '/etc/app.conf'

Главный процесс попытался открыть файл, которого нет в образе, и завершился с ошибкой. Контейнер жил ровно столько, сколько процесс.

Уборка: docker rm rpd-broken — контейнер уже остановлен, -f не нужен.

Воспроизведено: Docker Engine 29.8.2, образ-заменитель python:3.12-slim.

Проверка понимания

  1. Нет. Код 0 — главный процесс завершился успешно: например, выполнил свою работу и вышел, или обработал SIGTERM при docker stop. Упал бы с ненулевым кодом. Чтобы понять, что было, — docker logs.
  2. start запускает существующий контейнер: те же настройки, тот же журнал, те же изменения файлов в его слое. run создаёт новый контейнер из образа; с тем же именем не получится, пока старый не удалён, — Docker ответит Conflict.

Прочитали и разобрались? Отметьте урок — прогресс сохранится в этом браузере.