System design · DevOps
Как работает Docker: разбор от docker run до ядра Linux
Архитектура Docker, namespaces и cgroups, слои образов и OverlayFS, сеть, хранилище и модель безопасности. Что на самом деле происходит под командой docker run.
Коротко
Docker упаковывает приложение со всеми зависимостями в образ и запускает его в контейнере — обычном процессе Linux, который изолирован средствами ядра. Своего ядра у контейнера нет, поэтому он стартует быстрее и расходует меньше ресурсов, чем виртуальная машина. Под командой docker run работают пять механизмов:
- цепочка компонентов Docker CLI → dockerd → containerd → containerd-shim → runc;
- namespaces — ограничивают, что контейнер видит: процессы, сеть, файловую систему;
- cgroups — ограничивают, сколько контейнер потребляет: CPU, память, дисковый I/O;
- слоёный образ и OverlayFS — слои только для чтения и один записываемый слой сверху;
- виртуальные Ethernet-пары, iptables и volumes — сеть и данные, которые переживают контейнер.
Что такое Docker и какую проблему он решает?
Docker — платформа, которая упаковывает приложение вместе с зависимостями в переносимый образ и запускает его в изолированном контейнере. Контейнер ведёт себя одинаково на ноутбуке разработчика, в CI и в продакшне, поэтому Docker закрывает проблему «у меня всё работает» без накладных расходов полноценной виртуальной машины.
- Изоляция: namespaces и cgroups ядра Linux
- Формат образов: OCI
- Альтернативы: Podman, containerd, CRI-O
Знакомая фраза: «У меня на машине всё работает».
Приложение в разработке: тесты проходят, зависимости ставятся, всё готово к выкладке. Но в продакшне что-то ломается. На сервере может стоять другая версия Python, библиотека может быть установлена локально, но отсутствовать в окружении деплоя. Иногда проблему создаёт сама операционная система.
Дело никогда не было только в приложении — дело в окружении вокруг него.
Почему виртуальные машины не решили проблему окружения?
Долгое время лучшим решением у инженеров были виртуальные машины. Вместо одного приложения команды стали упаковывать вместе с ним целую операционную систему. Это закрыло большую часть проблемы согласованности: окружение вело себя одинаково, где бы ни запускалась нагрузка. Но у подхода была цена, которую трудно игнорировать.
Каждая виртуальная машина несла собственное ядро, службы и системные накладные расходы ради одного приложения. Машины занимали много памяти, долго загружались и становились слишком сложными в управлении по мере роста инфраструктуры.
Индустрии был нужен способ изолировать приложения, не платя каждый раз за полноценную операционную систему.
Как Docker сделал контейнеры удобными?
Самое интересное: в GNU/Linux уже были почти все строительные блоки. Namespaces умели изолировать процессы друг от друга, cgroups — ограничивать, сколько CPU и памяти разрешено потреблять процессу. Примитивы существовали, но пользоваться ими напрямую было трудно, и для большинства разработчиков это было слишком сложно.
Docker превратил эти низкоуровневые возможности ядра в удобный для разработчика процесс. Инженерам больше не нужно было вручную настраивать примитивы изоляции: они получили простой CLI, переносимые образы, воспроизводимые окружения и среду выполнения для запуска контейнеров в масштабе.
Этот сдвиг изменил современную инфраструктуру. Сегодня контейнеры лежат в основе почти всего: от CI/CD-пайплайнов до кластеров Kubernetes. Но при всей распространённости большинство инженеров работает только с верхним слоем. Они знают команды, умеют собирать образы и запускать контейнеры. А стек среды выполнения, который работает под этими командами, видят редко.
Эта статья — про нижний слой.
Из чего состоит архитектура Docker?
Архитектура Docker — не одна программа, а стек взаимодействующих компонентов: клиент Docker CLI, демон dockerd, среда выполнения containerd, процесс containerd-shim и низкоуровневый runtime runc. Каждый слой отвечает за свою задачу и передаёт работу следующему, пока ядро Linux не запустит процесс контейнера.
- Клиент: Docker CLI
- Демон: dockerd
- Runtime: containerd, containerd-shim, runc
- Стандарт: OCI
Большинство инженеров работает с CLI и считает, что Docker делает всё сам где-то за кулисами. Но команда docker run проходит через целую цепочку среды выполнения, прежде чем запустится хоть один процесс. Если понять эту цепочку, остальное устройство Docker становится проще.

У каждого слоя своя зона ответственности. Одни компоненты обслуживают API, другие управляют жизненным циклом контейнеров, третьи работают напрямую с ядром Linux. Такое разделение делает среду выполнения модульной и упрощает её сопровождение.
| Компонент | Роль | Как общается со следующим |
|---|---|---|
| Docker CLI | Тонкий клиент: превращает команду в API-запрос | REST API через /var/run/docker.sock |
| dockerd | Демон: образы, сети, volumes, Docker API | gRPC к containerd |
| containerd | Жизненный цикл контейнеров, загрузка и распаковка образов, снапшоты | Запускает shim на каждый контейнер |
| containerd-shim | Родитель процесса контейнера, держит stdin/stdout и код выхода | Вызывает runc |
| runc | OCI-runtime: создаёт namespaces, cgroups, файловую систему и стартует процесс | Системные вызовы ядра Linux |
Разберём цепочку по звеньям.
Что такое Docker CLI?
Docker CLI — тонкий клиент командной строки. Когда вы набираете docker run или docker build, CLI превращает команду в API-запрос и отправляет его демону Docker через /var/run/docker.sock.
Сам CLI не собирает образы, не скачивает слои и не запускает контейнеры. Он только пересылает запросы, а настоящая работа идёт за ним.
Это разделение важно: CLI и демону даже не обязательно работать на одной машине. Docker может открыть свой API удалённо, и тогда внешние инструменты и системы автоматизации общаются с демоном напрямую.
Что такое dockerd?
dockerd — демон Docker. Он принимает запросы от CLI и управляет образами, сетями, volumes и всем Docker API. Но контейнеры dockerd сам не запускает: управление их жизненным циклом он передаёт containerd.
Благодаря этому разделению контейнеры продолжают работать, даже если демон Docker перезапускается. Слой среды выполнения живёт независимо от более высокого слоя Docker API.

Демон также играет роль слоя оркестрации для локальных операций Docker. Когда вы скачиваете образ, создаёте сеть или подключаете volume, запрос сначала проходит через dockerd и только потом попадает в нижние компоненты среды выполнения.
Что такое containerd?
containerd — основная среда выполнения контейнеров (container runtime), которая управляет их жизненным циклом. Через gRPC API она скачивает образы, ведёт снапшоты, распаковывает образы и выполняет операции среды выполнения.
Когда dockerd нужно запустить контейнер, он поручает это containerd. С этого момента демон Docker в основном выходит из пути выполнения.

Изначально containerd был частью самого Docker. В 2017 году Docker передал его в Cloud Native Computing Foundation (CNCF) как независимый проект. Сегодня Kubernetes общается не с Docker, а с containerd и другими runtime, совместимыми с CRI (Container Runtime Interface).
Этот сдвиг заметно изменил экосистему контейнеров. Docker остался платформой для разработчика, а containerd стал средой выполнения под системами оркестрации. Но и containerd не создаёт контейнеры напрямую — он передаёт эту работу ниже по стеку.
Что такое containerd-shim?
containerd-shim — лёгкий процесс-посредник между containerd и работающим контейнером. Перед стартом контейнера containerd сначала запускает shim. Shim становится родителем процесса контейнера и сохраняет контейнер живым, даже если containerd упадёт или перезапустится.
У каждого контейнера свой shim. Этот процесс отделяет работающий контейнер от слоя среды выполнения и поддерживает связь с ним.
Кроме того, shim управляет потоками ввода-вывода контейнера и сообщает код его завершения. Без shim containerd пришлось бы оставаться напрямую привязанным к каждому процессу контейнера.
Когда shim готов, он вызывает компонент, который на самом деле создаёт контейнер.
Что такое runc?
runc — runtime стандарта Open Container Initiative (OCI), который напрямую работает с ядром Linux и создаёт контейнер. Он читает конфигурацию контейнера, создаёт namespaces и cgroups, настраивает файловую систему и запускает процесс контейнера.
На этом слое контейнеры перестают быть объектами Docker и становятся обычными процессами Linux, границы изоляции которых обеспечивает ядро.

После старта процесса runc завершается: его работа сделана. Дальше за контейнером следит shim и сообщает его статус обратно в containerd.
Что такое стандарты OCI?
OCI (Open Container Initiative) — набор открытых стандартов для образов контейнеров и сред выполнения. Все компоненты стека выше работают вместе именно потому, что следуют одним стандартам OCI, и это позволяет разным инструментам экосистемы контейнеров чисто взаимодействовать.
Образы, собранные Docker, без изменений запускаются в containerd, CRI-O и Podman. Kubernetes может заменить один OCI-runtime другим, не меняя остальной стек.
До стандартизации OCI инструменты для контейнеров были связаны куда жёстче. Docker популяризировал контейнеры, а OCI позволил разным инструментам работать вместе. Благодаря стандартам среды выполнения стали модульными, а не привязанными к одному вендору или набору инструментов.
Стек среды выполнения ясен. Теперь посмотрим на примитивы Linux, которые на самом деле делают контейнеры возможными.
Что такое namespaces в Linux?
Namespace (пространство имён) в Linux — механизм ядра, который оборачивает глобальный системный ресурс и делает его видимым для процесса как частный. Процесс внутри namespace считает, что у него своя изолированная копия ресурса, а хост и остальные процессы по-прежнему видят исходный глобальный ресурс.
- Что ограничивает: что процесс видит
- Для контейнеров: 6 типов — PID, NET, MNT, UTS, IPC, USER
- Кто создаёт в Docker: runc
Контейнеры — это не возможность Docker, а возможность ядра Linux. Docker даёт инструменты, CLI, формат образов и рабочий процесс. Но изоляция, благодаря которой контейнеры работают, исходит от самого ядра, и в её центре стоит один примитив — namespaces.

Контейнеры опираются на шесть namespaces Linux. Каждый изолирует свою часть системы, а вместе они создают иллюзию, что контейнер работает на отдельной машине. В ядре есть и другие типы — например, cgroup и time namespace, — но основу изоляции контейнера составляют эти шесть.
| Namespace | Что изолирует | Что это даёт контейнеру |
|---|---|---|
| PID | Идентификаторы процессов | Скрывает процессы хоста, свой PID 1 |
| Network (NET) | Интерфейсы, IP-адреса, маршруты, порты | Приватный сетевой стек |
| Mount (MNT) | Точки монтирования и вид файловой системы | Своя корневая файловая система |
| UTS | Имя хоста и доменное имя | Чистая, предсказуемая идентичность |
| IPC | Разделяемая память, очереди сообщений | Нет вмешательства через память между контейнерами |
| User (USER) | Идентификаторы пользователей и групп | Root в контейнере — не root на хосте |
Пройдём по каждому.
Что такое PID namespace?
PID namespace даёт контейнеру собственную нумерацию процессов. У каждого процесса в Linux есть идентификатор процесса (PID), и на хосте эти идентификаторы глобальны: все процессы делят одно пространство номеров. PID namespace это меняет.
Когда контейнер стартует в своём PID namespace, его первый процесс получает PID 1. Изнутри контейнера этот процесс выглядит корнем всей системы. Он видит свои дочерние процессы, но не видит ничего, что работает на хосте или в других контейнерах.
Хост при этом видит процесс контейнера под его настоящим PID в глобальном пространстве. Контейнер об этом не знает.
Это важно, потому что на PID 1 в Linux лежат особые обязанности: он должен обрабатывать сигналы и подбирать осиротевшие дочерние процессы. Контейнеры, которые делают это неправильно, обрастают процессами-зомби. Поэтому то, что работает как PID 1 внутри контейнера, — важное проектное решение (подробнее — в разделе о жизненном цикле).
Что такое network namespace?
Network namespace даёт контейнеру изолированный сетевой стек: свой виртуальный сетевой интерфейс, IP-адрес, таблицу маршрутизации и набор портов. По умолчанию все процессы на машине с Linux делят один сетевой стек — одни и те же интерфейсы, таблицы маршрутизации и порты.
Процесс внутри контейнера может занять port 80, ни с чем не конфликтуя на хосте.

Именно так разные контейнеры одновременно слушают порт 80 на одной машине. На самом деле они не делят один порт: у каждого свой приватный network namespace.
Чтобы соединить network namespace каждого контейнера с хостом, Docker создаёт виртуальную Ethernet-пару. Трафик идёт через эту пару, а хост маршрутизирует его куда нужно.
Мост делает сеть контейнеров возможной, но реальной изоляцию делает именно namespace.
Что такое mount namespace?
Mount namespace управляет тем, что процесс видит, когда смотрит на файловую систему. Без него все процессы в системе делят одно представление файловой системы, и любой процесс видит каждый смонтированный диск, каждый каталог и каждый файл на машине.
С mount namespace у каждого контейнера свой вид файловой системы. Запуская контейнер, Docker собирает слоёную файловую систему с помощью OverlayFS и монтирует её в новом mount namespace. Контейнер видит только её: он не может просматривать каталоги хоста или обращаться к файлам за пределами своего корня.
Так у каждого контейнера появляется изолированная корневая файловая система — со своими файлами операционной системы, библиотеками и кодом приложения, не затрагивающими хост.
Что такое UTS namespace?
UTS namespace (Unix Time-Sharing) управляет именем хоста и доменным именем процесса. Он проще остальных, но важен для того, как контейнеры представляются в сети.
Без него все контейнеры делили бы имя хост-машины, и логам, системам мониторинга и инструментам service discovery было бы трудно отличить контейнеры от хоста.
UTS namespace даёт каждому контейнеру собственное имя хоста. Когда контейнер выполняет hostname, он видит имя, которое назначил ему Docker, а не имя хоста под ним. Эта мелочь сохраняет идентичность контейнеров чистой и предсказуемой.
Что такое IPC namespace?
IPC namespace (Inter-Process Communication) изолирует механизмы межпроцессного взаимодействия: сегменты разделяемой памяти и очереди сообщений, через которые процессы Linux общаются напрямую.
Процессы внутри контейнера могут общаться только с процессами в том же IPC namespace. Добраться через разделяемую память до другого контейнера или до хоста они не могут.
Без этой изоляции процесс в одном контейнере потенциально мог бы вмешаться в сегменты разделяемой памяти другого контейнера на том же хосте или прочитать из них данные. IPC namespace закрывает эту брешь.
Что такое user namespace?
User namespace сопоставляет идентификаторы пользователей и групп внутри контейнера с другими идентификаторами на хосте. Процесс может выглядеть как root внутри контейнера, а на хосте на самом деле соответствовать непривилегированному пользователю. Это самый мощный из шести namespaces и самый тонкий.
Для безопасности это различие критично. Многие контейнеры по умолчанию запускают процессы от root. Без user namespace такой процесс — root и на хосте, и побег из контейнера (container escape) даст атакующему полный root-доступ к машине.
С включённым user namespace root внутри контейнера — просто обычный непривилегированный пользователь на хосте. Радиус поражения при побеге из контейнера заметно сокращается.
Именно user namespaces делают возможными rootless-контейнеры. Docker, Podman и другие среды выполнения используют их, чтобы инженеры могли запускать контейнеры вообще без root-прав на хосте.
Как namespaces работают вместе?
Изоляция контейнера складывается из всех шести namespaces сразу: ни один из них по отдельности контейнер не создаёт.
- PID namespace скрывает процессы хоста.
- Network namespace даёт приватную сеть.
- Mount namespace даёт каждому контейнеру свою файловую систему.
- UTS namespace задаёт чистую идентичность.
- IPC namespace не даёт контейнерам вмешиваться в память друг друга.
- User namespace ограничивает ущерб, если что-то пошло не так.
Но namespaces управляют только тем, что контейнер видит. О том, сколько он может потреблять, они не говорят ничего. Здесь в дело вступает второй примитив Linux.
Что такое cgroups в Linux?
cgroups (control groups, контрольные группы) — механизм ядра Linux, который ограничивает, учитывает и изолирует потребление ресурсов группой процессов. Каждый контейнер Docker живёт в своей cgroup, и она определяет, сколько CPU, памяти и дискового ввода-вывода ему разрешено использовать.
- Что ограничивает: сколько процесс потребляет
- Ресурсы: CPU, память, дисковый I/O
- В ядре: с 2008 года
- Версии: v1 и v2
Namespaces управляют тем, что контейнер видит, но ничего не говорят о том, сколько он может потреблять. Без контроля ресурсов один контейнер мог бы занять весь CPU хоста, исчерпать доступную память и оставить без ресурсов все остальные процессы на машине. Изоляция была бы настоящей, но система оставалась бы хрупкой: один контейнер, который ведёт себя плохо, мог бы уронить всё.
Эту проблему и решают cgroups. Если контейнер выходит за установленные лимиты, вмешивается ядро.
cgroups Docker тоже не изобретал. Они есть в ядре Linux с 2008 года, а Docker просто сделал их простыми в настройке.
Какие ресурсы ограничивают cgroups?
cgroups дают точный контроль над тремя основными ресурсами контейнера: процессорным временем, памятью и пропускной способностью диска.
- CPU. Контейнерам можно назначить доли CPU (shares): они определяют, сколько процессорного времени получает контейнер относительно других. Можно задать и жёсткие квоты — они ограничивают контейнер фиксированной долей CPU независимо от того, что ещё работает на машине.
- Память. Задаёт максимальный объём памяти контейнера. Если контейнер пытается его превысить, вмешивается механизм ядра OOM killer (Out of Memory) и завершает процесс. Так один контейнер не может занять всю доступную память и уронить хост.
- Дисковый I/O. Можно ограничить пропускную способность чтения и записи на диск. Это не даёт нагрузкам, активно работающим с хранилищем, монополизировать ввод-вывод (I/O) и мешать другим контейнерам на той же машине.
Именно эти механизмы делают безопасным запуск множества контейнеров на одном хосте: каждый получает свою долю ресурсов, а ядро автоматически следит за границами.
Чем cgroups v2 отличается от cgroups v1?
cgroups v2 объединяет все контроллеры ресурсов в одну иерархию, тогда как в cgroups v1 у каждого ресурса — CPU, памяти, I/O — была своя отдельная иерархия. Большинство современных дистрибутивов Linux по умолчанию используют cgroups v2.
cgroups v1 была исходной реализацией. Она работала, но с фундаментальной проблемой в устройстве: ресурсы одного контейнера оказывались разбросаны по разным контрольным группам. Управлять ими согласованно было сложно, а некоторые пограничные случаи было трудно обработать корректно.
cgroups v2 исправила это, собрав всё в единую иерархию. Все ограничения ресурсов контейнера живут в одном месте, поэтому учёт ресурсов стал чище, согласованнее и понятнее.

Kubernetes тоже перешёл на поддержку cgroups v2. Так что если у вас свежее ядро, контейнерами под капотом почти наверняка управляет именно cgroups v2.
Как Docker использует cgroups?
Docker переводит лимиты ресурсов, которые вы задаёте контейнеру, в конфигурацию cgroup на хосте, а дальше их автоматически применяет ядро. Например, при запуске:
docker run --memory=512m --cpus=1.5 nginxDocker создаёт cgroup для этого контейнера и записывает лимиты в ядро. С этого момента ядро само следит за их соблюдением. Docker не нужно надзирать за контейнером: это делает ядро на самом низком уровне.
В этом ключевая идея cgroups: ограничения применяются не в пространстве пользователя (userspace), а в ядре. Поэтому они быстрые, надёжные, и контейнер не может их обойти.
Namespaces и cgroups объясняют, как контейнеры получают изоляцию и контроль ресурсов. Но одна изоляция ещё не делает контейнеры безопасными. Чтобы понять реальные границы безопасности, нужно посмотреть на модель безопасности контейнеров.
Насколько безопасны контейнеры Docker?
Контейнеры Docker изолированы слабее виртуальных машин, потому что все контейнеры на машине делят одно ядро хоста: уязвимость ядра затрагивает их все. Поэтому поверх namespaces и cgroups работают дополнительные слои защиты — урезание capabilities, rootless-режим, фильтр системных вызовов seccomp и модули AppArmor и SELinux.
- Главный риск: общее ядро
- Слои защиты: capabilities, rootless, seccomp, AppArmor/SELinux
- Принцип: эшелонированная защита
Общее ядро — самый большой компромисс в безопасности, на который контейнеры идут по сравнению с виртуальными машинами. Уязвимость ядра затрагивает не один контейнер, а все контейнеры на этом хосте. Поэтому Kubernetes, Docker и само ядро Linux добавляют над namespaces и cgroups дополнительные слои безопасности, чтобы сделать контейнеры безопаснее на практике.

Чем изоляция контейнера отличается от изоляции виртуальной машины?
Виртуальная машина запускает собственное ядро, и гипервизор ставит жёсткую границу между нагрузками. Контейнеры делят ядро хоста, поэтому их изоляция легче и слабее: эксплойт ядра может затронуть весь хост, а не одну нагрузку.
| Свойство | Виртуальная машина | Контейнер |
|---|---|---|
| Ядро | Своё у каждой ВМ | Общее ядро хоста |
| Кто изолирует | Гипервизор | Механизмы ядра: namespaces, cgroups и др. |
| Эксплойт ядра | Остаётся внутри ВМ | Может затронуть весь хост |
| Изоляция | Сильнее по устройству | Легче и слабее |
| Цена | Своя ОС, больше памяти, медленная загрузка | Быстрый старт, меньше накладных расходов |
Это не делает контейнеры небезопасными. Это значит, что у них другая модель безопасности. Виртуальные машины по своему устройству дают более сильную изоляцию, а контейнеры меняют часть изоляции на скорость и эффективность.
Понимание этого компромисса и позволяет принимать правильные инфраструктурные решения.
Как Docker урезает привилегии контейнера?
Docker урезает привилегии через Linux capabilities: при старте контейнера он по умолчанию отбирает у процесса многие capabilities. Поэтому даже контейнер, работающий от root, не может загружать модули ядра или менять сетевые интерфейсы хоста.
Root внутри контейнера — не то же самое, что root на хосте. Но без дополнительных ограничений разница неприятно мала.
Процессы Linux несут набор capabilities, которые определяют, что им разрешено делать: например, занимать привилегированные порты, загружать модули ядра и менять сетевые интерфейсы. У полностью привилегированного процесса есть все capabilities, у ограниченного — только нужные. Docker убирает опасные capabilities ещё до старта контейнера.
Точнее настроить набор можно двумя флагами:
--cap-dropубирает capability, которая иначе была бы у контейнера;--cap-addвыдаёт capability, которой у контейнера по умолчанию нет.
Принцип тот же, что и везде в безопасности: давайте процессу только то, что нужно для его работы, и ничего сверх этого.
Что такое rootless-контейнеры?
Rootless-режим позволяет запускать демон Docker и его контейнеры без root-прав на хосте. Это следующий шаг после урезания привилегий: в сочетании с user namespaces root внутри контейнера сопоставляется с непривилегированным пользователем на хосте.
В стандартной установке Docker dockerd работает от root, а значит, взлом демона — это взлом хоста. Rootless-режим меняет это: демон и все контейнеры работают от непривилегированного пользователя.
Но у компромисса есть реальная цена. У rootless-контейнеров есть ограничения:
- часть сетевых возможностей недоступна;
- некоторые операции с cgroups требуют дополнительной настройки.
Некоторые окружения ставят безопасность выше удобства. В них rootless-режим уменьшает радиус поражения при побеге из контейнера.
Что такое seccomp в Docker?
Seccomp (secure computing mode) — механизм ядра Linux, который фильтрует системные вызовы, разрешённые контейнеру. Docker применяет профиль seccomp по умолчанию к каждому контейнеру; этот профиль блокирует около 44 системных вызовов, которые редко нужны приложениям, но часто используются в атаках.
Даже без лишних capabilities процесс контейнера всё ещё может сделать широкий набор системных вызовов Linux (syscalls). Большинство из них типичным приложениям никогда не нужны, но они остаются доступными, и вредоносный процесс может использовать их для атаки на ядро.
Когда контейнер пытается сделать заблокированный системный вызов, ядро его не выполняет. В профиле Docker по умолчанию такой вызов просто завершается ошибкой «операция не разрешена», а профиль можно настроить и на немедленное завершение процесса. В обоих случаях опасный вызов до ядра не доходит.
Для нагрузок с особыми требованиями можно применять собственные профили seccomp: доверенным контейнерам могут понадобиться более мягкие ограничения, чувствительным — более строгие.
Что такое AppArmor и SELinux?
AppArmor и SELinux — модули безопасности Linux, которые добавляют ещё один слой защиты поверх namespaces, cgroups и seccomp. Seccomp фильтрует отдельные системные вызовы, а AppArmor и SELinux применяют более широкие политики поведения процесса и доступа к системе.
Они определяют:
- какие файлы процесс может читать;
- какие сетевые операции ему разрешены;
- как он может взаимодействовать с другими процессами.
Docker применяет профиль AppArmor по умолчанию в системах, где AppArmor доступен. В системах на базе Red Hat ту же роль играет SELinux. Оба работают независимо от Docker, и их политики применяет напрямую ядро.
Эта независимость важна: даже если Docker неправильно настроен или скомпрометирован, собственные политики безопасности ядра остаются в силе.
Ни один слой по отдельности не делает контейнеры безопасными. Безопасность контейнеров держится на их сочетании: namespaces, урезание capabilities, rootless-режим, seccomp и модули безопасности ядра вместе образуют эшелонированную защиту (defense in depth).
С моделью безопасности разобрались. Следующий вопрос — что на самом деле работает внутри контейнера. Всё начинается с образа, и здесь становится интересно: как образы собираются и хранятся.
Что такое образ Docker?
Образ Docker (container image) — стек слоёв файловой системы, доступных только для чтения; каждый слой описывает набор изменений файловой системы. При запуске контейнера Docker объединяет слои в единое представление с помощью OverlayFS и добавляет сверху один записываемый слой.
- Состоит из: read-only слоёв
- Объединение слоёв: OverlayFS
- Идентификация: SHA-256 digest
- Стандарт: OCI Image Spec
Любой контейнер начинается с образа. Но образ — это не zip-архив и не снимок виртуальной машины: он ведёт себя иначе, чем то и другое.
Если понять, как устроены образы, меняется то, как вы их собираете и отлаживаете. Это же объясняет, почему одни шаблоны Dockerfile собираются быстрее других.
Из чего состоит образ Docker?
Образ Docker состоит из стека слоёв только для чтения, где каждый слой — набор изменений файловой системы. Добавление файла создаёт слой. Установка пакета создаёт слой. Копирование кода приложения создаёт слой.

При сборке образа слои не сливаются в одну большую файловую систему — они остаются раздельными. Когда Docker запускает контейнер из образа, он объединяет все слои в одно общее представление прямо во время выполнения. Этим объединением занимается OverlayFS.
Как OverlayFS объединяет слои образа?
OverlayFS — файловая система Linux, которая берёт несколько каталогов и показывает их как один общий каталог. Docker с её помощью объединяет все слои образа только для чтения в одну целостную файловую систему, которой пользуется контейнер.

Во время работы это выглядит так:
- все слои образа только для чтения лежат внизу стека;
- при старте контейнера Docker добавляет сверху один записываемый слой;
- чтение идёт вниз по стеку, пока файл не найдётся;
- запись попадает только в верхний записываемый слой и никогда не затрагивает слои только для чтения под ним.
Когда контейнер изменяет файл, OverlayFS сначала копирует его из слоя только для чтения в записываемый слой (copy-up), и контейнер меняет уже копию. Исходный файл в слое только для чтения остаётся нетронутым.
Записываемый слой временный: удалите контейнер — и он исчезнет. Слои только для чтения остаются без изменений и готовы для следующего контейнера.
Как работают общие слои и кэш сборки Docker?
Слои образа доступны только для чтения и адресуются по содержимому, поэтому образы на одном хосте могут делить их между собой. Если десять контейнеров используют один базовый образ, его слои хранятся на диске один раз: каждый контейнер делит общие слои и получает поверх свой записываемый слой.
Docker не дублирует базовый слой десять раз. На этом же держится кэш сборки Docker.
При сборке образа Docker сверяет каждую инструкцию Dockerfile со своим кэшем. Если с прошлой сборки ничего не изменилось, он переиспользует слой из кэша, а не собирает его заново. Как только что-то меняется, Docker инвалидирует этот слой и все последующие.
Последний пункт важнее, чем думает большинство инженеров. Если скопировать исходный код в начале Dockerfile, даже маленькая правка кода заставит Docker заново устанавливать все зависимости. А если сначала установить зависимости и копировать исходный код последним, Docker пересоберёт только изменившиеся слои.
Порядок слоёв — не вопрос стиля. Он напрямую влияет на скорость сборки.
Что такое спецификация образов OCI?
Спецификация образов OCI (OCI Image Spec) — стандартный формат образов контейнеров от Open Container Initiative. Благодаря ей образ, собранный Docker, без изменений запускается в containerd, CRI-O, Podman и других OCI-совместимых средах выполнения.
Docker не только популяризировал контейнеры, но и задал формат образов, которому теперь следует вся экосистема. Что полезно знать об устройстве OCI-образов:
- Каждый слой идентифицируется digest'ом
sha256— хешем содержимого слоя. Если у двух слоёв одинаковый digest, они гарантированно идентичны. - Образы адресуются по содержимому: digest служит одновременно идентификатором и проверкой целостности.
- При скачивании из реестра Docker проверяет digest каждого слоя.
- Один тег образа может указывать на разные сборки для разных архитектур. Скачав nginx на машине amd64 и на машине arm64, вы получите разные слои, но один тег. Это сопоставление прозрачно обеспечивает manifest list.
Эти свойства делают OCI-образы переносимыми, проверяемыми и эффективными в распространении.
Слоёная модель — не просто деталь реализации. Именно она делает образы Docker быстрыми в сборке, экономными в хранении и переносимыми между любыми OCI-совместимыми средами выполнения.
Мы разобрались, как образы устроены внутри. Теперь нужно понять, как Docker их собирает.
Что такое Dockerfile и как по нему собирается образ?
Dockerfile — текстовый файл с инструкциями, по которому Docker собирает образ: каждая инструкция говорит, что добавить, установить или настроить. Результат — воспроизводимый переносимый образ, который при каждом запуске ведёт себя одинаково. Dockerfile — это рецепт, а образ — результат.
- Создают слои: FROM, RUN, COPY, ADD
- Задают метаданные: ENV, EXPOSE, CMD, ENTRYPOINT
- Движок сборки: BuildKit
Какие инструкции Dockerfile создают слои?
Слои файловой системы в Dockerfile создают инструкции FROM, RUN, COPY и ADD, а ENV, EXPOSE, CMD и ENTRYPOINT только задают метаданные. Понимание этой разницы помогает писать более компактные и быстрые Dockerfile.
Инструкции, которые создают слои:
FROMзадаёт базовый образ. С него начинается любой Dockerfile, и все последующие слои строятся поверх него.RUNвыполняет команду во время сборки. Установка пакетов, компиляция кода, запуск скриптов — всё это идёт черезRUN. Каждая инструкцияRUNсоздаёт новый слой.COPYиADDкопируют файлы с локальной машины в образ. Каждая из них создаёт слой с этими файлами.
Инструкции, которые задают метаданные:
ENVзадаёт переменные окружения, доступные во время работы.EXPOSEдокументирует, какие порты слушает контейнер.CMDиENTRYPOINTопределяют, что запускается при старте контейнера. Слоёв файловой системы они не создают, но для поведения контейнера критичны.
Каждая инструкция RUN создаёт новый слой, поэтому объединение команд помогает уменьшить образ и число слоёв.
Как ускорить сборку Docker-образа за счёт кэша слоёв?
Кэш сборки Docker переиспользует неизменившиеся слои, но любое изменение инвалидирует слой и все слои после него. Поэтому сначала копируйте файлы зависимостей, затем устанавливайте зависимости и только в конце копируйте исходный код приложения.
Кэш сборки — одна из самых полезных возможностей процесса сборки и одновременно одна из тех, что проще всего незаметно сломать. Docker проверяет инструкции по порядку. Если слой не изменился с прошлой сборки, Docker берёт его из кэша. Как только слой меняется, Docker инвалидирует его и пересобирает все слои после него. Значит, порядок инструкций в Dockerfile имеет огромное значение.
Простое правило:
- Скопируйте файлы зависимостей.
- Установите зависимости.
- Скопируйте исходный код приложения последним.
Порядок важен, потому что зависимости меняются намного реже исходного кода. Если копировать исходный код до установки зависимостей, каждая правка кода вызывает полную переустановку. Если же зависимости установлены раньше, Docker пересобирает только шаги начиная с копирования кода. Сборки остаются быстрыми даже по мере роста кодовой базы.
Лишняя инвалидация кэша — одна из самых частых причин, почему сборка Docker кажется медленной.
Что такое multi-stage сборка в Docker?
Multi-stage сборка (многоэтапная сборка) — приём, при котором в одном Dockerfile несколько инструкций FROM, и каждая открывает отдельный этап. Первый этап собирает приложение, а в финальный образ копируется только скомпилированный результат, поэтому инструменты сборки не попадают в продакшн.
Частая проблема Dockerfile — инструменты сборки оказываются в продакшн-образе. Компиляторы, тестовые фреймворки и зависимости сборки нужны во время сборки, но в финальном продакшн-образе им не место: они увеличивают размер и расширяют поверхность атаки.

Multi-stage сборка решает это чисто. Первый этап отвечает за сборку, второй описывает финальный образ. Вы копируете из этапа сборки только результат компиляции, а всё остальное остаётся позади.
Итог — маленький чистый продакшн-образ, в котором есть только то, что нужно приложению для работы.
Что такое BuildKit?
BuildKit — современный движок сборки Docker, который в последних версиях Docker заменил старый сборщик и включён по умолчанию (в Docker Engine — начиная с версии 23.0). Он выполняет независимые шаги параллельно, точнее кэширует, умеет передавать секреты без записи в слои и генерирует аттестации сборки.
По сравнению со старым сборщиком BuildKit лучше в нескольких важных аспектах:
- Параллельное выполнение. Независимые шаги сборки идут параллельно, и сборка занимает меньше времени.
- Лучшее кэширование. Поведение кэша точнее и согласованнее в разных окружениях.
- Работа с секретами. Секреты — API-ключи или учётные данные — можно передать в сборку, не запекая их ни в один слой. Секрет доступен во время сборки, но никогда не записывается в образ.
- Аттестации сборки. Современный BuildKit умеет генерировать SBOM (Software Bill of Materials, перечень компонентов ПО) и метаданные о происхождении (provenance). Эти записи показывают, что вошло в образ и как сборка его создала, и повышают безопасность цепочки поставок.
BuildKit включён по умолчанию, но понимание того, что он делает под капотом, помогает использовать его в полную силу.
Хорошо написанный Dockerfile держит образы маленькими, а сборку быстрой. Плохие практики увеличивают размер образа, замедляют сборку и создают риски безопасности.
Пока образ существует только локально. Следующий шаг — понять, как Docker его хранит и распространяет.
Что такое реестр образов Docker?
Реестр образов (container registry) — система хранения и распространения образов контейнеров, через которую собранный образ попадает на продакшн-сервер, в кластер Kubernetes или на машину коллеги. Реестр по умолчанию — Docker Hub; тот же протокол поддерживают реестры облачных провайдеров и Git-хостингов, а также приватные реестры.
- По умолчанию: Docker Hub
- Облачные: реестры облачных провайдеров
- Свой: open-source проект distribution
После сборки образ нужно доставить туда, где он будет работать. Для этого и существуют реестры образов.

Реестры не просто хранят образы — они управляют тем, как образы перемещаются между системами. Каждый раз, скачивая образ, Docker загружает и собирает его слой за слоем.
Прежде чем контейнер стартует на новой машине, Docker может понадобиться сначала скачать образ из реестра. Этот процесс устроен сложнее, чем загрузка одного архива.
Как работает docker pull?
docker pull скачивает не один файл, а набор независимо проверяемых слоёв: Docker запрашивает у реестра манифест образа, загружает только те слои, которых нет локально, и сверяет SHA-256 digest каждого слоя. Несовпадение digest'а останавливает загрузку до запуска чего-либо.
Что происходит при выполнении docker pull nginx:
- Docker обращается к реестру и запрашивает манифест образа.
- Манифест описывает образ: его слои, их digest'ы и целевую архитектуру.
- Docker проверяет, какие слои уже есть на локальной машине.
- Скачиваются только недостающие слои.
- У каждого слоя есть digest
SHA-256— уникальный отпечаток, вычисленный по содержимому слоя. - После загрузки Docker сверяет digest и так убеждается, что слой дошёл целым и не был изменён по дороге.
Шаг проверки важен. Слои адресуются по содержимому, поэтому Docker сразу обнаруживает повреждение или подмену при передаче. Несовпадение digest'а означает, что загрузка падает до того, как что-либо запустится.
Дедупликация слоёв работает и здесь. Если два образа делят базовый слой, Docker скачивает его один раз, и все образы, которые его используют, ссылаются на одну локальную копию. Общие слои экономят место на диске и ускоряют загрузку.
Но в продакшне мало эффективной загрузки: нужно ещё точно знать, какую версию образа скачивает Docker. Для этого Docker использует теги образов — человекочитаемые метки вроде nginx:latest, python:3.12 или node:20. Проблема в том, что теги изменяемы.
Почему тег latest опасен в продакшне?
Тег latest — изменяемый указатель, а не версия: при каждом push он переезжает на новый образ. Скачав nginx:latest сегодня, можно получить не тот образ, что полгода назад, поэтому деплой становится невоспроизводимым. Решение — закреплять конкретный тег версии, а лучше digest образа.
Любой тег образа — указатель. Отправляя nginx:latest, вы говорите реестру перевести этот тег на ваш новый образ. Сам тег не несёт информации о версии — это просто метка.
В продакшне это создаёт реальную проблему. Обновление базового образа может незаметно изменить поведение приложения без единой правки кода с вашей стороны.
Исправление — закрепить конкретный тег версии или, ещё лучше, сразу digest:
nginx@sha256:a3b2c1...Digest SHA-256 однозначно идентифицирует конкретный образ. В отличие от тегов, digest'ы никогда не переезжают, потому что навсегда привязаны к содержимому образа. Деплой становится воспроизводимым, что бы ни случилось с тегом.
Конечно, воспроизводимым образам тоже нужно где-то жить, и в зависимости от задачи реестр может быть публичным или полностью приватным.
Какой реестр образов выбрать: публичный или приватный?
Публичный реестр вроде Docker Hub удобен для open-source проектов и базовых образов, а для проприетарных образов приложений правильный выбор — приватный реестр. У большинства облачных провайдеров есть управляемые приватные реестры с контролем доступа, сканированием уязвимостей и георепликацией из коробки.
Docker Hub по умолчанию публичный. У каждого облачного провайдера есть свой реестр, интегрированный с остальными его сервисами. Если нужен полный контроль, можно поднять собственный реестр на open-source проекте distribution.
Реестр стоит между пайплайном сборки и средой выполнения. Правильные теги и закрепление digest'ов делают деплой предсказуемым и защищают от неожиданных изменений в продакшне.
Теперь образ наконец на машине. Но сам по себе образ ничего не делает. Настоящая история начинается, когда Docker превращает его в работающий контейнер, — с жизненного цикла контейнера.
Какой жизненный цикл у контейнера Docker?
Жизненный цикл контейнера Docker состоит из четырёх состояний: created (создан), running (работает), stopped (остановлен) и removed (удалён). Каждое состояние имеет чёткий смысл, а каждый переход запускается конкретным действием — docker create, docker start, docker stop или docker rm.
- Состояния: created, running, stopped, removed
- Процесс контейнера: PID 1
- Политика перезапуска по умолчанию: no
Образ, лежащий в реестре вроде Docker Hub, сам по себе ничего не делает. Как только Docker превращает его в работающий контейнер, тот проходит предсказуемую последовательность состояний. Зная её, проще отлаживать контейнеры и понимать сбои.

- Created. Контейнер существует, но ещё не запущен. Docker подготовил файловую систему и выделил ресурсы, но ни один процесс не работает.
- Running. Процесс контейнера активно работает и выполняет задачи приложения. В этом состоянии контейнеры проводят большую часть жизни.
- Stopped. Процесс контейнера завершился — штатно или из-за сбоя. Контейнер всё ещё существует на диске, и его можно перезапустить.
- Removed. Контейнер полностью удалён. Его записываемый слой — временная файловая система, которой контейнер пользовался во время работы, — исчез. Все данные, не сохранённые в volume, потеряны навсегда.
Почему важно, какой процесс работает в контейнере как PID 1?
PID 1 в Linux отвечает за корректную обработку сигналов и уборку осиротевших дочерних процессов, а обычные приложения к этой роли не готовы. Если процесс с PID 1 игнорирует SIGTERM, Docker выжидает тайм-аут и убивает контейнер через SIGKILL без корректного завершения. Решение — маленькая init-система вроде tini.
При старте контейнера то, что вы указали как entrypoint, работает внутри контейнера как PID 1. Звучит как мелочь, но у неё реальные последствия.
В Linux PID 1 обрабатывается не так, как обычные процессы. Сигналы — это сообщения, которые говорят процессу, что делать. Например:
SIGTERMпросит процесс корректно завершиться (graceful shutdown), чтобы он закончил текущую работу и освободил ресурсы;SIGKILLпринудительно останавливает процесс немедленно, без уборки.
PID 1 также убирает осиротевшие дочерние процессы. Когда дочерний процесс завершается, Linux ожидает, что его корректно уберёт родитель. Если родителя уже нет, эта обязанность переходит к PID 1.
Проблема в том, что большинство приложений никогда не проектировались для работы в роли PID 1. Сервер на Node.js, приложение на Python или бинарник на Go созданы для логики приложения, а не для того, чтобы вести себя как init-система.
Поэтому, когда Docker отправляет SIGTERM, чтобы корректно остановить контейнер, многие приложения игнорируют сигнал или обрабатывают его неправильно. Docker ждёт завершения процесса, выходит по тайм-ауту (по умолчанию — 10 секунд) и отправляет SIGKILL. Контейнер обрывается вместо чистого завершения.
Для этого и существуют маленькие init-системы вроде tini. Они работают как PID 1, правильно обрабатывают сигналы, убирают осиротевшие процессы и передают управление вашему приложению. Одна строка в Dockerfile полностью решает проблему; у docker run для этого есть и флаг --init.
Какие политики перезапуска есть в Docker?
Политика перезапуска (restart policy) говорит Docker, перезапускать ли остановившийся контейнер. Вариантов четыре: no (по умолчанию), on-failure, always и unless-stopped. Контейнеры задуманы заменяемыми, а не ценными, поэтому автоматический перезапуск — нормальная часть их жизни.
- no — никогда не перезапускать. Значение по умолчанию.
- on-failure — перезапускать, только если контейнер завершился с ненулевым кодом ошибки.
- always — перезапускать всегда, как бы контейнер ни остановился.
- unless-stopped — перезапускать всегда, если только вы не остановили его вручную.
Но политика перезапуска говорит только о том, работает ли контейнер, и ничего — о том, здоров ли приложение внутри. Контейнер может работать и при этом отвечать ошибками на каждый запрос. Для этого существуют проверки здоровья (health checks): они позволяют Docker обнаружить сломанное состояние приложения и отреагировать на него независимо от того, жив ли сам процесс.
И ещё одно, что стоит помнить: записываемый слой контейнера временный. Когда контейнер удаляют, всё записанное внутри исчезает. Данным, которые должны пережить контейнер, место в volume, а не внутри контейнера.
Жизненный цикл ясен. Посмотрим, как Docker запускает ваше приложение.
Что происходит при выполнении docker run?
Команда docker run проходит шесть шагов: CLI отправляет запрос в dockerd, dockerd при необходимости скачивает образ и передаёт работу containerd, containerd готовит bundle и запускает shim, shim вызывает runc, runc создаёт namespaces и cgroups и стартует процесс, Docker настраивает сеть, а shim дальше следит за контейнером.
- Docker CLI отправляет API-запрос в dockerd.
- dockerd проверяет образ и передаёт создание контейнера containerd.
- containerd запускает containerd-shim.
- runc создаёт изолированное окружение и запускает процесс.
- Docker подключает контейнер к сети.
- Shim следит за работающим контейнером.
Вы уже знаете отдельные части: namespaces, cgroups, образы, слои и стек среды выполнения. Но изучать компоненты по отдельности и видеть, как они работают вместе, — не одно и то же. Этот раздел связывает весь процесс.

Вот что происходит на машине с момента выполнения docker run до момента, когда приложение начинает работать.
Шаг 1. Куда Docker CLI отправляет команду docker run?
Docker CLI превращает docker run nginx в API-запрос и отправляет его демону Docker через /var/run/docker.sock. На этом роль CLI заканчивается: дальше работа уходит глубже в стек среды выполнения.
dockerd получает запрос и проверяет, есть ли образ на машине. Если образ доступен локально, процесс сразу идёт дальше. Иначе Docker скачивает его из реестра — только недостающие слои.
Шаг 2. Что dockerd передаёт в containerd?
dockerd, когда образ готов, поручает создание контейнера containerd. Здесь слой Docker API отходит в сторону, и управление берёт слой среды выполнения.
containerd готовит так называемый bundle. Bundle — каталог, который создаётся для среды выполнения контейнера и состоит из двух основных частей:
- корневой файловой системы, собранной из слоёв образа с помощью OverlayFS;
- файла конфигурации, который описывает, как должен работать контейнер.
Конфигурация включает namespaces, cgroups, монтирования, сетевые настройки и лимиты ресурсов.
Шаг 3. Зачем containerd запускает shim?
containerd перед стартом контейнера запускает процесс containerd-shim — лёгкий вспомогательный процесс, который остаётся привязанным к контейнеру после запуска. Shim становится родительским процессом контейнера и держит его живым независимо от самого containerd.
Благодаря этому разделению containerd можно перезапускать или обновлять, не прерывая работающие контейнеры. Когда shim готов, он вызывает компонент, который создаёт контейнер.
Шаг 4. Как runc создаёт контейнер?
runc получает от shim подготовленный bundle, читает конфигурацию и готовит окружение контейнера. На этом шаге напрямую включается ядро Linux:
- namespaces изолируют от хост-системы процессы, сеть, монтирования, имена хостов и межпроцессное взаимодействие;
- OverlayFS собирает корневую файловую систему контейнера из слоёв образа;
- cgroups применяют лимиты CPU, памяти и дискового I/O;
- Linux capabilities убирают ненужные привилегии;
- seccomp блокирует чувствительные системные вызовы, которые контейнер никогда не должен делать.
Когда окружение готово, runc запускает процесс контейнера. Этот процесс становится PID 1 в PID namespace контейнера. С этого момента контейнер существует в системе как изолированный процесс.
После запуска runc завершается: его работа сделана.
Шаг 5. Как Docker настраивает сеть контейнера?
Docker, когда процесс контейнера уже работает, настраивает сеть: создаёт виртуальную Ethernet-пару, которая работает как кабель между хостом и контейнером. Один её конец подключён к network namespace контейнера, другой — к виртуальному мосту Docker на хосте.
Затем Docker назначает контейнеру IP-адрес и устанавливает нужные правила iptables для маршрутизации трафика.
Если опубликовать порт через -p 8080:80, Docker создаёт правило перенаправления: трафик, приходящий на порт 8080 хоста, перенаправляется на порт 80 внутри контейнера. На этом шаге контейнер становится доступен извне.
Шаг 6. Кто следит за контейнером после запуска?
containerd-shim наблюдает за процессом контейнера после запуска: управляет потоками ввода и вывода, отслеживает код завершения и сообщает об изменениях состояния в containerd. Контейнер полностью активен, приложение работает.
Сила этой последовательности не в каком-то одном шаге, а в слоёном устройстве под ней. Каждый компонент отвечает за свою задачу:
- namespaces обеспечивают изоляцию;
- cgroups контролируют потребление ресурсов;
- OverlayFS собирает файловую систему;
- shim держит контейнеры живыми независимо от верхних слоёв среды выполнения.
В итоге все эти слои работают вместе, чтобы запустить изолированный процесс контейнера. Вот что происходит за кулисами команды docker run. Теперь посмотрим, как контейнеры общаются друг с другом и с внешними системами.
Как работает сеть в Docker?
Сеть в Docker строится на сетевых драйверах: у каждого контейнера свой network namespace и IP-адрес, а виртуальные Ethernet-пары, мост docker0 и правила iptables связывают контейнеры друг с другом, с хостом и с внешним миром. Для большинства нагрузок подходит пользовательская bridge-сеть.
- По умолчанию: bridge-сеть docker0
- Рекомендуется: пользовательская bridge-сеть
- Другие режимы: host, none, overlay, macvlan
Контейнеры изолированы по устройству: у каждого свой network namespace, IP-адрес и сетевой стек. Но одной изоляции недостаточно. Контейнерам всё равно нужно общаться друг с другом и получать трафик извне, а иногда и обращаться к сервисам на хосте. Эту задачу и решает сеть контейнеров. Docker делает это через разные сетевые драйверы, каждый под свой сценарий.
Что такое bridge-сеть docker0?
docker0 — виртуальный сетевой мост, который Docker создаёт на машине при установке. Мост работает как программный коммутатор на хосте: каждый подключённый к нему контейнер получает свой IP-адрес и может общаться с другими контейнерами на том же мосту.

При старте контейнера Docker создаёт виртуальную Ethernet-пару:
- один конец появляется внутри контейнера как
eth0; - другой подключается к мосту
docker0на хосте.
Трафик идёт через эту пару в обе стороны.
Но у bridge-сети по умолчанию есть важное ограничение: контейнеры не могут находить друг друга по имени и общаются только по IP-адресам. А поскольку IP-адреса контейнеров непостоянны, опора на них быстро делает систему хрупкой.
Зачем нужны пользовательские сети в Docker?
Пользовательская сеть (user-defined network) в Docker позволяет контейнерам обращаться друг к другу по имени, а не по IP-адресу: имена разрешает встроенный DNS-сервер Docker. Контейнер api может обратиться к контейнеру database, просто указав database как имя хоста.
DNS-сервер Docker разрешает имена за кулисами, и уже неважно:
- какой IP-адрес у контейнера базы данных;
- перезапускался ли контейнер;
- изменился ли после этого его IP.
Это стандартный подход для большинства реальных конфигураций. Пользовательские сети также улучшают изоляцию: контейнеры в разных сетях не могут общаться, пока вы явно их не соедините.
Как работает публикация портов в Docker?
Публикация порта флагом -p делает контейнер доступным снаружи хоста: Docker устанавливает на хосте правило iptables, которое перенаправляет входящий трафик с порта хоста на порт внутри контейнера. По умолчанию контейнер в своём network namespace недоступен ни для чего за пределами хоста.
docker run -p 8080:80 nginxВ этом примере:
- 8080 — порт, открытый на хосте;
- 80 — порт, который приложение слушает внутри контейнера.
Когда трафик приходит на порт 8080:
- Docker перенаправляет его на порт 80 внутри контейнера;
- приложение обрабатывает запрос;
- ответ возвращается тем же путём.
Снаружи кажется, что приложение работает прямо на хосте. Внутри контейнера оно по-прежнему слушает порт 80.
Какие ещё сетевые режимы есть в Docker?
Кроме bridge, Docker поддерживает сетевые режимы host, none, overlay и macvlan — каждый под свой сценарий. Для большинства нагрузок правильный выбор — пользовательская bridge-сеть: она даёт разрешение имён через DNS, изоляцию и простую связность без лишней сложности.
| Режим | Как работает | Когда использовать |
|---|---|---|
| Bridge (пользовательская) | Свой мост, DNS по именам контейнеров, изоляция между сетями | Большинство нагрузок |
| Host | Контейнер делит network namespace хоста напрямую: нет виртуального интерфейса и трансляции портов | Максимальная производительность ценой полной потери сетевой изоляции |
| None | У контейнера нет сетевого интерфейса вообще | Нагрузки, которым нельзя иметь доступ к сети |
| Overlay | Распределённая виртуальная сеть между контейнерами на разных хостах | Часто — Docker Swarm |
| Macvlan | Свой MAC-адрес, контейнер выглядит в сети как физическое устройство | Legacy-приложения, которым нужен прямой доступ к сети |
С сетью разобрались. Следующая часть картины — хранилище, а точнее то, как контейнеры управляют данными, которые должны пережить сам контейнер.
Как хранить данные в Docker-контейнере?
Данные Docker-контейнера, которые должны пережить его удаление, хранят вне записываемого слоя одним из трёх механизмов: volume, bind mount или tmpfs mount. Volumes — стандарт для продакшн-данных, bind mounts — для локальной разработки, tmpfs — для секретов, которые не должны попасть на диск.
- Продакшн: volumes
- Разработка: bind mounts
- Секреты в памяти: tmpfs
Контейнеры эфемерны. Когда контейнер удаляют, вместе с ним исчезает записываемый слой, где хранятся изменения, сделанные во время работы. Логи, загруженные файлы и записи базы данных внутри контейнера теряются навсегда.
Для приложений без состояния (stateless) это нормально. Но большинству реальных систем нужно, чтобы данные пережили отдельный контейнер. База данных не может терять записи при каждом перезапуске, а сервис загрузки файлов — терять файлы при каждом деплое.

| Механизм | Где хранятся данные | Переживает удаление контейнера | Когда использовать |
|---|---|---|---|
| Volume | Каталог, которым управляет Docker | Да | Продакшн-данные, базы данных, общие данные нескольких контейнеров |
| Bind mount | Указанный каталог хоста | Да, это файлы хоста | Локальная разработка: код с хоста виден в контейнере сразу |
| tmpfs mount | Оперативная память хоста | Нет, исчезает при остановке | Секреты, токены, временные учётные данные |
Что такое volume в Docker?
Volume — стандартный способ сохранять данные в Docker: Docker управляет им вне файловой системы контейнера, поэтому данные переживают удаление контейнера. Контейнер пишет в volume точно так же, как в любой другой каталог, но volume существует отдельно от контейнера.
Volumes решают и проблему, которая становится очевидной в продакшне: один volume могут одновременно смонтировать несколько контейнеров. Веб-сервер и агент логирования могут работать с одним каталогом, не согласовывая это между собой.
Кроме того, volumes чисто интегрируются с инструментами резервного копирования и миграции Docker. Если данные важны, им место в volume.
Что такое bind mount в Docker?
Bind mount отображает конкретный каталог хоста прямо в контейнер: контейнер видит ровно то, что лежит по этому пути на хосте. Изменения внутри контейнера сразу появляются на хосте, и наоборот.
Поэтому bind mounts очень полезны в разработке. Вы монтируете каталог с исходным кодом в контейнер, и каждое локальное изменение сразу видно внутри работающего контейнера — без пересборок и перезапусков.
Но в продакшне у bind mounts есть реальный риск: неправильно настроенный bind mount может открыть контейнеру чувствительные каталоги хоста. Поэтому продакшн-нагрузки обычно используют volumes, а bind mounts — в основном для локальной разработки.
Что такое tmpfs mount в Docker?
tmpfs mount хранит данные в оперативной памяти хоста, а не на диске, и данные исчезают в момент остановки контейнера. Поэтому tmpfs подходит для чувствительных данных, которые никогда не должны касаться файловой системы: секретов, токенов и временных учётных данных.
Каждый механизм хранения решает свою задачу: volumes сохраняют данные, bind mounts связывают контейнеры с файлами хоста, tmpfs держит чувствительные данные только в памяти. Выбор полностью зависит от того, что это за данные и сколько они должны жить.
Какие сильные стороны и ограничения у Docker?
Docker сделал окружения переносимыми и воспроизводимыми, а слоёные образы и стандарты OCI ускорили сборку и доставку. Главное ограничение — общее ядро хоста: изоляция контейнеров принципиально слабее изоляции виртуальных машин. Кроме того, Docker сам по себе рассчитан на одну машину и плохо справляется с нагрузками на многих серверах.
Docker сделал контейнеры простыми в использовании. Но эта простота скрывает под собой много сложности.
Что Docker делает хорошо?
Главное достижение Docker — переносимые и воспроизводимые окружения. До контейнеров команды поддерживали согласованность окружений вручную: скриптами, документацией и «надеждой». Docker изменил это, сделав окружения переносимыми между разными системами.
Что ещё Docker делает хорошо:
- упаковка приложения с зависимостями в один образ сильно снижает дрейф окружений (environment drift);
- слоёный формат образов ускоряет сборку и экономит место за счёт переиспользования закэшированных слоёв;
- стандарты OCI позволяют запускать образы Docker на разных платформах без изменений;
- Docker Compose заметно упростил локальный запуск приложений из нескольких сервисов.
Где Docker не справляется?
Слабое место Docker — общее ядро хоста. Из-за одного этого факта изоляция контейнеров принципиально слабее изоляции виртуальных машин. Для большинства нагрузок такой компромисс приемлем, для чувствительных мультиарендных (multi-tenant) окружений — нет.
Где ещё Docker не дотягивает:
- плохие практики в Dockerfile быстро приводят к раздутым и медленно собирающимся образам;
- root внутри контейнера по-прежнему рискован, потому что контейнеры делят ядро хоста;
- нагрузки с состоянием (stateful) поначалу выглядят просто, но управлять постоянными данными в контейнерах сложно;
- Docker хорошо работает на одной машине, но сам по себе с трудом справляется, когда нагрузка распределена по многим машинам и требует автоматического восстановления.
Последний пункт — ровно то место, где появляется следующий элемент инфраструктуры (о нём — ниже).
Какие есть альтернативы Docker?
Альтернативы Docker — Podman (без демона и по умолчанию без root), containerd (работает под Kubernetes без Docker), CRI-O (лёгкий runtime специально для Kubernetes) и Kata Containers (каждый контейнер в лёгкой виртуальной машине). Все они понимают образы OCI, поэтому образы, собранные Docker, работают в них без изменений.
Docker популяризировал контейнеры. Но он больше не единственная среда выполнения, а во многих современных системах даже не среда по умолчанию.
Экосистема контейнеров движется к модульности: каждый слой стека теперь заменяем, и появились инструменты, которые берут на себя разные части рабочего процесса Docker.
| Инструмент | Чем отличается от Docker | Для чего |
|---|---|---|
| Podman | Вообще без демона: контейнеры работают как обычные процессы без фоновой службы и по умолчанию не требуют root | Замена Docker на машине разработчика и на серверах |
| containerd | Работает прямо под Kubernetes, Docker не участвует вовсе | Одна из самых распространённых сред выполнения в продакшне |
| CRI-O | Лёгкий runtime, в котором только компоненты, нужные для Container Runtime Interface (CRI) | Специально для Kubernetes |
| Kata Containers | Каждый контейнер в лёгкой виртуальной машине — закрывает брешь общего ядра | Нагрузки, которым нужна более сильная изоляция |
Всё это стало возможным благодаря OCI. Все среды выполнения следуют одним открытым стандартам, поэтому образы, собранные Docker, без изменений работают в любой из них. Kubernetes общается с любым совместимым runtime через CRI, не интересуясь, какой именно это runtime.
Docker начал контейнерную революцию, но созданная им экосистема давно переросла его самого.
Какой компонент Docker за что отвечает?
Каждый механизм Docker решает одну задачу: стек среды выполнения управляет жизненным циклом контейнера, namespaces ограничивают видимость, cgroups — потребление ресурсов, слоёные образы дают переносимость, а сеть и хранилище построены на обычных примитивах Linux. Все они существуют ради одного: изолировать процессы, которые делят одно ядро.
| Механизм | За что отвечает | Из чего построен |
|---|---|---|
| Стек среды выполнения | Полный жизненный цикл контейнера | dockerd, containerd, containerd-shim, runc |
| Namespaces | Что контейнер видит: процессы, сеть, файловая система | PID, NET, MNT, UTS, IPC, USER |
| cgroups | Сколько контейнер потребляет | Лимиты CPU, памяти, дискового I/O |
| Защита | Что контейнеру разрешено делать | Capabilities, seccomp, AppArmor/SELinux, rootless |
| Образы | Переносимость, экономное хранение, быстрая доставка | Read-only слои, OverlayFS, SHA-256 digest, OCI |
| Сеть | Связь контейнеров между собой и с внешним миром | veth-пары, мост docker0, iptables, встроенный DNS |
| Хранилище | Данные, которые переживают контейнер | Volumes, bind mounts, tmpfs |
Docker кажется простым на поверхности: вы запускаете команду, контейнер стартует, и приложение ведёт себя одинаково почти везде. Но под этой простотой работает стек примитивов Linux.
Когда понимаешь эти слои, поведение контейнеров становится намного понятнее. Контейнеры — это просто процессы Linux, работающие в тщательно контролируемом окружении.
Это понимание становится важным в продакшне. Когда контейнеры падают, потребляют слишком много ресурсов или ведут себя по-разному в разных окружениях, корень проблемы обычно лежит где-то в нижних слоях.
Как применять устройство Docker на практике?
Знание устройства Docker на практике помогает быстро находить причину сбоя: почти у каждого типичного симптома — долгой остановки контейнера, медленной сборки, потерянных данных, недоступного соседнего сервиса — есть конкретный механизм-виновник и стандартное решение.
| Симптом | Причина | Решение |
|---|---|---|
docker stop ждёт около 10 секунд, контейнер обрывается | Процесс с PID 1 не обрабатывает SIGTERM, Docker добивает его через SIGKILL | tini или docker run --init, обработка SIGTERM в приложении |
| Внутри контейнера копятся процессы-зомби | PID 1 не убирает осиротевшие дочерние процессы | Init-система вроде tini в роли PID 1 |
| Контейнер внезапно убит, код выхода 137 | Превышен лимит памяти cgroup, сработал OOM killer (137 = 128 + номер сигнала SIGKILL) | Пересмотреть --memory или потребление памяти приложением |
| Каждая правка кода пересобирает зависимости | Код копируется до установки зависимостей, кэш слоёв инвалидируется | Сначала файлы зависимостей и их установка, код — последним |
| Продакшн-образ весит сотни мегабайт | В образе остались компиляторы и инструменты сборки | Multi-stage сборка |
| Секрет виден в истории образа | Секрет передан через COPY или ENV и запечён в слой | Секреты BuildKit при сборке, tmpfs во время работы |
| Деплой без изменений кода повёл себя иначе | Тег latest или другой изменяемый тег переехал на новый образ | Закрепить версию или digest @sha256:… |
| Контейнеры не находят друг друга по имени | Контейнеры в bridge-сети по умолчанию, где нет DNS по именам | Пользовательская сеть |
| Данные пропали после пересоздания контейнера | Данные лежали в записываемом слое контейнера | Volume |
| Контейнер «работает», но отвечает ошибками | Политика перезапуска видит только живой процесс | Health check |
| Приложению нужна одна привилегия, например сетевая | Docker по умолчанию отбирает многие capabilities | Точечный --cap-add вместо --privileged |
Какие ошибки чаще всего допускают при работе с Docker?
- Считают контейнер таким же изолированным, как виртуальная машина, и запускают в соседних контейнерах недоверенные нагрузки разных клиентов.
- Запускают процессы в контейнере от root без user namespaces или rootless-режима.
- Хранят важные данные в записываемом слое, а не в volume.
- Используют bind mounts в продакшне и случайно открывают контейнеру чувствительные каталоги хоста.
- Деплоят по тегу
latest. - Ломают кэш сборки порядком инструкций и тащат инструменты сборки в финальный образ.
- Полагаются на политику перезапуска вместо проверок здоровья.
Что делать, когда контейнеров становится тысяча?
Оркестратор контейнеров нужен, когда приложение распределяется по многим серверам: он планирует размещение контейнеров, восстанавливает упавшие нагрузки, управляет сетью между сервисами и выкатывает обновления без простоя. Docker решает задачу запуска контейнеров на одной машине, а стандартом оркестрации стал Kubernetes.
Запустить один контейнер на одной машине «легко». Сложности начинаются в продакшне: на какую машину поставить следующий контейнер? Кто перезапустит его после падения? Что будет, если упадёт сама машина? Контейнерам нужно находить друг друга на разных хостах, обновления должны выкатываться без простоя, а у неудачного деплоя должен быть безопасный путь отката.
Это не редкие ситуации, а обычные задачи эксплуатации контейнеров. Docker никогда не проектировался, чтобы решать их все в одиночку. Docker изменил то, как приложения упаковываются и доставляются, а Kubernetes — то, как ими управляют в масштабе.
Как устроен Kubernetes и как он оркестрирует контейнеры — в нашей отдельной статье о Kubernetes.
Частые вопросы о Docker
Чем контейнер Docker отличается от виртуальной машины?
Виртуальная машина запускает собственное ядро и полноценную операционную систему, а гипервизор жёстко отделяет её от других машин. Контейнер — обычный процесс Linux, изолированный namespaces и ограниченный cgroups, и все контейнеры на хосте делят одно ядро. Поэтому контейнеры стартуют быстрее и занимают меньше памяти, но изолированы слабее.
Чем Docker отличается от Kubernetes?
Docker собирает образы и запускает контейнеры на одной машине. Kubernetes — оркестратор: он распределяет контейнеры по кластеру серверов, перезапускает упавшие, управляет сетью между сервисами и выкатывает обновления без простоя. Kubernetes не зависит от Docker и запускает контейнеры через совместимые с CRI среды выполнения, например containerd или CRI-O.
Нужен ли Docker для Kubernetes?
Нет. Kubernetes общается со средой выполнения через интерфейс CRI и работает напрямую с containerd или CRI-O; встроенную поддержку Docker Engine (dockershim) убрали в версии 1.24. Образы, собранные Docker, при этом продолжают работать в Kubernetes без изменений, потому что соответствуют стандарту OCI.
Чем containerd отличается от Docker?
containerd — среда выполнения контейнеров, которая управляет их жизненным циклом, скачивает и распаковывает образы. Docker — платформа для разработчика поверх неё: CLI, демон dockerd, сборка образов, сети и volumes. Когда вы выполняете docker run, dockerd передаёт создание контейнера именно containerd, а тот через containerd-shim вызывает runc.
Почему docker stop останавливает контейнер 10 секунд?
docker stop отправляет процессу с PID 1 сигнал SIGTERM и по умолчанию ждёт 10 секунд. Если процесс не обрабатывает сигнал, а приложения часто не рассчитаны на роль PID 1, по истечении тайм-аута Docker отправляет SIGKILL и обрывает контейнер. Помогает init-система tini, флаг --init у docker run или корректная обработка SIGTERM в самом приложении.
Чем CMD отличается от ENTRYPOINT в Dockerfile?
Обе инструкции определяют, что запускается при старте контейнера, и обе задают метаданные, а не слои файловой системы. ENTRYPOINT задаёт основную команду контейнера, а CMD — команду или аргументы по умолчанию, которые легко переопределить аргументами docker run. Если обе указаны в exec-форме, содержимое CMD передаётся в ENTRYPOINT как аргументы.
Чем volume отличается от bind mount в Docker?
Volume хранится в каталоге, которым управляет сам Docker, переживает удаление контейнера и подходит для продакшн-данных. Bind mount отображает в контейнер произвольный каталог хоста: изменения видны сразу с обеих сторон, поэтому его удобно использовать в разработке. В продакшне bind mount рискован: ошибка в пути может открыть контейнеру чувствительные каталоги хоста.
Как Docker работает на Windows и macOS?
Контейнеры Linux требуют ядра Linux, поэтому Docker Desktop на Windows и macOS запускает лёгкую виртуальную машину с Linux, а на Windows может использовать для этого WSL 2. Контейнеры работают внутри этой машины и делят её ядро, а не ядро вашей системы.