Введение в механику работы 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] переводит созданный контейнер в активное состояние.