Что происходит при docker run: жизненный цикл контейнера

Разбираем по шагам всё, что делает Docker между нажатием Enter на docker run и завершением контейнера.

docker run — это не одна операция, а цепочка: загрузка образа, создание контейнера, запуск процесса PID 1 и, в конце, его остановка и удаление. Контейнер живёт ровно столько, сколько живёт его главный процесс.

Зачем это знать на практике

Самая частая загадка новичка — «я запустил контейнер, а он сразу остановился». Ответ кроется в жизненном цикле: контейнер не «сервер, который работает всегда», а оболочка вокруг одного процесса. Понимание стадий объясняет также, почему приложение нужно учить корректно реагировать на сигналы остановки, чем stop отличается от kill, и какие состояния показывает docker ps.

docker run = create + start (и иногда pull)

Команда docker run — это удобное объединение нескольких шагов. Эквивалентно можно сделать всё по отдельности:

docker pull nginx          # 1) загрузить образ, если его нет локально
docker create --name web nginx   # 2) создать контейнер (writable-слой, конфиг), но НЕ запускать
docker start web           # 3) запустить главный процесс

# то же самое одной командой:
docker run --name web nginx

Разберём стадии:

1. Pull — загрузка образа

Если нужного образа нет в локальном кеше, Docker тянет его из реестра: читает манифест, скачивает недостающие слои (уже имеющиеся переиспользует) и собирает образ из слоёв. Если образ уже есть локально, шаг пропускается.

2. Create — создание контейнера

На этой стадии Docker готовит контейнер, но не запускает процесс: заводит для него writable-слой поверх слоёв образа, формирует конфигурацию (какую команду запускать, переменные окружения, тома, сеть), резервирует имя и ID. Контейнер переходит в состояние created. Ничего ещё не исполняется.

3. Start — запуск PID 1

Здесь runc создаёт namespaces и cgroups (из прошлого урока), монтирует корневую ФС и запускает главный процесс контейнера — команду из CMD/ENTRYPOINT образа или ту, что вы указали. Этот процесс становится PID 1 внутри контейнера, и контейнер переходит в состояние running.

Главный принцип: контейнер живёт, пока жив PID 1

Это ключевая идея всего урока. Контейнер — оболочка вокруг одного главного процесса. Как только PID 1 завершается (неважно, успешно или с ошибкой), контейнер немедленно переходит в состояние exited. Поэтому контейнер, чей процесс отработал и вышел, «умирает» сразу — это не сбой, а ожидаемое поведение.

# PID 1 — это echo: напечатал и вышел → контейнер сразу exited
docker run --name once ubuntu echo "привет"
docker ps -a
# STATUS
# Exited (0) 2 seconds ago

Отсюда типичный казус: docker run ubuntu без команды завершается мгновенно, потому что у образа нет долгоживущего PID 1. Сервер вроде nginx, наоборот, работает в running, потому что его главный процесс не завершается и держит контейнер.

# процесс-сервер не выходит → контейнер остаётся running
docker run -d --name web nginx
docker ps   # STATUS: Up 10 seconds

# чтобы «просто ubuntu» жил, дайте ему долгоживущий PID 1
docker run -d --name idle ubuntu sleep 3600

Остановка: SIGTERM, период ожидания, SIGKILL

Когда вы вызываете docker stop, Docker делает graceful shutdown в два этапа. Сначала он шлёт PID 1 сигнал SIGTERM — вежливую просьбу завершиться: процесс может дописать данные, закрыть соединения, сбросить буферы. Затем Docker ждёт grace-период (по умолчанию 10 секунд). Если за это время процесс не вышел, Docker посылает SIGKILL — сигнал, который нельзя перехватить или проигнорировать; ядро убивает процесс немедленно.

docker stop web              # SIGTERM, ждать 10s, потом SIGKILL
docker stop -t 30 web        # дать приложению 30 секунд на корректное завершение
docker kill web              # сразу SIGKILL, без вежливости и ожидания

Разница важна: stop даёт приложению шанс завершиться чисто, kill рубит мгновенно. Чтобы graceful shutdown работал, приложение должно обрабатывать SIGTERM. Если игнорирует — оно всё равно умрёт через grace-период по SIGKILL, но уже грубо, потеряв несохранённое.

Полный цикл состояний

           docker create
               │
               ▼
          [created] ──docker start──► [running] ──завершился PID 1──► [exited]
               │                          │                              │
               │                     docker pause                   docker rm
               │                          ▼                              ▼
               │                      [paused]                       (удалён)
               └──────────── docker rm ─────────────────────────────────►

Основные состояния: created (создан, не запускался), running (работает), paused (заморожен через cgroup-freezer, docker pause), exited (процесс завершился — код выхода сохранён), а также restarting, если задана политика рестарта. Из exited контейнер можно снова start (writable-слой и конфиг сохранены) либо удалить.

Remove — удаление

Остановленный контейнер не исчезает: он остаётся в состоянии exited, занимая writable-слой и сохраняя логи и код выхода (полезно для разбора). Удаляет его docker rm — вот тогда стирается writable-слой и метаданные. Флаг --rm в docker run удаляет контейнер автоматически сразу после выхода — удобно для разовых запусков.

docker rm web                # удалить остановленный контейнер
docker run --rm ubuntu echo hi   # запустить и сразу удалить после завершения
docker ps -a --filter status=exited   # посмотреть «трупы» контейнеров

Как это работает под капотом

За кулисами dockerd передаёт команды containerd, тот на стадии start поднимает containerd-shim и через runc запускает процесс. Shim остаётся родителем PID 1, ловит его код выхода и сообщает наверх — поэтому даже после рестарта демона код выхода не теряется. Сигналы stop/kill доходят до процесса через ту же цепочку.

Частые ошибки

  • Ожидать, что контейнер «работает всегда». Он живёт ровно столько, сколько его PID 1; завершился процесс — завершился контейнер.
  • Игнорировать SIGTERM в приложении. Тогда docker stop каждый раз ждёт grace-период и добивает по SIGKILL, теряя данные. Обрабатывайте сигнал и завершайтесь сами.
  • Путать stop и kill. stop — корректное завершение (SIGTERM → ожидание → SIGKILL), kill — немедленный SIGKILL.
  • Забывать удалять exited-контейнеры. Они копятся и занимают место; для разовых запусков используйте --rm.

Итоги

  • docker run = (при необходимости) pull + create + start; каждую стадию можно выполнить отдельной командой.
  • На стадии start запускается главный процесс — PID 1; контейнер живёт ровно столько, сколько живёт этот процесс.
  • docker stop делает graceful shutdown: SIGTERM, grace-период, затем SIGKILL; docker kill бьёт SIGKILL сразу.
  • Состояния: created → running (→ paused) → exited; docker rm окончательно удаляет контейнер и его writable-слой.
Проверьте себя
1. Почему контейнер, запущенный как `docker run ubuntu echo hi`, сразу оказывается в состоянии exited?
AПроизошла ошибка запуска образа
BГлавный процесс (echo) напечатал строку и завершился, а контейнер живёт лишь пока жив PID 1
CУ образа ubuntu нет файловой системы
DDocker по умолчанию останавливает все контейнеры через секунду
2. Чем docker stop отличается от docker kill?
AНичем, это псевдонимы
Bstop удаляет контейнер, kill только останавливает
Cstop шлёт SIGTERM, ждёт grace-период и лишь потом SIGKILL; kill сразу отправляет SIGKILL
Dkill корректнее: он даёт процессу время завершиться
3. Что делает стадия create при разложении docker run на шаги?
AЗапускает главный процесс контейнера
BГотовит контейнер (writable-слой, конфиг, имя), но не запускает процесс — состояние created
CСкачивает образ из реестра
DУдаляет предыдущий контейнер с тем же именем