Обзор
Контейнеры в Linux — не отдельная технология, а комбинация двух ядерных механизмов: namespaces (изоляция ресурсов) и cgroups (ограничение ресурсов). Docker, Podman, LXC и другие — лишь userspace-инструменты, которые оркестрируют эти примитивы ядра, добавляя image management, сетевую конфигурацию и удобный CLI.
Принципиальное отличие контейнеров от виртуальных машин: контейнер разделяет ядро хоста. Нет гипервизора, нет эмуляции оборудования, нет отдельной копии ядра. Это даёт минимальный overhead при запуске (миллисекунды vs секунды для VM), но и ограничивает изоляцию — контейнер не может использовать другое ядро или другую ОС. В FreeBSD аналог контейнеров — jails, появившиеся в 2000 году (за 13 лет до Docker), но без инструментария для image management и оркестрации.
Namespaces: изоляция ресурсов
Namespace партиционирует ядерный ресурс так, что процессы в одном namespace видят свою копию ресурса, а процессы в другом — свою. Каждый процесс принадлежит ровно одному экземпляру каждого типа namespace. Текущие namespaces процесса видны как симлинки в /proc/PID/ns/.
Linux (6.x) предоставляет 8 типов namespaces:
PID (с ядра 2.6.24) — изоляция дерева процессов. Первый процесс в новом PID namespace получает PID 1 и становится «init» этого namespace: orphaned процессы перепривязываются к нему, его завершение убивает все процессы в namespace. PID namespaces вложенные: процесс виден в своём namespace и во всех родительских (с разными PID).
NET (2.6.29) — изолированный сетевой стек: собственные интерфейсы, IP-адреса, маршруты, iptables/nftables-правила, сокеты, connection tracking. Физический интерфейс может принадлежать только одному namespace; для связи между namespaces используются veth-пары (подробнее — статья 3).
MNT (2.4.19, самый первый namespace) — изолированное дерево mount points. Контейнер видит свою файловую систему; mount/umount внутри не затрагивает хост.
UTS (2.6.19) — изоляция hostname и NIS domain name. Каждый контейнер может иметь свой hostname.
IPC (2.6.19) — изоляция System V IPC (shared memory, semaphores, message queues) и POSIX message queues.
USER (3.8) — маппинг UID/GID: root (UID 0) внутри контейнера может быть маппирован на непривилегированного пользователя (например, UID 100000) на хосте. Фундамент rootless-контейнеров. Единственный namespace, не требующий CAP_SYS_ADMIN для создания, — но дистрибутивы это ограничивают. Апстримный рычаг — sysctl user.max_user_namespaces (ноль запрещает создание вовсе); kernel.unprivileged_userns_clone — Debian-специфичная заплатка, а Ubuntu с 24.04 по умолчанию режет непривилегированные user namespaces средствами AppArmor (kernel.apparmor_restrict_unprivileged_userns).
CGROUP (4.6) — изолирует видимость cgroup-иерархии: процесс в контейнере видит свою cgroup как корневую, не зная о cgroup-структуре хоста.
TIME (5.6) — изоляция CLOCK_MONOTONIC и CLOCK_BOOTTIME. Позволяет контейнеру иметь другое «время с момента загрузки».
Syscalls
Три системных вызова управляют namespaces: clone() (создать дочерний процесс в новых namespaces, с флагами CLONE_NEWPID, CLONE_NEWNET и т.д.), unshare() (текущий процесс переходит в новые namespaces), setns() (процесс входит в существующий namespace по файловому дескриптору).
Userspace-утилиты unshare и nsenter (из util-linux) — обёртки над этими syscalls:
# Создать изолированное окружение вручную (минимальный «контейнер»)
sudo unshare --fork --pid --mount-proc --uts --ipc --net /bin/bash
# Внутри: PID 1, свой hostname, изолированная сеть, свои mount points
# Войти в namespaces существующего контейнера
nsenter --target <PID> --mount --uts --ipc --net --pid /bin/bash
# Или через docker/podman:
docker exec -it <container> /bin/bash
Cgroups: ограничение ресурсов
Namespaces изолируют видимость, cgroups ограничивают потребление. Control groups — механизм ядра для учёта и лимитирования CPU, памяти, I/O, количества процессов для группы процессов.
cgroup v1 vs v2
cgroup v1 (с 2.6.24) — каждый контроллер (cpu, memory, blkio, pids) монтируется как отдельная иерархия. Процесс мог принадлежать разным cgroup в разных иерархиях. Сложно, непоследовательно, с проблемами в corner cases (например, memory accounting). В ядре механизм ещё есть, но userspace от него отказался: systemd 258 (сентябрь 2025) удалил поддержку legacy- и hybrid-иерархий целиком, так что загрузка с systemd.unified_cgroup_hierarchy=0 больше не работает.
cgroup v2 (с 4.5, unified hierarchy) — единая иерархия, все контроллеры в одном дереве. Смонтирована в /sys/fs/cgroup/. Современные дистрибутивы (Fedora 31+, Ubuntu 21.10+, Debian 11+, RHEL 9) по умолчанию используют v2. Проверка: stat -fc %T /sys/fs/cgroup — cgroup2fs означает v2.
Контроллеры cgroup v2
Взаимодействие через файлы в /sys/fs/cgroup/:
# Создать cgroup
mkdir /sys/fs/cgroup/myapp
# Включить контроллеры для дочерних cgroup
echo "+cpu +memory +io +pids" > /sys/fs/cgroup/cgroup.subtree_control
# Ограничить память (100MB hard limit)
echo "100M" > /sys/fs/cgroup/myapp/memory.max
# Soft limit (рекомендация для reclaim)
echo "80M" > /sys/fs/cgroup/myapp/memory.high
# Ограничить CPU (50% одного ядра)
echo "50000 100000" > /sys/fs/cgroup/myapp/cpu.max # quota period (µs)
# Ограничить количество процессов (защита от fork bomb)
echo "100" > /sys/fs/cgroup/myapp/pids.max
# Добавить процесс в cgroup
echo $PID > /sys/fs/cgroup/myapp/cgroup.procs
# Мониторинг
cat /sys/fs/cgroup/myapp/memory.current # текущее потребление
cat /sys/fs/cgroup/myapp/cpu.stat # usage_usec, system_usec, etc.
cat /sys/fs/cgroup/myapp/memory.pressure # PSI (Pressure Stall Information)
memory.max vs memory.high: max — жёсткий лимит (OOM при превышении), high — мягкий лимит (ядро агрессивно рекламирует память, процесс замедляется, но не убивается). Рекомендуется устанавливать оба: high = рабочий лимит, max = аварийный потолок.
PSI (Pressure Stall Information) — с ядра 4.20, файлы memory.pressure, cpu.pressure, io.pressure в каждой cgroup. Показывают, какой процент времени процессы были заблокированы из-за нехватки ресурса. systemd-oomd использует PSI для proactive OOM (убивает процессы до того, как ядерный OOM killer вступит в действие).
systemd и cgroups
systemd тесно интегрирован с cgroups v2. Каждый systemd unit (service, scope, slice) автоматически получает свою cgroup. systemd-cgls показывает дерево, systemd-cgtop — top-подобный мониторинг ресурсов по cgroup. Ресурсные лимиты в unit-файлах (MemoryMax=, CPUQuota=, TasksMax= — см. статью 2) транслируются напрямую в cgroup v2 файлы.
OCI: стандарт контейнеров
Open Container Initiative (OCI) определяет три спецификации: Image Spec (формат образа: layers, config, manifest), Runtime Spec (как запускать контейнер: namespaces, cgroups, rootfs, mounts), Distribution Spec (как хранить и передавать образы через registry).
OCI — результат стандартизации Docker-формата. Благодаря OCI образы портабельны между runtime'ами: образ, собранный Docker, запускается в Podman, containerd, CRI-O. Два основных OCI-совместимых low-level runtime: runc (Go, reference implementation, дефолт для Docker) и crun (C, Red Hat, быстрее, меньше памяти, дефолт для Podman).
Docker
Архитектура
Docker использует клиент-серверную модель:
docker CLI → Docker daemon (dockerd) → containerd → runc
dockerd — фоновый демон, работающий как root. Управляет containers, images, networks, volumes. Единая точка отказа: если dockerd падает, все контейнеры останавливаются. containerd — выделенный container runtime (CNCF-проект, extracted из Docker в 2016). Управляет lifecycle контейнеров: pull images, create/start/stop containers, manage snapshots. Kubernetes может использовать containerd напрямую (без Docker). runc — low-level runtime, создающий namespaces/cgroups и exec'ающий процесс контейнера.
Сеть
Docker по умолчанию создаёт bridge-сеть (docker0): каждый контейнер получает veth-пару (один конец в контейнере, другой подключён к bridge), IP из подсети bridge (обычно 172.17.0.0/16), NAT для выхода наружу (masquerade через iptables/nftables).
Альтернативные сетевые режимы: host (контейнер разделяет сетевой namespace хоста — нет изоляции, но нет overhead NAT), none (никакой сети), macvlan/ipvlan (контейнер получает адрес в физической сети), overlay (multi-host сеть для Swarm/Kubernetes).
Storage
Образ — стек read-only слоёв (layers). При запуске контейнера поверх добавляется writable layer. Union filesystem объединяет слои в единое дерево. Storage drivers: overlay2 (классический, наиболее совместимый), btrfs, zfs, vfs (для тестов); aufs удалён в Docker 24.0, devicemapper — в 25.0. С Docker Engine 29.0 на новых установках место классических graph drivers по умолчанию занимает containerd image store (снапшоттеры containerd); обновлённые с прежних версий демоны остаются на старом драйвере, пока не мигрируют вручную. Volumes (/var/lib/docker/volumes/) — предпочтительный способ persistent storage, bind mounts — прямое монтирование пути хоста.
Практика
docker run -d --name web -p 8080:80 --memory=256m --cpus=1.5 nginx:alpine
docker exec -it web /bin/sh
docker logs -f web
docker stats # real-time ресурсы всех контейнеров
docker inspect web # полная конфигурация (JSON)
docker system df # использование диска
docker system prune -a # очистка неиспользуемых ресурсов
# Compose (multi-container)
docker compose up -d
docker compose logs -f
docker compose down -v # включая volumes
Podman
Архитектура: daemonless, rootless
Podman принципиально отличается от Docker архитектурно:
Нет daemon. Каждый контейнер — дочерний процесс podman (fork/exec модель). Если podman-процесс завершится, контейнер продолжает работать под conmon (container monitor). Нет единой точки отказа.
Rootless по умолчанию. Контейнеры запускаются от обычного пользователя с использованием user namespaces. Root внутри контейнера маппится на непривилегированный UID хоста (subuid/subgid, настраиваются в /etc/subuid, /etc/subgid). Нет привилегированного демона — если контейнер скомпрометирован, атакующий получает права обычного пользователя, не root.
CLI-совместимость. podman run ≈ docker run. Многие проекты используют alias docker=podman. Podman поддерживает Docker-совместимый REST API через podman.socket.
Buildah и Skopeo
Podman-экосистема разделяет ответственность: Podman — запуск контейнеров, Buildah — сборка образов (без daemon, с тонким контролем над слоями), Skopeo — копирование и инспекция образов между registry (без pull на локальную машину).
Pods
Podman нативно поддерживает pods — группы контейнеров, разделяющих namespaces (как в Kubernetes). Это позволяет генерировать Kubernetes YAML из запущенных pods: podman generate kube mypod > pod.yaml и наоборот: podman play kube pod.yaml.
systemd-интеграция
Podman умеет генерировать systemd unit-файлы для контейнеров (podman generate systemd --new --name web > web.service), и контейнер дальше управляется systemd как обычный сервис. Но эта команда объявлена устаревшей — она получает только срочные багфиксы, новых возможностей в ней не будет; рекомендуемый путь — Quadlet. Quadlet (с Podman 4.4+) — декларативный формат: .container файлы в /etc/containers/systemd/ автоматически транслируются в systemd units. Кроме .container поддерживаются .pod, .volume, .network, .image, .kube, .build и .artifact, а с Podman 5.6 появилась и CLI-обвязка podman quadlet install|list|print|rm.
# /etc/containers/systemd/web.container (Quadlet)
[Container]
Image=docker.io/library/nginx:alpine
PublishPort=8080:80
Volume=web-data.volume:/usr/share/nginx/html:ro
[Service]
Restart=always
[Install]
WantedBy=default.target
LXC/LXD и systemd-nspawn
LXC (Linux Containers) — предшественник Docker, ориентирован на system containers (полноценная OS внутри контейнера, с init, сервисами, cron). В отличие от application containers (Docker/Podman — один процесс на контейнер), LXC-контейнер ближе к лёгкой VM.
LXD — высокоуровневый менеджер поверх LXC с REST API, image management, storage backends (ZFS, btrfs, LVM), live migration. С 2023 года LXD разрабатывает Canonical, а сообщество Linux Containers ведёт собственный форк Incus; проекты давно разошлись и развиваются независимо.
systemd-nspawn — контейнерный runtime, встроенный в systemd. Минималистичный, без image management. Используется для тестирования, сборки пакетов (mock/pbuilder), запуска полных OS-деревьев. Управление через machinectl.
Практика: что происходит при docker run
Пошаговая декомпозиция docker run -d --name web nginx:alpine:
- docker CLI отправляет запрос dockerd через Unix socket (/var/run/docker.sock).
- dockerd проверяет наличие образа; если нет — pull из registry (Docker Hub по умолчанию).
- dockerd запрашивает containerd создать контейнер.
- containerd подготавливает rootfs (overlay mount из слоёв образа).
- containerd создаёт OCI bundle (config.json + rootfs) и вызывает runc.
- runc создаёт namespaces (PID, NET, MNT, UTS, IPC), настраивает cgroups, монтирует файловые системы, применяет seccomp-фильтры и AppArmor/SELinux-профили.
- runc exec'ает entrypoint контейнера (nginx) как PID 1 в новых namespaces.
- runc передаёт контроль containerd (через shim процесс) и завершается.
В Podman шаги 1-3 заменяются на прямое взаимодействие podman CLI → conmon → crun/runc, без daemon.
Нюансы и trade-off'ы
Контейнер — не VM. Контейнеры разделяют ядро хоста. Это означает: уязвимость в ядре (container escape) компрометирует хост, kernel tunables (sysctl) часто глобальны (не per-namespace), seccomp и LSM (AppArmor/SELinux — см. статью 7) — критический дополнительный слой защиты.
Docker daemon как single point of failure. dockerd перезапуск = перезапуск всех контейнеров (хотя с live-restore=true containerd может сохранить контейнеры). В production Kubernetes использует containerd или CRI-O напрямую, минуя Docker (dockershim объявлен устаревшим в K8s 1.20 и удалён в 1.24).
Rootless trade-offs. Rootless контейнеры (Podman default, Docker optional) безопаснее, но с ограничениями: нет доступа к привилегированным портам (<1024) без CAP_NET_BIND_SERVICE, сеть идёт через userspace-стек и потому медленнее kernel bridge (в Podman с 6.0 это только pasta: slirp4netns и опция --network-cmd-path удалены; в Docker с 29.5 дефолтный драйвер — gvisor-tap-vsock, а slirp4netns больше не кладётся в пакеты), overlay в rootless давно работает нативно (Docker считает fuse-overlayfs ненужным с ядра 5.11, Podman требует 5.12.9), и fuse-overlayfs (чуть медленнее kernel overlay2) нужен только на более старых ядрах. Для production web-серверов rootful с правильной конфигурацией (user namespaces, seccomp, no-new-privileges) может быть практичнее.
cgroup v1 vs v2 в контейнерах. Вопрос давно перевернулся: беспокоиться нужно не о поддержке v2, а о том, что v1 уходит. Podman 6.0 на cgroup v1 не запускается вовсе, kubelet начиная с Kubernetes 1.35 по умолчанию отказывается стартовать на узле с v1 (обход — failCgroupV1: false в конфиге kubelet, но код планируют выкинуть не раньше 1.38). Дольше всех тянут Docker и containerd: оба объявили cgroup v1 устаревшим, но обещают поддерживать его как минимум до мая 2029.
Image layers и security. Каждый слой образа — immutable. Если секрет (пароль, ключ) попал в слой, он остаётся в образе навсегда, даже если удалён в следующем слое. Multi-stage builds решают это: промежуточные слои со сборочными зависимостями не попадают в финальный образ.
Docker Compose vs Kubernetes. Compose (docker-compose.yml) — инструмент для локальной разработки и простых deployments (один хост). Kubernetes — production-grade оркестратор (multi-host, auto-scaling, rolling updates, service discovery). Podman Compose и podman play kube размывают эту границу.
Сравнение с FreeBSD
Jails — предшественник контейнеров (FreeBSD 4.0, 2000). Jail предоставляет: изолированное дерево файловой системы (chroot на стероидах), изолированный набор процессов, опционально изолированный сетевой стек (VNET, аналог network namespace). В отличие от Linux-контейнеров, jails — единый механизм ядра, а не комбинация независимых namespaces. Это проще концептуально, но менее гранулярно: нельзя изолировать только PID без файловой системы, например.
VNET (виртуализированный сетевой стек в jail) — аналог network namespace. Даёт jail'у собственные интерфейсы, маршруты, firewall (PF). Service jails (FreeBSD 15.0+) — одна строка в rc.conf для запуска сервиса в изолированном jail (см. статью 2).
Отсутствие cgroups. FreeBSD использует RCTL (Resource Limits) для ограничения ресурсов jail'ов и процессов: CPU %, memory, max processes, open files, wallclock time. RCTL менее гранулярен, чем cgroups v2 (нет I/O bandwidth control, нет PSI), но покрывает основные сценарии.
Image management. Главное отличие: FreeBSD jails не имеют стандартизированного формата образов (аналога OCI). Создание jail'а — это подготовка directory tree (bsdinstall, poudriere, или bastillebsd). Bastille и pot — инструменты для управления jail'ами с элементами image management, но они не достигают зрелости Docker/Podman экосистемы. Это фундаментальная причина, по которой Docker-style контейнеризация доминирует в production.
Bhyve (FreeBSD hypervisor) — альтернатива, когда нужна полная изоляция (разное ядро, другая ОС). В Linux аналог — KVM/QEMU. Гибриды: Kata Containers, gVisor, Firecracker запускают контейнеры в micro-VM для усиленной изоляции с overhead, близким к контейнерам.
Источники
- namespaces(7) — man page по Linux namespaces
- cgroups(7) — man page по cgroups
- Control Group v2 — kernel.org — документация ядра по cgroup v2
- OCI Runtime Specification — спецификация OCI runtime
- Docker Architecture — официальная документация
- Podman documentation — официальная документация Podman
- Quadlet — systemd-интеграция Podman
- Container security fundamentals — Datadog — глубокий разбор namespaces
- What are namespaces and cgroups — NGINX — практическое руководство
- FreeBSD Handbook: Jails — для сравнения
Связанные заметки
- Linux — systemd: init, сервисы, таймеры — cgroup v2 и делегирование ресурсов
- Linux — безопасность: hardening, SELinux/AppArmor — capabilities, seccomp и MAC для контейнеров
- Linux — справочник команд: контейнеры и Kubernetes — диагностика контейнеров и cgroup
- FreeBSD — Jails — изоляция на уровне ОС во FreeBSD, для сравнения
- Linux — сводный cheatsheet — быстрая справка по docker и podman
- FreeBSD — bhyve: виртуализация после jails — та же граница «общее ядро против своего» со стороны FreeBSD
- Локальный инференс LLM — движки инференса запускают в контейнере с проброшенным GPU
- Адресное пространство: зачем трансляция и какие есть альтернативы — аппаратная изоляция, поверх которой namespaces изолируют только имена
- Структура ядра: монолит, микроядро и что между — почему изоляция имён не уменьшает доверенную базу и чем от неё отличается вынос драйвера наружу
Из библиотеки института уносят только знания. Читателей здесь не считают: посещение не регистрируется, отметок в издании не остаётся, счётчиков нет ни у одного издания фонда.
Издание пополняется и пересверяется. Текст доступен на условиях CC BY-SA 4.0 — унесённое можно передавать дальше на тех же условиях.