Коли ви виконуєте команду docker run nginx, на екрані з'являється повідомлення про завантаження образу, а через кілька секунд веб-сервер Nginx вже працює у контейнері. Цей процес здається простим та майже магічним. Але що насправді відбувається між моментом натискання Enter та запуском контейнера?
За лаконічним інтерфейсом Docker CLI ховається складна багатошарова архітектура, в якій кожен компонент виконує чітко визначену роль. Розуміння цієї архітектури критично важливе не лише для задоволення інтелектуальної цікавості, але й для ефективного використання Docker, діагностики проблем та прийняття обґрунтованих рішень щодо конфігурації.
У цій статті ми детально розглянемо внутрішню будову Docker Engine — від клієнтської команди до низькорівневого створення контейнера на рівні ядра Linux. Ми простежимо повний шлях виконання команди docker run через усі шари архітектури, зрозумієм роль кожного компонента та побачимо, як Docker взаємодіє з операційною системою.
Docker побудований за класичною клієнт-серверною моделлю, де клієнт та сервер є окремими процесами, які спілкуються через добре визначений інтерфейс. Ця архітектурна модель не є унікальною для Docker — вона використовується в безлічі систем, від баз даних (PostgreSQL, MySQL) до веб-серверів (Apache, Nginx). Але в контексті Docker вона має особливе значення.
Архітектура Docker складається з трьох основних компонентів, які взаємодіють один з одним:
Docker Client (docker) — це інструмент командного рядка, з яким безпосередньо взаємодіє користувач. Коли ви виконуєте команду docker run, docker build або docker ps, ви працюєте саме з клієнтом. Клієнт сам по собі не виконує жодних операцій з контейнерами — він лише формує запити та відправляє їх серверу.
Docker Daemon (dockerd) — це фоновий процес (daemon), який виконує всю важку роботу: управління образами, створення та запуск контейнерів, налаштування мереж, управління томами. Демон постійно працює у фоновому режимі, очікуючи на команди від клієнтів.
Docker REST API — це інтерфейс, через який клієнт спілкується з демоном. API визначає набір HTTP-ендпоінтів для кожної операції: створення контейнера, завантаження образу, перегляд логів тощо. Використання REST API означає, що клієнт та демон можуть працювати на різних машинах — клієнт на вашому ноутбуці може керувати демоном на віддаленому сервері.

Щоб краще зрозуміти клієнт-серверну модель Docker, уявімо ресторан:
Коли ви кажете офіціанту "Принесіть мені пасту карбонара" (аналог docker run nginx), офіціант не біжить на кухню готувати — він передає замовлення через стандартизовану систему (меню/API), і кухня виконує всю роботу. Ви отримуєте готову страву (запущений контейнер), не знаючи деталей приготування.
Розділення клієнта та сервера дає кілька важливих переваг:
Віддалене управління: Docker CLI на вашому ноутбуці може керувати демоном на сервері в іншому дата-центрі. Достатньо вказати змінну оточення DOCKER_HOST, і всі команди будуть виконуватися віддалено.
Множинні клієнти: Один демон може обслуговувати кілька клієнтів одночасно. Розробник може виконувати команди через CLI, тоді як CI/CD система взаємодіє з тим самим демоном через API.
Різні інтерфейси: Окрім CLI, існують графічні інтерфейси (Docker Desktop, Portainer), бібліотеки для різних мов програмування (Python docker-py, Go moby/moby/client), які використовують той самий API.
Безпека: Демон працює з підвищеними привілеями (зазвичай від root), тоді як клієнт може працювати від звичайного користувача. Доступ до демона контролюється через права на Unix socket або TLS-сертифікати для мережевого доступу.
docker context ls. За замовчуванням це локальний Unix socket, але можна налаштувати віддалені контексти для управління кількома Docker-хостами.Docker Daemon (dockerd) — це центральний компонент Docker Engine, який виконує всю важку роботу. Це довготривалий процес (long-running daemon), який запускається при завантаженні системи та працює у фоновому режимі, очікуючи на команди.
Демон відповідає за управління всіма аспектами життєвого циклу контейнерів:
Управління образами: завантаження образів з реєстрів (Docker Hub, приватні реєстри), зберігання їх локально, побудова нових образів з Dockerfile, видалення непотрібних образів.
Управління контейнерами: створення контейнерів з образів, запуск та зупинка контейнерів, моніторинг їхнього стану, збір логів, виконання команд всередині працюючих контейнерів.
Управління мережами: створення віртуальних мереж для контейнерів, налаштування DNS-резолюції між контейнерами, проброс портів з хоста в контейнер, ізоляція мережевого трафіку.
Управління томами: створення та видалення томів для збереження даних, монтування томів у контейнери, управління життєвим циклом даних незалежно від контейнерів.
Взаємодія з іншими демонами: Docker Daemon може спілкуватися з іншими демонами для реалізації розподілених сценаріїв (наприклад, Docker Swarm, хоча це за межами нашого курсу).
Коли ви запускаєте Docker Daemon командою dockerd, він виконує кілька ініціалізаційних кроків:
/var/run/docker.sock (на Linux) або named pipe на Windows. Це точка входу для всіх клієнтів./etc/docker/daemon.json (якщо він існує), де можна налаштувати параметри роботи демона.Після ініціалізації демон працює в нескінченному циклі, обробляючи запити від клієнтів. Кожен запит обробляється в окремій горутині (Go concurrency primitive), що дозволяє демону обслуговувати багато клієнтів одночасно.
Поведінку Docker Daemon можна налаштувати через файл /etc/docker/daemon.json. Це JSON-файл, який містить параметри конфігурації:
{
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "3"
},
"storage-driver": "overlay2",
"default-address-pools": [
{
"base": "172.17.0.0/16",
"size": 24
}
]
}
Цей файл дозволяє централізовано керувати параметрами, які інакше довелося б передавати через прапорці командного рядка при кожному запуску dockerd.
daemon.json вимагають перезапуску Docker Daemon. Це означає, що всі запущені контейнери будуть зупинені (якщо не налаштовано live restore). Плануйте зміни конфігурації в періоди низького навантаження.Docker REST API — це серце комунікації між клієнтом та демоном. Це HTTP-based API, який визначає набір ендпоінтів для кожної операції з Docker-об'єктами.
API організований навколо ресурсів (resources), кожен з яких представляє певний тип об'єкта:
/containers — операції з контейнерами/images — операції з образами/networks — операції з мережами/volumes — операції з томами/system — системна інформація та подіїКожен ресурс підтримує стандартні HTTP-методи:
GET /containers/json повертає список контейнерів)POST /containers/create створює новий контейнер)DELETE /containers/{id} видаляє контейнер)На Linux-системах Docker CLI та Daemon спілкуються через Unix domain socket — спеціальний тип сокета, який працює через файлову систему замість мережевого стека. За замовчуванням цей socket знаходиться за адресою /var/run/docker.sock.
Unix socket має кілька переваг над TCP-сокетом:
Продуктивність: комунікація через Unix socket швидша, оскільки не потрібно проходити через мережевий стек ядра.
Безпека: доступ до socket контролюється через файлові права. Тільки користувачі з правами на читання/запис до /var/run/docker.sock можуть взаємодіяти з демоном.
Простота: не потрібно налаштовувати порти, firewall, TLS-сертифікати для локальної комунікації.
Ви можете безпосередньо взаємодіяти з Docker API через curl, минаючи Docker CLI:
# Отримати список контейнерів
curl --unix-socket /var/run/docker.sock http://localhost/v1.43/containers/json
# Створити контейнер
curl --unix-socket /var/run/docker.sock \
-H "Content-Type: application/json" \
-d '{"Image": "nginx", "HostConfig": {"PortBindings": {"80/tcp": [{"HostPort": "8080"}]}}}' \
-X POST http://localhost/v1.43/containers/create
# Запустити контейнер
curl --unix-socket /var/run/docker.sock \
-X POST http://localhost/v1.43/containers/{container_id}/start
v1.43 у прикладі) відповідає версії Docker Engine. Docker підтримує зворотну сумісність — новіші версії демона можуть обробляти запити від старіших клієнтів. Поточна версія API завжди доступна через docker version.Для управління віддаленими Docker-хостами можна налаштувати демон на прослуховування TCP-порту замість (або на додаток до) Unix socket. Це дозволяє виконувати команди на віддаленому сервері:
# На віддаленому сервері: запустити демон з TCP listener
dockerd -H tcp://0.0.0.0:2375
# На локальній машині: підключитися до віддаленого демона
export DOCKER_HOST=tcp://remote-server:2375
docker ps # Ця команда виконається на віддаленому сервері
Docker Daemon не створює контейнери безпосередньо. Замість цього він делегує цю роботу containerd — високорівневому контейнерному runtime, який управляє життєвим циклом контейнерів.
containerd спочатку був частиною Docker Engine, але в 2016 році Docker Inc. виділила його в окремий проєкт та передала під управління Cloud Native Computing Foundation (CNCF). Це рішення мало кілька цілей:
Сьогодні containerd використовується не тільки Docker, але й:
containerd відповідає за:
Управління життєвим циклом контейнера: створення, запуск, зупинка, видалення контейнерів.
Управління образами: завантаження образів з реєстрів, розпакування шарів, зберігання образів локально.
Управління snapshots: створення знімків файлової системи для контейнерів (через драйвери на кшталт overlayfs).
Управління мережею та storage: налаштування мережевих інтерфейсів контейнера, монтування томів.
Моніторинг та метрики: збір інформації про стан контейнерів, споживання ресурсів.
containerd надає gRPC API, через який Docker Daemon взаємодіє з ним. Коли демон отримує запит на створення контейнера, він формує gRPC-виклик до containerd, передаючи специфікацію контейнера.
containerd займає проміжну позицію між Docker Daemon (високорівневий інтерфейс для користувачів) та runc (низькорівневий інструмент для створення контейнерів). Він абстрагує складність роботи з низькорівневими деталями, надаючи зручний API для управління контейнерами.
Аналогія: якщо Docker Daemon — це диспетчер таксі, який приймає замовлення від клієнтів, то containerd — це менеджер автопарку, який керує конкретними автомобілями (контейнерами), призначає водіїв (процеси), стежить за станом машин.
ctr, який постачається разом з containerd. Це корисно для діагностики проблем на низькому рівні, але для повсякденної роботи використовуйте Docker CLI.На самому дні архітектурного стека знаходиться runc — низькорівневий контейнерний runtime, який безпосередньо взаємодіє з ядром Linux для створення контейнерів. Якщо containerd — це менеджер, то runc — це виконавець, який робить "брудну роботу".
runc — це CLI-інструмент для створення та запуску контейнерів відповідно до специфікації OCI (Open Container Initiative). Він написаний на Go та є еталонною реалізацією OCI Runtime Specification.
runc не є демоном — це звичайна програма, яка запускається, створює контейнер та завершується. Кожен контейнер створюється окремим викликом runc. containerd викликає runc для кожного контейнера, передаючи йому конфігурацію у форматі JSON.
Коли containerd просить runc створити контейнер, runc виконує наступні низькорівневі операції:
Створює Linux namespaces: викликає системні виклики clone() або unshare() для створення ізольованих просторів імен (PID, NET, MNT, UTS, IPC, USER).
Налаштовує cgroups: створює cgroup для контейнера та встановлює ліміти на CPU, пам'ять, I/O відповідно до специфікації.
Налаштовує файлову систему: монтує rootfs контейнера (зазвичай через overlayfs), створює необхідні точки монтування (/proc, /sys, /dev).
Налаштовує мережу: створює мережеві інтерфейси, налаштовує IP-адреси, маршрутизацію (хоча часто це робить окремий CNI-плагін).
Застосовує security profiles: налаштовує SELinux, AppArmor, seccomp для обмеження можливостей контейнера.
Запускає процес: виконує exec() для запуску головного процесу контейнера (того, що вказано в CMD або ENTRYPOINT Dockerfile).
Після створення контейнера runc може або залишитися як батьківський процес (foreground mode), або передати контроль containerd та завершитися (detached mode).
runc реалізує OCI Runtime Specification — відкритий стандарт, який визначає:
config.json, який описує всі параметри контейнера (namespaces, cgroups, mounts, process)Завдяки OCI стандарту, runc можна замінити на інші OCI-сумісні runtime (наприклад, crun — реалізація на C, kata-runtime — контейнери у легковісних VM). containerd працюватиме з будь-яким runtime, який дотримується специфікації.
crun (швидший, написаний на C), youki (написаний на Rust), kata-runtime (контейнери у мікро-VM для додаткової ізоляції). Але runc залишається найпопулярнішим та найстабільнішим.Open Container Initiative (OCI) — це організація під егідою Linux Foundation, яка розробляє відкриті стандарти для контейнерних технологій. OCI була заснована в 2015 році Docker, CoreOS, Google, IBM, Microsoft та іншими лідерами індустрії.
OCI визначає три стандарти, які разом забезпечують повну інтероперабельність контейнерних технологій:
Image Specification визначає формат контейнерних образів. Образ складається з:
Завдяки цій специфікації, образ, створений Docker, можна запустити в Podman, containerd або будь-якому іншому OCI-сумісному runtime.
Runtime Specification визначає, як контейнер має бути створений та запущений. Вона описує:
config.json — конфігурація контейнера (namespaces, cgroups, mounts, capabilities)runc є еталонною реалізацією цієї специфікації.
Distribution Specification визначає, як образи мають розповсюджуватися через реєстри (registries). Вона описує:
Docker Hub, GitHub Container Registry, Google Container Registry — всі вони реалізують OCI Distribution Spec.
До появи OCI кожен вендор мав власний формат контейнерів. Образ, створений для Docker, не працював у rkt (контейнерний runtime від CoreOS). Це створювало vendor lock-in та фрагментацію екосистеми.
OCI вирішила цю проблему, створивши відкриті стандарти, які підтримуються всіма основними гравцями. Тепер:
Стандартизація знизила бар'єри входу для нових інструментів та забезпечила довгострокову стабільність екосистеми.
docker build, результат автоматично відповідає OCI Image Specification. Вам не потрібно робити нічого додатково — Docker вже дотримується стандартів.Тепер, коли ми розглянули всі компоненти архітектури, простежимо повний шлях виконання команди docker run nginx через усі шари системи.
Користувач виконує команду:
docker run -d -p 8080:80 --name my-nginx nginx:latest
Docker CLI парсить аргументи командного рядка:
-d — detached mode (запустити у фоновому режимі)-p 8080:80 — пробросити порт 80 контейнера на порт 8080 хоста--name my-nginx — ім'я контейнераnginx:latest — образ для запускуCLI перетворює команду на серію HTTP-запитів до Docker API:
Перевірка наявності образу: GET /images/nginx:latest/json
POST /images/create?fromImage=nginx&tag=latest для завантаження з Docker HubСтворення контейнера: POST /containers/create?name=my-nginx
{
"Image": "nginx:latest",
"HostConfig": {
"PortBindings": {
"80/tcp": [{"HostPort": "8080"}]
}
}
}
Запуск контейнера: POST /containers/{id}/start
Ці запити відправляються через Unix socket /var/run/docker.sock.
Docker Daemon отримує API-запити та виконує відповідні дії:
Перевіряє образ: шукає nginx:latest у локальному сховищі образів. Якщо образ відсутній, демон завантажує його з Docker Hub (або іншого налаштованого registry).
Створює конфігурацію контейнера: демон формує повну специфікацію контейнера, включаючи:
Викликає containerd: демон відправляє gRPC-запит до containerd з проханням створити контейнер.
containerd отримує запит від Docker Daemon та виконує підготовчі операції:
Розпаковує образ: containerd бере шари образу nginx:latest та створює snapshot файлової системи через драйвер overlayfs. Це створює об'єднану файлову систему з усіх шарів образу.
Створює bundle: containerd формує OCI bundle — директорію, яка містить:
config.json — OCI Runtime Specification для контейнераrootfs/ — кореневу файлову систему контейнера (змонтовані шари образу)Налаштовує мережу: containerd викликає CNI (Container Network Interface) плагіни для створення мережевих інтерфейсів контейнера, призначення IP-адреси, налаштування маршрутизації.
Викликає runc: containerd запускає runc з шляхом до OCI bundle:
runc create --bundle /var/run/containerd/io.containerd.runtime.v2.task/moby/abc123 abc123
runc виконує низькорівневі системні виклики для створення ізольованого середовища:
Створює namespaces: викликає clone() з прапорцями CLONE_NEWPID | CLONE_NEWNET | CLONE_NEWNS | CLONE_NEWUTS | CLONE_NEWIPC | CLONE_NEWUSER для створення ізольованих просторів імен.
Налаштовує cgroups: створює cgroup для контейнера в /sys/fs/cgroup/ та встановлює ліміти ресурсів (CPU, memory, I/O).
Монтує файлову систему: виконує mount() для створення rootfs контейнера через overlayfs, монтує /proc, /sys, /dev всередині контейнера.
Налаштовує capabilities: обмежує можливості процесу контейнера, видаляючи небезпечні capabilities (наприклад, CAP_SYS_ADMIN).
Застосовує seccomp: встановлює seccomp-фільтр, який обмежує системні виклики, доступні процесу контейнера.
Запускає процес: виконує exec() для запуску головного процесу контейнера — в нашому випадку це nginx з PID 1 всередині контейнера.
Після успішного створення:
Контейнер тепер працює як ізольований процес на хості. Nginx слухає на порту 80 всередині контейнера, а Docker Daemon налаштував iptables правила для проброса трафіку з порту 8080 хоста на порт 80 контейнера.

Ця діаграма показує повний шлях від команди користувача до запущеного контейнера. Кожен компонент виконує свою роль, і разом вони створюють ілюзію легковісної віртуальної машини, хоча насправді це просто ізольований процес на хості.
Архітектура Docker Engine — це багатошарова система, де кожен компонент має чітко визначену відповідальність:
Docker CLI — інтерфейс користувача, який перетворює команди на API-запити.
Docker REST API — стандартизований інтерфейс для комунікації між клієнтом та демоном.
Docker Daemon — центральний компонент, який управляє образами, контейнерами, мережами та томами.
containerd — високорівневий runtime, який управляє життєвим циклом контейнерів та взаємодіє з низькорівневим runtime.
runc — низькорівневий runtime, який безпосередньо створює контейнери через системні виклики Linux (namespaces, cgroups).
OCI — набір відкритих стандартів, які забезпечують інтероперабельність між різними контейнерними технологіями.
Ця модульна архітектура дозволяє:
Розуміння цієї архітектури критично важливе для діагностики проблем, оптимізації продуктивності та прийняття обґрунтованих рішень щодо конфігурації Docker у продакшен-середовищах.
У наступній статті ми перейдемо від теорії до практики та розглянемо процес встановлення Docker на різних операційних системах.
Виконайте наступні команди для дослідження Docker Socket:
# Перевірте, чи існує Docker socket
ls -la /var/run/docker.sock
# Подивіться права доступу
stat /var/run/docker.sock
# Спробуйте виконати API-запит через curl
curl --unix-socket /var/run/docker.sock http://localhost/version
Питання для роздумів:
Запустіть контейнер та дослідіть процеси на хості:
# Запустіть контейнер
docker run -d --name test-nginx nginx
# Знайдіть процеси, пов'язані з контейнером
ps aux | grep nginx
# Подивіться ієрархію процесів
pstree -p | grep containerd
Питання:
Створіть власну діаграму архітектури Docker, яка показує:
Використайте будь-який інструмент (Mermaid, draw.io, навіть папір та олівець).
Дослідіть архітектуру Podman та порівняйте з Docker:
| Аспект | Docker | Podman |
|---|---|---|
| Наявність демона | Так (dockerd) | ? |
| Низькорівневий runtime | runc | ? |
| Високорівневий runtime | containerd | ? |
| Комунікація клієнт-сервер | REST API через socket | ? |
Додаткове питання: Які переваги та недоліки daemonless архітектури Podman порівняно з daemon-based архітектурою Docker?