Що таке Docker daemon і Docker client? Як вони взаємодіють?
Docker daemon і Docker client утворюють класичну client-server пару. Daemon робить реальну роботу; client це фронтенд, що говорить REST. Розуміння цього розпилу дозволяє дебажити «чому CLI висить» або «чому мій Mac спілкується з dockerd всередині VM».
Теорія
TL;DR
- Daemon (
dockerd) = довгоживучий root-процес. Управляє image, container, мережами, volume. Володіє ВСІМ станом. - Client (
dockerCLI) = stateless фронтенд. Переводить твої команди у REST API виклики. - Транспорт = Unix socket (
/var/run/docker.sock) за замовчуванням; TCP socket, якщо ставишDOCKER_HOST=tcp://.... - Формат на проводі = HTTP + JSON. Engine API версіонований (
/v1.46/...); daemon домовляється про версію з клієнтом. - Один до багатьох = можна спрямувати один CLI на кілька daemon через Docker-контексти (
docker context use ...).
Швидкий приклад
# Що `docker ps` насправді робить:
$ curl --unix-socket /var/run/docker.sock \
http://docker/v1.46/containers/json | jq '.[0]'
{
"Id": "a3f9d2b8c1e4...",
"Names": ["/web"],
"Image": "nginx:1.27-alpine",
"Status": "Up 5 minutes",
"Ports": [{ "PrivatePort": 80, "PublicPort": 8080, "Type": "tcp" }]
}Це рівно те, що CLI зробив під капотом, коли ти набрав docker ps. GET у socket; daemon повертає JSON; CLI форматує його у таблицю.
Daemon (dockerd)
Linux-сервіс, зазвичай стартує systemd як root. Відповідальності:
- Управління image: тягне з registry, білдить через BuildKit, зберігає у
/var/lib/docker/. - Container lifecycle: делегує на
containerdіrunc, але володіє user-facing container API. - Мережа: створює віртуальні bridge, керує iptables-правилами, виділяє IP-адреси container.
- Volume: named volume, bind mount, plugin-based storage driver.
- API endpoint: слухає налаштовані socket'и.
Конфігурація живе у /etc/docker/daemon.json і command-line флагах. Типові налаштування: data-root (де зберігати image), storage-driver (overlay2 за замовчуванням), log-driver, dns, registry-mirrors.
Client (docker CLI)
Один бінар, без daemon, без власного стану. Коли ти набираєш команду:
- Парсить аргументи (
docker run -p 8080:80 nginx). - Дивиться на активний контекст (з яким daemon спілкуватися).
- Будує HTTP-запит з відповідними заголовками (
Content-Type: application/json, API-версія). - Шле через socket; читає відповідь.
- Форматує відповідь (таблиця, JSON, кастомний шаблон через
--format).
CLI настільки тонкий, що його можна замінити на curl для будь-якої операції, і багато docs роблять рівно це.
Транспорт: Unix socket vs TCP
За замовчуванням dockerd слухає Unix socket: /var/run/docker.sock. Local-only, швидко, захищено permissions файлу.
$ ls -l /var/run/docker.sock
srw-rw---- 1 root docker 0 Apr 30 10:00 /var/run/docker.sock
# Mode 660: owner=root, group=docker. Щоб юзати docker без sudo,
# додай себе у групу `docker`.Для remote-керування daemon може слухати TCP. Налаштовуєш у daemon.json:
{
"hosts": ["unix:///var/run/docker.sock", "tcp://0.0.0.0:2376"],
"tls": true,
"tlsverify": true,
"tlscacert": "/etc/docker/ca.pem",
"tlscert": "/etc/docker/server-cert.pem",
"tlskey": "/etc/docker/server-key.pem"
}Тоді спрямуй remote-client на нього:
$ DOCKER_HOST=tcp://docker.example.com:2376 docker psЗавжди вмикай TLS для TCP-експозиції. Незахищений tcp://0.0.0.0:2376 це remote root shell, що чекає, коли йому скористаються, Docker API може запустити будь-який container з --privileged, монтувати шляхи з хоста і фактично отримати машину.
Docker-контексти
Один CLI, кілька daemon. Корисно, коли ти керуєш локальним dev плюс віддаленим prod або staging.
$ docker context create staging \
--docker "host=ssh://user@staging.example.com"
$ docker context ls
NAME DESCRIPTION DOCKER ENDPOINT
default Local Docker unix:///var/run/docker.sock
staging ssh://user@staging.example.com
$ docker context use staging
$ docker ps # тепер показує container на staging, через SSHЖодного більше жонглювання DOCKER_HOST=... env-змінними. Перемикання однією командою.
API-версіонування
Engine API версіонований. CLI шле версію типу /v1.46/...; daemon або приймає, або, якщо застара, повертає downgrade hint. Forward-сумісність означає, що клієнт v25 зазвичай говорить з daemon v23 (і навпаки в розумних межах). Mismatch:
$ docker ps
Error response from daemon: client version 1.47 is too new.
Maximum supported API version is 1.45.Фікс: апгрейдь daemon або пінься на клієнт через DOCKER_API_VERSION=1.45.
Типові помилки
Додавати себе у групу docker як security-«зручність»
$ sudo usermod -aG docker $USER
# Тепер можна юзати docker без sudo. Зручно. Також небезпечно.Бути у групі docker еквівалентно root. Daemon API може монтувати шляхи з хоста у container і chmod'ити файли, будь-який unprivileged-юзер з доступом до socket має шлях до root. Для дев-машини нормально. Для multi-user сервера ні.
Експозиція TCP без TLS
Знайдено на безлічі інтернет-доступних серверів. dockerd -H tcp://0.0.0.0:2375 (зверни увагу на нешифрований порт) дає будь-якому інтернет-юзеру можливість запустити privileged container і отримати root хоста. Завжди TLS, завжди client cert auth.
Плутати, що CLI зламано і daemon зламано
$ docker ps
Cannot connect to the Docker daemon at unix:///var/run/docker.sock.
Is the docker daemon running?Помилка чесна: це може бути зупинений daemon (systemctl status docker), відсутній socket-файл або проблема з permissions (ти не у групі docker). Сам CLI рідко зламано; повідомлення показує на daemon.
Рестартити dockerd, щоб щось пофіксити, і випадково повбивати всі container
З дефолтними налаштуваннями на старіших Docker systemctl restart docker вбивав container. Сучасний Docker має live-restore:
// /etc/docker/daemon.json
{ "live-restore": true }З цим увімкненим systemctl restart docker тримає container живими крізь рестарт daemon. Containerd-shim на кожен container це те, що робить це можливим.
Реальне застосування
- Docker Desktop на Mac/Windows: daemon живе всередині маленької Linux VM. Твій
dockerCLI на хості спілкується з ним через forwarded socket. Це бачиш у Docker Desktop > Settings > Resources, де виділяєш RAM і CPU для VM. - CI/CD runners: у кожного runner свій dockerd. Джоби використовують локальний daemon для білду і push image. «Docker-in-Docker» це патерн, де джоба сама крутить
dockerdвсередині privileged-container, але має security-tradeoff. - Remote-host керування: ops-команди використовують
docker contextчерез SSH, щоб адмінити віддалений staging чи prod, не даючи кожному адміну TCP-експозований daemon. - Rootless Docker:
dockerdкрутиться під non-root юзером. Кожен юзер отримує свій daemon і socket. Використовується у HPC-кластерах і безпеково чутливих середовищах.
Питання для поглиблення
Q: Що відбувається, якщо daemon помирає, а container запущені?
A: З увімкненим live-restore (дефолт у сучасному Docker) container продовжують крутитися. Containerd-shim на кожен container їх тримає. CLI не може взаємодіяти з daemon у це вікно, але самі container і їхня мережа продовжують працювати. Коли dockerd рестартує, він re-attach'ується.
Q: Чи можуть два клієнти спілкуватися з тим самим daemon одночасно?
A: Так. Daemon серіалізує операції над shared state, але охоче обробляє багато конкурентних клієнтів. CI-ран і твій локальний docker ps проти того самого daemon працюють паралельно нормально.
Q: Чому моя команда висить на docker ps?
A: Майже завжди проблема з daemon. Або він перевантажений (багато конкурентних операцій), або dead-lock'нувся на storage driver, або застряг на завислому containerd. journalctl -u docker і journalctl -u containerd твої перші зупинки.
Q: Чи можна написати свій Docker-клієнт?
A: Так, Engine API задокументований і стабільний. Офіційний Go SDK (github.com/docker/docker/client) це те, що використовує сам docker CLI. Купа сторонніх інструментів (Lazydocker, Portainer, Dockge) це по суті кастомні клієнти поверх того самого API.
Q: (Senior) Який security-ризик монтування /var/run/docker.sock у container?
A: Монтування socket дає тому container повний контроль над Docker daemon хоста. Зсередини він може запустити будь-який container з --privileged, bind-mount'нути / і фактично стати root на хості. Поширене у CI-інструментах, що мають піднімати білд-container (альтернатива DinD), але відомий supply-chain ризик: зловмисний image з доступом до socket ескалює до root хоста. Використовуй rootless Docker, або sysbox, або per-runner dockerds, щоб обмежити радіус ураження.
Приклади
Виклик Engine API напряму
# Список усіх image
$ curl --unix-socket /var/run/docker.sock \
http://docker/v1.46/images/json | jq '.[].RepoTags'
["nginx:1.27-alpine"]
["postgres:16", "postgres:latest"]
# Створення і старт container у двох API-викликах
$ CID=$(curl --unix-socket /var/run/docker.sock \
-H 'Content-Type: application/json' \
-X POST "http://docker/v1.46/containers/create?name=test" \
-d '{"Image":"alpine","Cmd":["echo","hi"]}' | jq -r .Id)
$ curl --unix-socket /var/run/docker.sock \
-X POST http://docker/v1.46/containers/$CID/startЖодного docker CLI. CLI це зручність; API авторитетний.
Перемикання між локальним і віддаленим
$ docker context create prod-eu --docker "host=ssh://ops@prod-eu.example.com"
$ docker context create prod-us --docker "host=ssh://ops@prod-us.example.com"
$ docker context use prod-eu
$ docker ps # container в EU
$ docker --context prod-us ps # одноразова команда на USОдин CLI, три daemon (local, EU, US). Файл контекстів у ~/.docker/contexts/ тримає роутинг.
Трасування CLI-виклику для підтвердження протоколу
$ DOCKER_DEBUG=true docker ps 2>&1 | head -10
GET /v1.46/containers/json HTTP/1.1
Host: docker
User-Agent: Docker-Client/26.1.0 (linux)
Content-Type: application/json
HTTP/1.1 200 OK
Api-Version: 1.46
Content-Type: application/json
Docker-Experimental: false
Server: Docker/26.1.0 (linux)GET-запит, JSON-відповідь, узгодження версії у заголовках. Це все, що є у протоколі.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.
Коментарі
Ще немає коментарів