Введение в механику работы Docker

Когда в пятницу вечером падающий production требует срочного деплоя, а мимолетный взгляд на мониторинг вызывает легкий тиковый нерв, первая мысль каждого инженера сводится к спасительной строчке в терминале. Пять символов — и вот изолированное окружение уже крутится на сервере, словно по волшебству (правда, иногда оказывается, что забыли пробросить порт, но это уже классика). Прямо сейчас, пока вы читаете эти строки, тысячи CI/CD пайплайнов по всему миру запускают контейнеры, даже не задумываясь о том колоссальном объеме работы, который операционная система выполняет по ту сторону экрана.

Каждый разработчик, начинающий путь в мире контейнеризации, неизменно сталкивается с командой docker run. Это настоящий швейцарский нож экосистемы Docker. Одной строчкой в терминале мы заставляем систему найти нужный образ в реестре, загрузить его, выделить ресурсы, создать изолированное окружение и запустить приложение. Магия происходит за миллисекунды, и сервис готов к работе.

Однако за внешней простотой скрывается сложная последовательность системных вызовов и ядерных абстракций Linux. В среде DevOps часто возникает вопрос: можно ли представить поведение docker run как комбинацию трех атомарных команд — docker pull, docker create и docker start? Давайте разберем архитектуру Docker под капотом, проанализируем жизненный цикл контейнера и выясним, насколько корректно это утверждение с технической точки зрения.

Анатомия команды docker run: что происходит под капотом?

Когда пользователь отправляет запрос docker run, Docker CLI передает его демону (dockerd), который через containerd и интерфейс воркера контейнеров инициирует многоступенчатый конвейер. Но как именно демон понимает, что пора выделять ресурсы? Давайте проследим этот путь от нажатия Enter до первого лога в консоли.

Контейнеры строятся на базе образов, использующих слоистую файловую систему (UnionFS). Вызов docker run запускает следующие этапы:

  • Проверка локального кэша образов; если образ отсутствует, инициируется скачивание (pull).
  • Генерация уникального UUID и выделение изолированного пространства.
  • Создание тонкого записываемого слоя поверх Read-Only слоев образа (Copy-on-Write).
  • Настройка сетевого стека: создание сетевых интерфейсов, выделение IP во внутренней сети моста (bridge) и маппинг портов.
  • Применение ограничений через Linux cgroups (CPU, RAM, I/O) и Namespaces (PID, NET, MNT, IPC, UTS, USER).
  • Монтирование томов (Volumes) и передача переменных окружения (включая те самые секреты, которые «временные» и висят в продакшене годами).
  • Запуск процесса, определенного инструкциями ENTRYPOINT и CMD.

Сравнение с цепочкой: pull, create, start

Разобравшись с глубинными механизмами демона, логично задаться вопросом: а можем ли мы собрать этот конструктор вручную из более мелких деталей, чтобы лучше контролировать каждый шаг?

Давайте сопоставим монолитный вызов docker run с поэтапным выполнением трех классических команд.

1. Получение образа (docker pull)

Команда docker pull скачивает слои образа с удаленного реестра (например, Docker Hub) в локальное хранилище. Точно так же docker run проверяет наличие локального образа и, если его нет, автоматически выполняет pull. Разница лишь в том, что run делает это неявно, прерывая запуск до завершения загрузки.

2. Создание контейнера (docker create)

Команда docker create подготавливает файловую систему, сети и конфигурацию контейнера, но не запускает его. Ей присваивается ID, создается записываемый слой, однако процесс внутри не стартует. На этом этапе контейнер находится в состоянии Created.

3. Запуск контейнера (docker start)

Команда docker start [CONTAINER_ID] переводит созданный контейнер в активное состояние.