namespaces, cgroups или OverlayFS.
Но когато контейнерът не стартира, мрежата не работи, volume-ът се държи странно или Docker започне да консумира дисково пространство,
тогава тези понятия престават да бъдат теория.
1. Защо изобщо ми е нужна архитектурата
В първата част разгледах основната идея зад Docker – контейнерът не е виртуална машина и не трябва да го използвам като такава. Това е важно, но само по себе си не е достатъчно.
Когато изпълня:
docker run -d --name nginx nginx
изглежда така, сякаш Docker прави една проста операция: намира image, създава container и го стартира. В действителност зад тази команда стои цяла верига от компоненти.
командата
Docker Engine
runtime
namespaces · cgroups · filesystem · networking
Тази архитектура е една от причините Docker да бъде толкова удобен за администратора. Аз работя основно с CLI и Compose, а по-ниските слоеве се грижат за изпълнението. Когато обаче нещо се счупи, трябва да мога да проследя пътя от командата до процеса.
2. Docker Engine
Docker Engine е сървърната част на Docker. Той е това, което реално управлява images, containers, networks и volumes.
В Linux среда най-важният процес е dockerd.
Когато напиша:
docker ps
командата не изпълнява сама контейнерите. CLI изпраща заявка към Docker daemon, а daemon-ът връща резултата. Това разделение е фундаментално.
Инструментът, с който работя.
Приема API заявки и управлява ресурсите.
Осигурява реалното изпълнение и изолация.
2.1. Docker daemon – dockerd
На Linux обикновено мога да проверя състоянието на Docker така:
systemctl status dockersystemctl is-active dockerps aux | grep dockerd
Ако daemon-ът не работи, обикновеният Docker CLI няма към кого да изпрати заявката. Това е една от първите проверки, които правя при проблем с Docker.
docker ps връща грешка от типа „Cannot connect to the Docker daemon“,
не започвам да ремонтирам контейнерите. Първо проверявам самия Docker daemon и достъпа до него.
3. Docker CLI
Docker CLI е клиентът, с който давам команди на Docker.
Команди като docker ps, docker pull, docker run,
docker inspect и docker network ls са интерфейсът, който използвам всекидневно.
Важно е да не бъркам CLI с Engine. Мога да имам Docker CLI на една машина и да управлявам Docker daemon на друга, стига да имам подходящ API достъп.
docker version
docker info
docker context ls
docker version е особено полезна команда, защото показва информация както за Client, така и за Server.
Ако двете части не могат да се видят, проблемът вече е локализиран доста по-тясно.
4. Docker API
Между CLI и daemon-а стои Docker API.
CLI не „влиза“ директно в dockerd. То изпраща API заявки.
По подразбиране на Linux комуникацията локално се извършва през Unix socket:
ls -l /var/run/docker.sock
В много системи това е символична връзка към:
/run/docker.sock
Този socket е изключително мощен интерфейс. Достъпът до него на практика означава много високи привилегии върху Docker host.
docker.sock на произволни контейнери и не го публикувам през TCP без сериозна причина.
Контейнер, който може да управлява Docker daemon-а, потенциално може да управлява и самия host.
5. containerd и runc
Тук вече слизаме един слой надолу. Docker daemon не върши сам цялата работа по стартирането на контейнерния процес. В съвременната архитектура Docker използва containerd като container runtime manager, а на по-ниско ниво runc реализира OCI container execution.
Не е необходимо всеки ден да управлявам ръчно тези компоненти. Напротив – обикновено не трябва. Но е важно да знам, че контейнерът не е „магия“, а процес, чието стартиране минава през конкретни слоеве.
5.1. Какво прави containerd
containerd се занимава с жизнения цикъл на контейнерите и управлението на container runtime.
Той е отделен проект и се използва не само от Docker.
Именно това разделяне е една от причините containerd да има значение и извън класическия Docker стек.
5.2. Какво прави runc
runc е ниско ниво OCI runtime.
Той получава описание на контейнера и използва Linux механизмите за създаване на изолирана среда и стартиране на процеса.
Когато казвам „контейнерът е процес“, това вече има съвсем конкретен смисъл. Няма отделна операционна система, която чака някой да я „включи“. Има процес, стартиран с определени ограничения и изолация.
6. Linux Kernel – мястото, където реално се случва всичко
Docker не може да създаде изолация самостоятелно. Той използва възможностите на Linux Kernel. Два от най-важните механизма са namespaces и cgroups.
Към тях трябва да добавя filesystem слоя, networking, capabilities, seccomp и други kernel механизми. Именно комбинацията от тези възможности създава това, което аз възприемам като container.
7. Linux Namespaces
Namespaces изолират изгледа на процесите към определени ресурси на системата. Най-просто казано: два процеса могат да работят на един и същ Kernel, но да виждат различна среда.
| Namespace | Какво изолира | Практически смисъл |
|---|---|---|
| PID | Process IDs | Контейнерът вижда собствено дърво от процеси. |
| NET | Network stack | Собствени interfaces, routes и ports. |
| MNT | Mount points | Собствен изглед към filesystem. |
| UTS | Hostname | Контейнерът може да има собствен hostname. |
| IPC | Inter-process communication | Изолация на IPC ресурси. |
| USER | User/Group IDs | Позволява user ID mapping между host и container. |
| CGROUP | Cgroup view | Изолира изгледа към control groups. |
Например, ако изпълня:
docker run --rm alpine ps
получавам изглед към процесите вътре в контейнера, а не нормалния списък с всички процеси на host системата. Това е една от най-добрите демонстрации на идеята зад container isolation.
8. cgroups – контрол върху ресурсите
Ако namespaces определят какво вижда процесът, cgroups определят до голяма степен какви ресурси може да използва.
Това е особено важно при production системи. Не искам един контейнер да изяде цялата RAM и да превърне останалите услуги в жертва.
Docker може да задава ограничения за CPU, memory и други ресурси.
docker run -d \
--name test-nginx \
--memory=512m \
--cpus=1.0 \
nginx
Тук давам на контейнера ограничение от 512 MB memory и приблизително един CPU. Това не означава, че контейнерът автоматично ще използва точно толкова. Означава, че задавам контрол върху максималното потребление според конкретната настройка.
9. Docker Images и Layers
Един от най-важните архитектурни принципи на Docker е, че image не е един монолитен файл. Image се изгражда от слоеве (layers).
Например един опростен Dockerfile:
FROM debian:bookworm
RUN apt-get update && apt-get install -y nginx
COPY nginx.conf /etc/nginx/nginx.conf
CMD ["nginx", "-g", "daemon off;"]
Всяка инструкция, която променя filesystem-а, може да доведе до нов layer. Именно това позволява повторното използване на вече съществуващи слоеве и прави image cache толкова важен.
Слоевете на image са по същество неизменяеми. Когато container стартира, Docker добавя writable layer за конкретния container. Точно тук се появява една много важна практическа последица: не трябва да разчитам на writable layer за постоянни данни.
10. OverlayFS и файловата система на контейнера
На Linux Docker обичайно използва storage driver, базиран на OverlayFS, когато средата го позволява. Идеята е няколко filesystem слоя да бъдат представени като един общ изглед.
От моя гледна точка това изглежда просто:
read-only
writable
Това е изключително удобно, но има цена. Ако започна да записвам огромни количества данни директно във writable layer на контейнера, губя част от предимствата на модела и усложнявам управлението на данните.
11. Къде тогава живеят данните?
Тук идват volumes и bind mounts. Те отделят жизнения цикъл на данните от жизнения цикъл на контейнера.
11.1. Docker volume
docker volume create mariadb-data
docker volume ls
docker volume inspect mariadb-data
11.2. Bind mount
docker run -d \
--name nginx \
-v /srv/nginx/conf:/etc/nginx/conf.d:ro \
nginx
Разликата между volume и bind mount ще бъде разгледана подробно в отделна част. Тук е достатъчно да запомня основния принцип:
12. Docker Networking
Контейнерите не получават просто „някакъв IP адрес“. Docker създава мрежова среда, в която контейнерите могат да комуникират според избраната network конфигурация.
При стандартната Docker инсталация най-често ще видя:
docker network ls
Типичен резултат:
NETWORK ID NAME DRIVER SCOPE
xxxxxx bridge bridge local
xxxxxx host host local
xxxxxx none null local
В реални Compose проекти обикновено създавам собствени bridge networks. Това ми позволява да разделя услугите логически и да контролирам кой с кого комуникира.
nginx / proxy
WordPress / app
MariaDB / PostgreSQL
В Compose мога да имам например:
services:
web:
image: nginx:alpine
networks:
- frontend
db:
image: mariadb:11
networks:
- backend
app:
image: myapp:latest
networks:
- frontend
- backend
networks:
frontend:
backend:
Така app може да вижда и двата сегмента, докато web няма директна връзка към database network.
Това е прост пример, но показва защо Docker networking е архитектурен инструмент, а не просто начин да получа IP адрес.
13. Жизнен цикъл на контейнера
Един container има жизнен цикъл. Той не е равнозначен на image. Това разграничение е критично.
Мога да имам един image и много containers от него.
docker run -d --name web1 nginx
docker run -d --name web2 nginx
docker run -d --name web3 nginx
И трите контейнера използват един и същ image, но са различни runtime обекти. Всеки има собствено име, metadata, network attachment и writable layer.
14. Как проверявам какво реално се случва
Когато нещо не ми хареса в Docker, не гадая.
Използвам inspect, логове и системните инструменти.
docker ps
docker ps -a
docker inspect <container>
docker logs <container>
docker stats
docker network inspect <network>
docker volume inspect <volume>
docker image inspect <image>
Например:
docker inspect nginx
оттам мога да видя mounts, networks, environment, command, restart policy, IP адреси, image ID и множество други параметри.
docker inspect често ми дава повече информация от десет опита да „оправя“ контейнера на сляпо.
15. Какво всъщност става при docker run
Нека разгледам една проста команда:
docker run -d --name web nginx
Опростено процесът изглежда така:
- Docker CLI приема командата.
- CLI изпраща заявка към Docker daemon.
- Daemon проверява дали image
nginxе наличен. - Ако не е наличен, image се изтегля от registry.
- Docker създава metadata за новия container.
- Създава се необходимата filesystem структура.
- Настройват се namespaces и cgroups.
- Конфигурира се network.
- При необходимост се монтират volumes или bind mounts.
- container runtime стартира процеса.
- Docker връща container ID.
Това е причината командата да изглежда проста, въпреки че под нея има доста работа.
16. Изолация не означава абсолютна сигурност
Тук трябва да бъда особено внимателен. Контейнерът не е магическа защитна стена и не трябва да го приемам като абсолютна security boundary.
Docker използва редица механизми за ограничаване на правата, включително Linux capabilities, seccomp и различни filesystem и namespace ограничения. Но конфигурацията има значение.
Например:
docker run --privileged ...
--privileged не е „още малко права“.
Това е сериозна промяна на security модела на контейнера.
Използвам го само когато имам конкретна причина и разбирам последствията.
Същото важи и за директното монтиране на чувствителни host директории и за предоставянето на Docker socket.
17. Къде се появява Docker Compose
Docker CLI е прекрасен инструмент за управление на отделни контейнери. Но когато приложението ми се състои от пет, десет или тридесет услуги, започвам да имам друг проблем: трябва да помня как точно съм ги създал.
Именно тук идва Docker Compose.
Вместо:
docker network create app-net
docker volume create db-data
docker run ...
docker run ...
docker run ...
docker run ...
описвам желаното състояние в YAML файл:
services:
app:
image: nginx:alpine
ports:
- "8080:80"
db:
image: mariadb:11
environment:
MARIADB_ROOT_PASSWORD: example
redis:
image: redis:alpine
После:
docker compose up -d
Compose не отменя архитектурата на Docker. То е инструмент върху нея. Това разграничение ще стане много важно в следващите части.
18. Моят модел за мислене
Когато работя с Docker, държа в главата си следната последователност:
какво ще бъде стартирано
конкретна runtime инстанция
как комуникира
къде са данните
как се управлява
Ако имам проблем, първо определям в кой слой е.
| Симптом | Първо проверявам |
|---|---|
Cannot connect to Docker daemon |
Docker service, socket, permissions, context |
| Container се рестартира постоянно | docker logs, exit code, command, healthcheck |
| Приложението няма достъп до DB | Docker network, service name, port, credentials |
| Данните изчезват | Volume / bind mount / writable layer |
| Дискът се запълва | Images, containers, volumes, logs, build cache |
| Контейнерът е бавен | CPU, memory, I/O, storage driver, приложение |
19. Грешки, които вече не си позволявам
- Не пазя важни данни само във filesystem layer на контейнера.
- Не редактирам произволно конфигурации вътре в контейнер и после да забравя какво съм променил.
- Не използвам
--privilegedбез конкретна причина. - Не публикувам Docker API към интернет.
- Не давам Docker socket на контейнер, без да разбирам security последствията.
- Не диагностицирам чрез налучкване.
- Не приемам, че „container running“ означава „application healthy“.
- Не бъркам image, container, volume и network – те са различни обекти с различен жизнен цикъл.
20. Най-важното от тази част
Ако трябва да запомня само няколко неща от тази глава, те са следните.
- Docker CLI е инструментът, с който давам команди.
- Docker daemon управлява Docker ресурсите.
- containerd и runc участват в реалното изпълнение на контейнерите.
- Linux Kernel осигурява основните механизми за изолация и контрол.
- Namespaces изолират изгледа на процесите и ресурсите.
- cgroups позволяват контрол върху ресурсите.
- Images са основата, от която се създават containers.
- Volumes и bind mounts отделят данните от жизнения цикъл на контейнера.
- Networks определят комуникацията между услугите.
- Compose описва желаното състояние на многоконтейнерни приложения.
Заключение
Docker изглежда прост, докато го използвам само с няколко команди. Когато започна да поддържам реална инфраструктура, простотата отпред вече не означава простота отдолу.
И точно това е хубавото. Не е необходимо да управлявам всеки слой ръчно. Не е необходимо да знам вътрешната реализация на всеки компонент наизуст. Но когато нещо се обърка, трябва да знам къде да търся.
За мен това е разликата между човек, който използва Docker, и човек, който администрира Docker.
В следващата част ще сляза още едно ниво надолу и ще разгледам Docker Images: как се създават, как работят layers, какво представлява image cache, защо редът на инструкциите в Dockerfile има значение и как една на пръв поглед дребна промяна може да направи build процеса многократно по-бавен или по-бърз.