Код: Выделить всё
docker exec -itКод: Выделить всё
ps auxКод: Выделить всё
hostnameКод: Выделить всё
df -hNamespaces: семь линз, через которые процесс смотрит на систему
Namespace (пространство имён) - это механизм ядра, который оборачивает какой-то глобальный ресурс так, что процессы внутри namespace видят свою изолированную версию этого ресурса. Ключевое слово - "видят". Память физически одна, ядро одно, но каждый процесс приписан к набору namespaces, и через них он смотрит на систему как через линзы. Поменял линзу - поменялась реальность процесса.
Linux namespaces бывают семи типов, и каждый изолирует свою грань:
- pid - дерево процессов. Внутри своего pid namespace первый процесс получает PID 1 и не видит процессы хоста. Поэтому внутри контейнера твой - это PID 1, хотя на хосте он, скажем, PID 24817.
Код: Выделить всё
nginx - net - сетевой стек целиком: интерфейсы, таблицы маршрутизации, iptables/nftables, порты. У контейнера свой , свой
Код: Выделить всё
eth0, своя таблица портов. Два контейнера могут оба слушать порт 80 и не конфликтовать - порты в разных net namespaces.Код: Выделить всё
lo - mnt - точки монтирования. Это та самая "своя файловая система". Контейнер видит свой корень и свои маунты, не видит хоста.
Код: Выделить всё
/home - uts - hostname и domainname. Отсюда случайное имя хоста внутри. UTS расшифровывается как Unix Timesharing System, исторический артефакт названия.
- ipc - межпроцессное взаимодействие: System V IPC, очереди сообщений, разделяемая память POSIX. Чтобы процессы одного контейнера могли общаться через shared memory, а соседний контейнер этого не видел.
- user - отображение UID/GID. Самый важный для безопасности: позволяет быть root (UID 0) внутри namespace, оставаясь непривилегированным пользователем на хосте. Фундамент rootless.
- cgroup - изолирует вид на иерархию cgroups, чтобы процесс внутри не видел полный путь своей cgroup на хосте. Не путай namespace cgroup с самим механизмом cgroups - это разные вещи с похожим именем.
Код: Выделить всё
clone()Код: Выделить всё
CLONE_NEWPIDКод: Выделить всё
CLONE_NEWNETКод: Выделить всё
unshare()Код: Выделить всё
setns()Код: Выделить всё
docker execКод: Выделить всё
nsenterКод: Выделить всё
/proc/PID/ns/
Щупаем namespaces руками: unshare и lsns
Хватит теории. Самый честный способ понять linux namespaces - создать их без всякого Docker. Утилита
Код: Выделить всё
unshareКод: Выделить всё
# создаём новый pid + mount + uts namespace и запускаем там bash
sudo unshare --pid --mount-proc --uts --fork --mount bash
Код: Выделить всё
# меняем hostname - это видно только тут, на хосте имя прежнее
hostname my-mini-container
# смотрим процессы - перемонтировали /proc, видим почти пусто
ps aux
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 0.0 0.0 10100 3200 pts/0 S 12:01 0:00 bash
root 14 0.0 0.0 11400 3400 pts/0 R+ 12:01 0:00 ps aux
Код: Выделить всё
bashКод: Выделить всё
--forkКод: Выделить всё
--mount-procКод: Выделить всё
/procКод: Выделить всё
psКод: Выделить всё
/procКод: Выделить всё
psКод: Выделить всё
/procИз другого терминала на хосте посмотри список namespaces:
Код: Выделить всё
lsns -t pid
NS TYPE NPROCS PID USER COMMAND
4026531836 pid 243 1 root /sbin/init
4026532641 pid 2 24817 root bash
Код: Выделить всё
ls -l /proc/24817/ns/pidcgroups v2: учёт и лимиты ресурсов
Namespaces отвечают на вопрос "что процесс видит". cgroups отвечают на вопрос "сколько процесс может взять". Без cgroups контейнер видел бы урезанный мир, но мог бы сожрать всю память и весь CPU хоста - изоляция видимости без изоляции ресурсов бесполезна в продакшене. Это и есть второй столп cgroups docker.
В 2026 году все актуальные дистрибутивы и Docker Engine работают на cgroups v2 - единой иерархии. В отличие от v1 с её зоопарком отдельных деревьев на каждый контроллер, v2 - одно дерево в
Код: Выделить всё
/sys/fs/cgroup- cpu - доля процессорного времени. Файл задаёт квоту в формате "quota period" в микросекундах.
Код: Выделить всё
cpu.max - memory - оперативная память. - жёсткий потолок,
Код: Выделить всё
memory.max- текущее потребление,Код: Выделить всё
memory.current- мягкий порог с троттлингом.Код: Выделить всё
memory.high - io - дисковый ввод-вывод по блочным устройствам, .
Код: Выделить всё
io.max - pids - максимум процессов/потоков, . Защита от fork-бомбы.
Код: Выделить всё
pids.max
Код: Выделить всё
docker run --memory=512m --cpus=1.5 nginxКод: Выделить всё
# узнаём полный id контейнера
docker inspect -f '{{.Id}}' my-nginx
# смотрим cgroup контейнера (путь через systemd-слайс)
ls /sys/fs/cgroup/system.slice/docker-<ID>.scope/
cat /sys/fs/cgroup/system.slice/docker-<ID>.scope/memory.max
536870912
cat /sys/fs/cgroup/system.slice/docker-<ID>.scope/cpu.max
150000 100000
Код: Выделить всё
memory.max = 536870912Код: Выделить всё
cpu.max = 150000 100000Код: Выделить всё
--cpus=1.5Код: Выделить всё
docker inspectКод: Выделить всё
OOMKilled: trueКод: Выделить всё
docker statsКод: Выделить всё
memory.currentКак Docker собирает контейнер: namespaces плюс cgroups плюс capabilities плюс корень
Теперь склеим картину. Docker сам namespaces и cgroups не создаёт - этим занимается runtime. Docker Engine (демон
Код: Выделить всё
dockerdКод: Выделить всё
containerdКод: Выделить всё
runcКод: Выделить всё
clone()Из чего runc собирает изоляцию контейнеров:
- Namespaces - новые pid/net/mnt/uts/ipc (и user в rootless) для нужной видимости.
- cgroups - cgroup с лимитами cpu/memory/io/pids.
- pivot_root - смена корня на распакованный образ. Это не древний , а более надёжный
Код: Выделить всё
chroot, от которого тяжелее сбежать; mnt namespace делает смену корня приватной.Код: Выделить всё
pivot_root - Capabilities - вместо бинарного "root или не root" Linux дробит привилегии root на ~40 кусочков (capabilities). - право слушать порты ниже 1024,
Код: Выделить всё
CAP_NET_BIND_SERVICE- почти всемогущий и опасный,Код: Выделить всё
CAP_SYS_ADMIN- менять владельца файлов. Docker по умолчанию оставляет контейнеру урезанный набор и выбрасывает опасные. Поэтому контейнерный root - это не хостовый root: ему обрезали capabilities, навесили seccomp-профиль (фильтр системных вызовов) и LSM (AppArmor/SELinux).Код: Выделить всё
CAP_CHOWN
Код: Выделить всё
docker run --rm alpine sh -c 'cat /proc/1/status | grep CapEff'
CapEff: 00000000a80425fb
# расшифровать набор:
capsh --decode=00000000a80425fb
Код: Выделить всё
capshКод: Выделить всё
cap_sys_adminКод: Выделить всё
cap_sys_moduleКод: Выделить всё
--privilegedUser namespace и rootless: root, которого не боишься
Самая интересная линза - user namespace. Она отображает диапазон UID/GID: UID 0 внутри namespace соответствует, например, UID 100000 на хосте. Процесс честно считает себя root, ставит пакеты, владеет файлами образа - но если он сбежит за пределы контейнера, на хосте он окажется бесправным пользователем 100000, который не может ничего.
На этом построен rootless-режим. В классическом Docker сам демон
Код: Выделить всё
dockerdКод: Выделить всё
/etc/subuidКод: Выделить всё
/etc/subgidКод: Выделить всё
slirp4netnsКод: Выделить всё
RootlessKitЧестная оговорка: rootless не серебряная пуля. unprivileged user namespaces - это сами по себе большая поверхность атаки ядра, в 2025-2026 регулярно всплывают CVE, эксплуатирующие именно их (поэтому Ubuntu по умолчанию ограничивает unprivileged userns через AppArmor). И в rootless есть свои ограничения: проброс привилегированных портов, некоторые сетевые сценарии, оверлей-ФС не всегда заводится. Но как слой защиты в supply chain и на многопользовательских хостах rootless резко снижает класс "побег = root хоста".
Типичные грабли и антипаттерны
- ps внутри показывает всё с хоста. Если в pid namespace процессы хоста всё ещё видны - почти всегда забыли перемонтировать (
Код: Выделить всё
/procу unshare). Namespace создан, ноКод: Выделить всё
--mount-procчитает старыйКод: Выделить всё
ps.Код: Выделить всё
/proc - Контейнер ест память хоста. Запустил без - cgroup без
Код: Выделить всё
--memory, потолка нет, упрётся в физическую RAM и потащит за собой OOM killer, который может прибить вообще не тот процесс. В проде всегда ставь лимиты.Код: Выделить всё
memory.max - --privileged "чтобы заработало". Классический антипаттерн. Сначала пойми, какой именно capability нужен, и добавь точечно через . Девяти из десяти задач хватает одного-двух capability вместо полного privileged.
Код: Выделить всё
--cap-add - Путаница cgroups v1 и v2. На старом хосте с v1 пути в другие (отдельные подкаталоги
Код: Выделить всё
/sys/fs/cgroup,Код: Выделить всё
memory), и часть инструментов/туториалов под v2 не сойдётся. Проверяй:Код: Выделить всё
cpuвыдаётКод: Выделить всё
stat -fc %T /sys/fs/cgroupдля v2.Код: Выделить всё
cgroup2fs - "Контейнер - это лёгкая VM". Нет. Ядро общее. Уязвимость ядра пробивает изоляцию всех контейнеров сразу - этого у настоящих VM нет. Для враждебного multi-tenant думают про gVisor, Kata Containers (микро-VM) или отдельные ноды.
Повтори на любом Linux с cgroups v2 (проверь
Код: Выделить всё
stat -fc %T /sys/fs/cgroup- Создай комбинированный namespace: . Внутри смени
Код: Выделить всё
sudo unshare --pid --mount-proc --uts --net --fork bash, выполниКод: Выделить всё
hostnameиКод: Выделить всё
ps aux- убедись, что дерево процессов урезано, а сетевой стек пустой (толькоКод: Выделить всё
ip addrв состоянии DOWN). Это твой "контейнер без образа".Код: Выделить всё
lo - Из второго терминала найди его: и
Код: Выделить всё
lsns -t net, сопоставь inode сКод: Выделить всё
lsns -t uts.Код: Выделить всё
/proc/PID/ns/ - Сделай свою cgroup и положи туда процесс:
Теперь текущий шелл ограничен 100 мегабайтами. Запусти жадный процесс и посмотри, как
Код: Выделить всё
sudo mkdir /sys/fs/cgroup/lab echo "100M" | sudo tee /sys/fs/cgroup/lab/memory.max echo $$ | sudo tee /sys/fs/cgroup/lab/cgroup.procs cat /sys/fs/cgroup/lab/memory.currentрастёт и упирается в потолок.Код: Выделить всё
memory.current - Сравни с настоящим Docker: , затем найди его scope в
Код: Выделить всё
docker run --rm --memory=100m --pids-limit=20 alpine shи прочитайКод: Выделить всё
/sys/fs/cgroup/system.slice/иКод: Выделить всё
memory.max. Убедись, что цифры те же, что ты задал руками. Вывод один: Docker делает ровно то, что ты только что сделал вручную, плюс capabilities, seccomp и pivot_root.Код: Выделить всё
pids.max
Код: Выделить всё
sudo rmdir /sys/fs/cgroup/labКонтрольные вопросы
- Почему после без
Код: Выделить всё
unshare --pid --forkкомандаКод: Выделить всё
--mount-procвсё равно показывает процессы хоста, хотя pid namespace уже создан?Код: Выделить всё
ps - Что произойдёт с процессом внутри контейнера, если он превысит и не сможет освободить память, и где это будет видно в Docker?
Код: Выделить всё
memory.max - Чем root внутри дефолтного контейнера отличается от root на хосте - назови минимум два механизма, которые его ограничивают.
- Как user namespace позволяет процессу быть UID 0 внутри и при этом быть безопасным для хоста, и какой файл задаёт маппинг UID?
Контейнер - не виртуалка и не магия Docker, а обычный процесс хоста, которому ядро Linux подкрутило две вещи: namespaces (что он видит - pid, net, mnt, uts, ipc, user, cgroup) и cgroups v2 (сколько он может взять - cpu, memory, io, pids). Сверху runc накидывает pivot_root, обрезанные capabilities, seccomp и LSM, а user namespace даёт безопасный rootless. Docker, containerd, Podman - это удобные обёртки над одними и теми же системными вызовами ядра. Понимаешь namespaces и cgroups - понимаешь, почему контейнер ведёт себя так, а не иначе, и чинишь проблемы по сути, а не методом тыка.