Что такое контейнер
Дальше начинается технический слой. Часть слов тут будут новыми — namespaces, cgroups, runc, overlayfs, syscall clone(). Это нормально, и пугаться их не надо. Ключевые понятия разбираются прямо ниже в этом уроке и подробно — в следующих модулях. Твоя задача сейчас — поймать общую картину «контейнер = изолированный процесс», а не выучить каждый термин наизусть с первого раза. Если какое-то слово осталось туманным — читай дальше, оно встретится ещё не раз и постепенно станет понятным.
Если спросить десять людей «что такое контейнер?», получишь десять ответов разной степени метафоричности. «Это как виртуалка, но легче». «Это коробочка, в которой живёт программа». «Это shipping container, only for software». Все ответы примерно правильные и одинаково бесполезные, если ты потом сядешь дебажить, почему docker exec валится с «exec format error».
Технически контейнер — это обычный процесс операционной системы. Тот же python app.py, который ты запускаешь в терминале. Только этот процесс запущен с дополнительной обёрткой, которая делает три вещи:
- Изолирует ему «вид» на систему — свой PID-namespace, свой mount-namespace, свой network-namespace. Процесс думает, что он один в системе, что корневая файловая система выглядит вот так, и что есть только одно сетевое устройство
eth0. - Ограничивает ресурсы — через cgroups говорим «этому процессу не больше 512 МБ RAM и не больше двух CPU».
- Подменяет корневой образ файловой системы — вместо реального
/хоста процесс видит распакованный image (например,python:3.12-slim).
Всё. Никакой виртуальной машины. Никакого отдельного ядра. Ядро Linux одно — хостовое.
Контейнер на одной картинке
Когда ты пишешь docker run -d --name pg postgres:16.4, под капотом происходит примерно следующее:
- Docker CLI говорит daemon’у: «хочу контейнер из image postgres:16.4».
- Daemon просит containerd. Containerd просит runc.
- Runc делает
clone()системный вызов с флагамиCLONE_NEWPID | CLONE_NEWNS | CLONE_NEWNET | .... Это создаёт новый процесс в собственных namespaces. - Прикручиваются cgroups (если ты задал лимиты).
- Монтируется overlayfs из слоёв image. Делается
pivot_root— корень меняется на этот overlay. - Запускается процесс PID 1 в этом контейнере. Для Postgres это
postgresглавный процесс.
С точки зрения хоста — это просто процесс в ps aux, ничего волшебного. С точки зрения самого процесса — он один в системе, у него / это корень postgres-образа, в /proc/1/status он видит «я PID 1».
Capabilities и namespaces — разрезаем «всемогущество root» на кусочки
Что такое «namespaces» и зачем их восемь
Namespace — это механизм Linux-ядра, который изолирует один ресурс. В Linux есть 8 типов:
- pid — изолирует список процессов. Внутри контейнера
ps auxпокажет только процессы этого контейнера. Главный процесс — это PID 1. - mount — изолирует список монтированных файловых систем. Контейнер видит только свои монтирования.
- net — изолирует сетевые устройства, IP, маршруты. Свой
eth0, свояlo. - uts — изолирует hostname и domainname. Поэтому
hostnameвнутри контейнера часто это короткий ID контейнера. - ipc — изолирует System V IPC и POSIX message queues.
- user — изолирует UID/GID. Root в контейнере не равен root на хосте (если используется user namespaces).
- cgroup — изолирует view на cgroup-иерархию.
- time — изолирует системное время (новый namespace, появился в 5.6).
Docker по умолчанию использует первые 7. Time namespace — пока редко.
Когда ты слышишь «контейнеры — это изоляция через namespaces», это значит, что процесс контейнера запущен с собственным набором этих восьми. Хостовые namespaces остаются на хосте, контейнерные процессы их не видят.
Cgroups — лимиты на ресурсы
Cgroups (control groups) — это отдельный механизм, не путать с namespaces. Namespaces говорят «что процесс видит», cgroups — «сколько процесс может потребить».
Swap, overcommit, OOM killer — что делать когда RAM не хватаетЕсли ты запускаешь:
docker run --memory=512m --cpus=2 nginx
Docker помещает процесс контейнера в свой cgroup, который ограничивает память до 512 МБ и CPU до двух ядер (по сумме). Если процесс попытается съесть больше памяти — он получит OOM kill.
В современных дистрибутивах используется cgroup v2 — единая иерархия. На старых системах ещё встречается v1 (отдельные иерархии для cpu, memory, и т.д.). Это видно в docker info:
docker info | grep -i cgroup
# Cgroup Driver: systemd
# Cgroup Version: 2
В курсе мы предполагаем cgroup v2 — это default на современных Linux-дистрибутивах (Ubuntu 22.04+, Debian 11+, Fedora 31+).
Чем контейнер не является
Чтобы избежать типичных заблуждений:
- Контейнер — не виртуальная машина. Нет отдельного ядра, нет hypervisor’а (на Linux). На macOS и Windows контейнер всё-таки крутится в Linux-VM внутри Docker Desktop / OrbStack / Rancher, но это деталь хоста, а не контейнера.
- Контейнер — не sandbox в смысле jail. Изоляция намного слабее, чем у настоящей VM. Если ядро уязвимо — контейнер не спасёт. Контейнер не предназначен запускать недоверенный код от анонимных пользователей.
- Контейнер — не «один процесс». Внутри контейнера может быть много процессов (например, supervisord -> nginx + php-fpm). Но это часто плохая практика — обычно один процесс на контейнер.
- Контейнер — не «легче» виртуалки магически. Он легче, потому что не таскает второе ядро и не загружает init-систему. Это всё.
Метафора shipping container
Метафора стандартного грузового контейнера, на которую опираются логотип Docker и название, всё-таки полезна, если её правильно понять.
В мире логистики до 1950-х годов товары грузились на корабли как попало — мешки, ящики, бочки. Каждый порт перегружал по-своему. Когда появились стандартные стальные контейнеры размером 20 или 40 футов, всё изменилось: их можно было ставить на любой корабль, любую фуру, любой поезд. Стандарт интерфейса.
С софтом до контейнеров была примерно та же история. Чтобы запустить приложение на сервере, нужно было правильно поставить Python (нужной версии), системные библиотеки, конфиги, права. На dev-машине одна версия, на staging другая, на проде третья — и всё ломалось. «Works on my machine» — это про это.
Контейнер — это «стандартизированная упаковка». В image внутри — твоё приложение, его зависимости, и нужная версия рантайма. Образ работает одинаково на любой машине, где есть Docker-совместимый рантайм. Не «работает на macOS» или «работает на Ubuntu 24.04» — а «работает там, где есть рантайм».
Это и есть главная причина, почему индустрия за 10 лет (2013-2023) полностью съехала на контейнеры. Это решение проблемы «у нас разъезжаются окружения dev/staging/prod».
Попробуй сам
Если у тебя установлен Docker, выполни:
docker run --rm -it alpine sh
Внутри контейнера:
ps aux
hostname
ls /
exit
Обрати внимание: внутри ps aux покажет только sh и сам ps — это PID 1 и PID 2. На хосте этот же процесс в ps aux будет совсем другой PID, например 38491. Это иллюстрация PID namespace: внутри контейнера своя нумерация.
Команда hostname покажет короткий ID контейнера — это иллюстрация UTS namespace.
Команда ls / покажет alpine-овский корень — это иллюстрация mount namespace + rootfs из image.
Связь с дальнейшими модулями
К концу модуля 4 ты будешь свободно запускать контейнеры через docker run, заходить внутрь через docker exec, смотреть логи через docker logs. К концу модуля 7 — собирать свои образы через Dockerfile. К концу модуля 17 — поднимать полный DE-стенд из Compose.
Но фундамент всегда тот же: контейнер — это процесс с namespaces, cgroups и собственным rootfs. Каждый раз, когда что-то ломается («почему этот процесс не видит мою сеть?», «почему kill -9 1 внутри контейнера убивает контейнер?»), ты возвращаешься к этой модели и спрашиваешь себя: «в каком namespace это происходит, и что ограничивает cgroups?».