Исторически главной проблемой развертывания веб-приложений была проблема дрейфа окружения (Environmental Drift), формулируемая разработчиками как «На моем локальном компьютере все работало!». Приложение, успешно протестированное на машине разработчика под macOS или Windows, при попадании на production-сервер Linux падало из-за несовпадающих версий системной библиотеки glibc, отсутствующих системных пакетов, различий в конфигурации часовых поясов или иных прав доступа.
Для решения проблемы изоляции окружений индустрия десятилетиями использовала аппаратную виртуализацию (Virtual Machines, VM). В модели виртуализации специальная программа — гипервизор (Hypervisor) — эмулирует физическое оборудование (виртуальный процессор, память, сетевую карту, дисковый контроллер). Внутри каждой виртуальной машины устанавливается и запускается полноценная независимая гостевая операционная система (Guest OS) со своим собственным ядром, системными демонами и таблицами страниц памяти.
Технология контейнеризации (Containerization) и платформа Docker предложили принципиально иную парадигму: изоляцию на уровне процессов внутри единого разделяемого ядра Linux. Контейнер — это не виртуальная машина. Контейнер — это обычный изолированный процесс операционной системы хоста, чьи права, видимость ресурсов и потребление памяти ограничены встроенными механизмами ядра Linux: Namespaces, Control Groups (cgroups) и слоистой файловой системой OverlayFS.
Сравним физическое устройство виртуальной машины и легковесного контейнера в оперативной памяти сервера:
+-------------------------------------------------------------------------+
| ВИРТУАЛИЗАЦИЯ (VM) VS КОНТЕЙНЕРИЗАЦИЯ (DOCKER) |
+-------------------------------------------------------------------------+
| ВИРТУАЛЬНЫЕ МАШИНЫ (Hypervisor): КОНТЕЙНЕРЫ (Docker Engine): |
| |
| ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐|
| │ App 1 (Node) │ │ App 2 (Postg)│ │ App 1 (Node) │ │ App 2 (Postg)│|
| ├──────────────┤ ├──────────────┤ ├──────────────┤ ├──────────────┤|
| │ Libs / Bins │ │ Libs / Bins │ │ Libs / Bins │ │ Libs / Bins │|
| ├──────────────┤ ├──────────────┤ ├──────────────┴─┴──────────────┤|
| │ Гостевая ОС │ │ Гостевая ОС │ │ Изоляция cgroups/namespaces │|
| │ (Свое ядро!) │ │ (Свое ядро!) │ ├───────────────────────────────┤|
| ├──────────────┴─┴──────────────┤ │ Docker Engine (runc) │|
| │ Гипервизор (KVM / ESXi) │ ├───────────────────────────────┤|
| ├───────────────────────────────┤ │ Ядро хоста Linux │|
| │ Оборудование (CPU/RAM) │ │ Оборудование (CPU/RAM) │|
| └───────────────────────────────┘ └───────────────────────────────┘|
| (Сотни МБ памяти на ядро ОС; (0 МБ накладных расходов на ядро;|
| минуты на запуск виртуальной машины) старт процесса за миллисекунды) |
+-------------------------------------------------------------------------+Три столпа контейнеризации в ядре Linux:
eth0), свои точки монтирования и свое имя хоста.memory.max), такты процессора (cpu.max) и дисковые операции ввода-вывода (I/O).Docker Daemon проверяет локальный кэш образов. Если образ отсутствует, он скачивает неизменяемые слои с Docker Hub или реестра.
Низкоуровневый рантайм runc выполняет системные вызовы clone() и unshare(), изолируя PID, сетевой стек, точки монтирования и IPC.
Ядро монтирует слои образа (lowerdir) в режиме 'только чтение' и прикрепляет пустую директорию контейнера (upperdir) для записи.
Рантайм создает директорию в псевдофайловой системе /sys/fs/cgroup и записывает туда лимиты оперативной памяти и процессорного времени.
Основной процесс приложения запускается внутри изолированного окружения, получая статус PID 1 в своем локальном дереве процессов.
Что физически происходит в операционной системе Linux при запуске команды `docker run -d node:alpine`?
Ядро Linux изолирует окружение контейнера через 6 ключевых пространств имен:
# 1. Просмотр всех пространств имен активного процесса Node.js
# Каждое пространство имеет свой уникальный inode в ядре
ls -la /proc/$$/ns/
# Вывод покажет ссылки вида:
# lrwxrwxrwx 1 root root 0 Oct 19 12:00 pid -> 'pid:[4026531836]'
# lrwxrwxrwx 1 root root 0 Oct 19 12:00 net -> 'net:[4026531992]'
# lrwxrwxrwx 1 root root 0 Oct 19 12:00 mnt -> 'mnt:[4026531840]'
# 2. Список пространств имен в системе через утилиту lsns
lsns -t pid
# 3. Ручной запуск изолированного процесса без Docker через команду unshare
# Создаем изолированное пространство имен PID и монтирования:
unshare --pid --net --mount-proc /bin/bash
# Внутри новой оболочки ps aux покажет только один процесс — ваш текущий bash как PID 1!Назначение пространств имен:
PID 1 внутри контейнера, однако на хосте этот же процесс имеет реальный системный идентификатор (например, PID 18420).eth0), собственную таблицу маршрутизации и собственный изолированный стек портов./ без доступа к диску хоста.hostname).root (UID 0) внутри контейнера на непривилегированного пользователя на хосте (например, UID 10001), повышая безопасность.Если контейнер не ограничен по ресурсам, утечка памяти в коде Node.js может потребить всю физическую RAM сервера, вынудив ядро аварийно уничтожить критические системные процессы (SSH, базу данных). Для предотвращения катастрофы Docker настраивает подсистему cgroups:
# Запуск контейнера с лимитом 512 Мегабайт памяти и 1.5 ядра процессора
docker run -d \
--name payment-service \
--memory="512m" \
--cpus="1.5" \
--restart=on-failure:5 \
node:alpine node server.js
# Как ядро Linux отслеживает этот лимит под капотом (в cgroups v2):
cat /sys/fs/cgroup/system.slice/docker-<CONTAINER_ID>.scope/memory.max
# Выведет точное число байт: 536870912 (512 МБ)
cat /sys/fs/cgroup/system.slice/docker-<CONTAINER_ID>.scope/memory.current
# Текущее фактическое потребление оперативной памяти контейнеромОбраз Docker состоит из набора неизменяемых слоев (Read-Only Layers). Когда контейнер стартует, Docker создает тонкий слой записи (Writable Container Layer).
+-------------------------------------------------------------------------+
| АНАТОМИЯ СЛОИСТОЙ ФАЙЛОВОЙ СИСТЕМЫ |
+-------------------------------------------------------------------------+
| [Слой контейнера] (Read-Write, верхний слой / upperdir): |
| Файлы логов, временные файлы кэша, измененные конфиги |
| (Существует только пока жив контейнер!) |
| ─────────────────────────────────────────────────────────────────────── |
| [Объединенный вид] (Merged View: то, что видит процесс внутри): |
| /app/server.js /usr/local/bin/node /etc/hosts /var/log/app.log |
| ─────────────────────────────────────────────────────────────────────── |
| [Слои образа] (Read-Only, нижние слои / lowerdir): |
| Слой 3: Исходный код приложения (COPY . /app) |
| Слой 2: Установленные npm-пакеты (RUN pnpm install) |
| Слой 1: Базовый дистрибутив Alpine (FROM node:alpine) |
| (Разделяются между сотнями контейнеров без дублирования памяти!) |
+-------------------------------------------------------------------------+Если процесс внутри контейнера читает файл из слоя образа, он читает его напрямую. Если процесс пытается изменить файл, ядро Linux мгновенно копирует этот файл из нижнего неизменяемого слоя (lowerdir) в верхний записываемый слой (upperdir) и модифицирует копию. Это и есть принцип Copy-on-Write (копирование при записи).
Демон containerd распаковывает образ в спецификацию OCI: корень файловой системы (rootfs) и манифест config.json.
Стандарт OCI (Open Container Initiative) гарантирует совместимость образов с любыми рантаймами (Docker, Podman, CRI-O).
Выполняется системный вызов clone с набором флагов CLONE_NEWPID, CLONE_NEWNET, CLONE_NEWNS для создания новых пространств имен.
Дочерний процесс рождается изолированным от системного окружения хоста.
Процесс вызывает pivot_root, делая директорию rootfs своим новым корнем файловой системы и отмонтируя старый корень хоста.
Безопаснее устаревшего chroot: предотвращает атаки выхода из изолированного каталога (chroot jailbreak).
Ядро применяет фильтр системных вызовов Seccomp (блокирует опасные вызовы вроде reboot) и устанавливает лимиты памяти в cgroups.
По умолчанию в профиле seccomp Docker заблокировано более 40 опасных системных вызовов ядра.
Контейнеру Node.js задан жесткий лимит памяти `--memory='256m'`. В коде возникла утечка памяти, и потребление достигло 260 МБ. Что произойдет с контейнером?
Начинающие разработчики часто пытаются установить внутрь одного контейнера SSH-сервер (sshd), планировщик cron, базу данных и веб-сервер.
Это фундаментальный антипаттерн: контейнер спроектирован для запуска ровно одного главного процесса (PID 1). Нарушение принципа «один контейнер — одна ответственность» ломает сбор логов (stdout), препятствует автоматическому масштабированию и делает невозможным мониторинг работоспособности через Healthcheck.
Запуск команды docker run -d my-app без флагов --memory опасен. В случае неконтролируемой утечки памяти процесс Node.js начнет вытеснять память хоста. Сработает глобальный хостовый OOM Killer, который с высокой вероятностью убьет не виновный контейнер, а СУБД PostgreSQL или процесс Docker Daemon.
Решение: Всегда задавайте --memory="512m" и лимиты CPU для каждого контейнера.
Файлы, создаваемые приложением внутри контейнера (загруженные пользователями аватары, данные базы данных), записываются в тонкий временный слой OverlayFS. При удалении контейнера (docker rm -f) этот слой безвозвратно удаляется с диска вместе со всеми данными.
Решение: Для любых сохраняемых данных обязательно используйте постоянные хранилища — Docker Volumes или Bind Mounts.
Так как контейнеры разделяют ядро хостовой операционной системы, невозможно запустить нативный 64-битный исполняемый файл .exe Windows внутри Linux-контейнера без эмулятора. Контейнеризация изолирует окружение, но не эмулирует чужую архитектуру процессора или системные вызовы чужого ядра.