Learning Platform
Глоссарий Troubleshooting
Урок 03.01 · 22 мин
Начальный
dockercontainersnamespacescgroups

Что такое контейнер

NOTE

Дальше начинается технический слой. Часть слов тут будут новыми — namespaces, cgroups, runc, overlayfs, syscall clone(). Это нормально, и пугаться их не надо. Ключевые понятия разбираются прямо ниже в этом уроке и подробно — в следующих модулях. Твоя задача сейчас — поймать общую картину «контейнер = изолированный процесс», а не выучить каждый термин наизусть с первого раза. Если какое-то слово осталось туманным — читай дальше, оно встретится ещё не раз и постепенно станет понятным.

Если спросить десять людей «что такое контейнер?», получишь десять ответов разной степени метафоричности. «Это как виртуалка, но легче». «Это коробочка, в которой живёт программа». «Это shipping container, only for software». Все ответы примерно правильные и одинаково бесполезные, если ты потом сядешь дебажить, почему docker exec валится с «exec format error».

Технически контейнер — это обычный процесс операционной системы. Тот же python app.py, который ты запускаешь в терминале. Только этот процесс запущен с дополнительной обёрткой, которая делает три вещи:

Анатомия процесса: PID, address space, fd-таблица
  1. Изолирует ему «вид» на систему — свой PID-namespace, свой mount-namespace, свой network-namespace. Процесс думает, что он один в системе, что корневая файловая система выглядит вот так, и что есть только одно сетевое устройство eth0.
  2. Ограничивает ресурсы — через cgroups говорим «этому процессу не больше 512 МБ RAM и не больше двух CPU».
  3. Подменяет корневой образ файловой системы — вместо реального / хоста процесс видит распакованный image (например, python:3.12-slim).

Всё. Никакой виртуальной машины. Никакого отдельного ядра. Ядро Linux одно — хостовое.


Контейнер на одной картинке

Контейнер: процесс с обёртками namespaces + cgroups + rootfs
Хост LinuxРеальная машина с реальным ядром Linux 6.x. Это единственное ядро в системе
запускает
Процесс PID=12345Обычный процесс. Если бы не контейнер, ты бы видел его в ps aux
namespacesPID, mount, net, uts, ipc, user, cgroup, time. Каждый изолирует один аспект 'вида на систему'
cgroupscgroup v2. Ограничивают memory, CPU, blkio, pids. Указываешь --memory=512m, --cpus=2
rootfsРаспакованный image, смонтированный через overlay2. Процесс видит / как корень своего образа
imagepython:3.12-slim или Postgres 16.4. Слоистая структура из tar-архивов, лежит в /var/lib/docker
распакован в
overlayfsОбъединение нескольких слоёв (read-only image + writable container layer) в один rootfs

Когда ты пишешь docker run -d --name pg postgres:16.4, под капотом происходит примерно следующее:

  1. Docker CLI говорит daemon’у: «хочу контейнер из image postgres:16.4».
  2. Daemon просит containerd. Containerd просит runc.
  3. Runc делает clone() системный вызов с флагами CLONE_NEWPID | CLONE_NEWNS | CLONE_NEWNET | .... Это создаёт новый процесс в собственных namespaces.
  4. Прикручиваются cgroups (если ты задал лимиты).
  5. Монтируется overlayfs из слоёв image. Делается pivot_root — корень меняется на этот overlay.
  6. Запускается процесс 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?».


Проверка знанийKnowledge check
Технически, чем процесс внутри контейнера отличается от обычного процесса хостовой системы?
ОтветAnswer
Технически почти ничем. Это обычный процесс хостового ядра Linux. Отличия только в обёртках. Первое: процесс запущен в собственном наборе namespaces (PID, mount, net, uts, ipc, user, cgroup), которые изолируют его «вид» на систему — он видит свой PID 1, свою сеть, свой корень файловой системы. Второе: процесс помещён в cgroup, который ограничивает ресурсы — память, CPU, IO. Третье: корнем файловой системы для этого процесса является не реальный корень хоста, а смонтированный через overlayfs распакованный image. Ядро при этом одно — хостовое. Нет виртуальной машины, нет hypervisor'а (на Linux). С точки зрения хоста это обычный процесс в ps aux, с точки зрения самого процесса — он один в системе.

Проверьте понимание

Результат: 0 из 0
Концептуальный
Вопрос 1 из 5. Технически контейнер — это:

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 5