Архитектура Docker: демон, клиент, containerd, OCI
Разбираем, из каких частей на самом деле состоит «Docker» и кто за что отвечает.
Docker — это не один процесс, а связка из клиента (CLI), демона
dockerdи нижележащих рантаймовcontainerdиrunc, общающихся по стандартам OCI.
Зачем это знать на практике
Пока всё работает, можно жить с моделью «Docker — это магия, которая запускает контейнеры». Но как только сервер тормозит, контейнер не стартует или нужно подключить Kubernetes, эта магия рассыпается. Понимание архитектуры отвечает на вопросы вроде «почему контейнеры пережили перезапуск dockerd?», «что такое containerd, про который пишут в логах k8s?» и «почему один docker build грузит CPU на машине, где я ничего не запускал». Это знание превращает отладку из гадания в чтение конкретного слоя.
Клиент и демон: две разные программы
Когда вы набираете docker run nginx, исполняемый файл docker — это всего лишь клиент. Он не умеет запускать контейнеры. Он переводит вашу команду в HTTP-запрос к REST API демона dockerd и печатает ответ. По умолчанию связь идёт через Unix-сокет.
# посмотреть, к чему обращается клиент
ls -l /var/run/docker.sock
# тот же запрос «вручную», без клиента docker
curl --unix-socket /var/run/docker.sock http://localhost/version
Демон dockerd — это долгоживущий фоновый процесс (служба systemd), который принимает запросы, управляет образами, сетями, томами и делегирует запуск контейнеров вниз. Важное следствие разделения: клиент и демон могут быть на разных машинах. Если выставить переменную окружения, клиент будет управлять удалённым демоном.
# направить локальный клиент на удалённый демон
export DOCKER_HOST=ssh://user@build-server
docker ps # покажет контейнеры на build-server, а не локально
containerd и runc: кто реально запускает контейнер
Сам dockerd тоже не запускает контейнеры напрямую. Он передаёт работу демону containerd — это отдельный высокоуровневый рантайм, который отвечает за загрузку образов, управление их хранением, сеть на уровне контейнера и жизненный цикл. containerd настолько самостоятелен, что его используют и без Docker — например, в Kubernetes.
Когда нужно фактически создать процесс контейнера, containerd вызывает runc — низкоуровневый OCI-runtime. Именно runc делает системные вызовы Linux: создаёт namespaces, настраивает cgroups, монтирует корневую файловую систему и запускает первый процесс. Сделав своё дело, runc завершается — он не остаётся «рядом» с контейнером.
Цепочка делегирования выглядит так:
docker (CLI)
│ HTTP по /var/run/docker.sock
▼
dockerd (демон Docker)
│ gRPC
▼
containerd (управление образами и жизненным циклом)
│ на каждый контейнер
▼
containerd-shim → runc (создаёт namespaces/cgroups и стартует процесс)
▼
ваш процесс (PID 1 внутри контейнера)
Между containerd и процессом стоит ещё containerd-shim — маленький процесс-«нянька». Именно он, а не runc, остаётся родителем контейнера. Благодаря shim контейнеры переживают перезапуск dockerd: демон можно обновить, а работающие контейнеры не упадут, потому что их держит shim.
Что такое OCI
OCI (Open Container Initiative) — это набор открытых спецификаций, чтобы контейнеры были совместимы между разными инструментами. Нас интересуют две из них:
- OCI Image Spec — как устроен образ: это манифест плюс набор слоёв-архивов плюс конфигурация (какую команду запускать, переменные окружения и т.п.). Благодаря этому образ, собранный Docker, запустится в Podman или containerd.
- OCI Runtime Spec — как из распакованного образа запустить контейнер: формат
config.json, описывающего namespaces, точки монтирования, лимиты. Эту спецификацию реализуетrunc.
Различайте образ (статичный артефакт на диске, как .zip с метаданными) и runtime (процесс, который из этого артефакта делает живой контейнер). Образ — «рецепт», runtime — «готовка».
Как это работает под капотом
Контейнер — это не виртуальная машина. У ВМ есть собственное ядро и эмулируемое «железо» через гипервизор; ВМ грузится секунды и съедает сотни мегабайт только на ОС. Контейнер же — это обычный процесс хоста, которому ядро Linux ограничило видимость (namespaces) и ресурсы (cgroups). Своего ядра у контейнера нет — он использует ядро хоста.
# процесс контейнера виден на хосте как обычный процесс
docker run -d --name web nginx
ps -ef | grep nginx # PID nginx есть прямо в списке процессов хоста
Поэтому контейнеры стартуют за миллисекунды и их плотность на сервере несравнимо выше, чем у ВМ. Плата за это — общее ядро: контейнеры менее изолированы, чем виртуальные машины, и запустить ядро другой ОС (например, настоящий Windows-контейнер на Linux-хосте) нельзя.
На том же разделении ролей строится и связка с другими инструментами. Поскольку containerd и runc — самостоятельные компоненты со стандартизированными интерфейсами, Kubernetes давно научился работать с containerd напрямую, минуя dockerd. Для вас как инженера это значит, что навык «читать слои» переносим: образы, собранные Docker, и понимание OCI пригодятся в любой современной контейнерной платформе, а не только в Docker.
Частые ошибки
- Думать, что «docker» — это процесс контейнера. Команда
dockerзавершается сразу после отправки запроса; контейнер держат shim и ваш процесс, а не клиент. - Считать контейнер виртуалкой. Из этого рождаются неверные ожидания: «там же своя ОS, поставлю туда другое ядро/модуль» — нет, ядро общее с хостом.
- Путать containerd и runc.
containerd— долгоживущий менеджер,runc— одноразовый инструмент запуска, который сразу выходит. - Бояться обновлять dockerd при работающих контейнерах. Из-за shim рестарт демона обычно безопасен для уже запущенных контейнеров.
Итоги
- Клиент
dockerтолько отправляет команды; всю работу делает демонdockerd. dockerd→containerd→containerd-shim→runc— цепочка делегирования запуска.- OCI Image Spec описывает формат образа, OCI Runtime Spec — как его запустить; благодаря им экосистема совместима.
- Контейнер — это процесс хоста с урезанной видимостью и ресурсами, а не виртуальная машина: ядро у него общее с хостом.