Быстрый старт: Docker и Docker Compose

Вводный курс по основам контейнеризации, ориентированный на понимание механизмов изоляции процессов и сборки образов. Вы научитесь упаковывать приложения и координировать работу нескольких сервисов для подготовки к работе с оркестраторами.

1. Фундамент контейнеризации: механизмы изоляции и архитектура Docker

Фундамент контейнеризации: механизмы изоляции и архитектура Docker

Разработчик пишет код на своем ноутбуке с macOS, использует определенные версии библиотек и локальную базу данных. Код работает идеально. Затем этот код передается DevOps-инженеру для развертывания на production-сервере под управлением Ubuntu. Внезапно приложение падает с ошибкой несовместимости версий Python или отсутствующей системной зависимости. Эта классическая проблема доставки ПО десятилетиями решалась с помощью виртуальных машин, пока индустрия не осознала их фатальный недостаток — избыточность.

Виртуальная машина (ВМ) эмулирует аппаратное обеспечение. Чтобы запустить изолированное приложение в ВМ, вам нужен гипервизор (например, KVM или VMware), поверх которого устанавливается полноценная гостевая операционная система со своим ядром, драйверами и фоновыми службами. Если на физическом сервере нужно запустить десять изолированных микросервисов, придется поднять десять виртуальных машин. Это означает десятикратное дублирование ядра ОС и колоссальный расход оперативной памяти (RAM) и процессорного времени (CPU) просто на поддержание работы самих гостевых систем, еще до того, как приложения начнут выполнять полезную работу.

Контейнеризация предлагает принципиально иной подход. Контейнер не виртуализирует железо — он виртуализирует операционную систему.

!Сравнение виртуальных машин и контейнеров

Контейнеры делят одно общее ядро хостовой операционной системы (Linux). Внутри контейнера нет своего ядра, нет эмуляции оборудования. С точки зрения хост-системы, контейнер — это просто еще один обычный процесс, такой же, как запущенный Nginx или SSH-сервер. Разница лишь в том, что этот процесс обернут в жесткие рамки изоляции. Благодаря этому контейнеры запускаются за миллисекунды (им не нужно загружать ОС) и потребляют ровно столько памяти, сколько нужно самому приложению.

Анатомия иллюзии: как Linux создает контейнеры

Docker не изобрел контейнеры. Технологии изоляции существовали в ядре Linux задолго до появления логотипа с синим китом (например, LXC). Инновация Docker заключалась в создании удобного интерфейса и экосистемы стандартизированных образов. Сам же механизм изоляции опирается на три фундаментальных примитива ядра Linux: Namespaces, cgroups и chroot.

Понимание этих механизмов — обязательное требование на технических собеседованиях для DevOps-инженеров, так как именно они объясняют, как диагностировать упавшие сервисы в Kubernetes или Docker.

#### Namespaces (Пространства имен): Изоляция видимости

Namespaces ограничивают то, что может «видеть» процесс. Если запустить приложение в обычном Linux, оно сможет увидеть все остальные процессы, сетевые интерфейсы и файловые системы. Namespaces создают для процесса иллюзию того, что он находится в системе один.

Существует несколько типов пространств имен:

  • PID (Process ID): Изолирует дерево процессов. Внутри контейнера ваше приложение может иметь PID 1 (главный процесс). Но если вы посмотрите на список процессов с хост-системы, вы увидите это же приложение под реальным номером, например, PID 14592.
  • NET (Network): Дает контейнеру собственный сетевой стек — свои IP-адреса, таблицы маршрутизации и порты. Именно поэтому два разных контейнера на одном хосте могут слушать порт 80, не конфликтуя друг с другом (конфликт возникнет только при попытке пробросить оба на один и тот же порт хоста).
  • MNT (Mount): Изолирует точки монтирования файловой системы. Контейнер видит свою собственную корневую директорию / и не имеет доступа к файлам хоста, если это не разрешено явно.
  • UTS (UNIX Time-Sharing): Позволяет контейнеру иметь собственное имя хоста (hostname).
  • USER: Позволяет процессу иметь права root внутри контейнера, оставаясь непривилегированным пользователем на хост-системе. Это важнейший механизм безопасности.
  • !Демонстрация изоляции PID Namespace

    Особое внимание стоит уделить PID 1 внутри контейнера. В мире UNIX процесс с PID 1 (обычно это systemd или init) несет особую ответственность: он должен обрабатывать системные сигналы (например, SIGTERM для корректного завершения) и «усыновлять» осиротевшие дочерние процессы. Если ваше приложение, запущенное в контейнере под PID 1, не умеет правильно обрабатывать сигнал SIGTERM, при команде docker stop контейнер не завершится элегантно. Docker подождет 10 секунд, а затем жестко убьет его сигналом SIGKILL, что может привести к повреждению данных в базе.

    #### cgroups (Control Groups): Ограничение ресурсов

    Если Namespaces ограничивают то, что процесс видит, то cgroups ограничивают то, что процесс может использовать.

    Представьте ситуацию: на сервере запущено два контейнера. В одном из них произошла утечка памяти (memory leak), и приложение начало бесконтрольно потреблять RAM. Без ограничений этот процесс исчерпает всю память физического сервера, что приведет к падению соседнего контейнера, а возможно, и самой хостовой ОС. Эту проблему называют «проблемой шумного соседа» (noisy neighbor).

    Control Groups решают эту задачу, устанавливая жесткие лимиты на:

  • Memory (RAM): Максимальный объем оперативной памяти. Если процесс в контейнере превысит этот лимит, ядро Linux вызовет механизм OOM Killer (Out Of Memory Killer) и принудительно завершит процесс. В логах Docker это будет отображаться как статус OOMKilled.
  • CPU: Доля процессорного времени. Можно ограничить контейнер половиной ядра или задать приоритет (CPU shares) относительно других контейнеров.
  • Block I/O: Ограничение скорости чтения и записи на диск, чтобы один контейнер не забрал на себя всю пропускную способность хранилища.
  • #### chroot и pivot_root: Изоляция файловой системы

    Механизм chroot (change root) появился в UNIX еще в 1979 году. Он позволяет изменить корневую директорию для текущего процесса. Процесс начинает считать, что директория, например, /var/lib/docker/containers/app/, является для него абсолютным корнем /. Он физически не может подняться на уровень выше и прочитать файлы хостовой ОС, такие как /etc/shadow. В современных контейнерах используется более продвинутый и безопасный системный вызов pivot_root, но концепция остается той же.

    Клиент-серверная архитектура Docker

    Многие начинающие инженеры воспринимают команду docker в терминале как монолитную программу, которая делает всё. В реальности Docker имеет клиент-серверную архитектуру, и понимание того, как компоненты общаются между собой, критично для настройки CI/CD пайплайнов (например, при пробросе Docker-сокета в GitLab Runner).

    !Архитектура компонентов Docker

  • Docker Client (CLI): Это та самая утилита docker, которую вы используете в терминале. Сама по себе она не запускает контейнеры. Ее единственная задача — принять вашу команду (например, docker run nginx), сформировать REST API запрос и отправить его демону.
  • Docker Daemon (dockerd): Серверная часть, работающая в фоне на хост-машине. Именно dockerd слушает API-запросы от клиента, управляет образами, сетями и томами. По умолчанию клиент и демон общаются через локальный UNIX-сокет (/var/run/docker.sock), но их можно настроить на общение по сети (TCP), что позволяет управлять контейнерами на удаленном сервере со своего ноутбука.
  • Docker Registry (Реестр): Хранилище образов. Когда демон получает команду запустить контейнер, образ которого отсутствует локально, он обращается к реестру (по умолчанию это публичный Docker Hub) и скачивает его. В корпоративной среде DevOps-инженеры настраивают приватные реестры (например, GitLab Container Registry или Harbor) для хранения проприетарного кода.
  • #### Эволюция под капотом: OCI, containerd и runc

    На заре своего существования dockerd делал всё сам. Сегодня архитектура стала модульной, чтобы соответствовать стандартам OCI (Open Container Initiative). Это частая тема на собеседованиях по Kubernetes.

    Когда dockerd решает запустить контейнер, он не обращается к ядру Linux напрямую. Он передает задачу компоненту containerd. containerd — это высокоуровневый менеджер жизненного цикла контейнеров. Он подготавливает файловую систему и сеть. Затем containerd вызывает runc. runc — это низкоуровневая легковесная утилита (CLI-инструмент), чья единственная задача — взаимодействовать с ядром Linux (создать Namespaces и cgroups) и запустить процесс. Как только процесс запущен, runc завершает свою работу, а процесс остается под наблюдением containerd.

    Почему это важно знать? В 2020 году Kubernetes объявил об отказе от Docker как среды выполнения контейнеров (удаление dockershim). Это вызвало панику, но на деле Kubernetes просто перестал общаться с тяжеловесным dockerd и начал напрямую использовать containerd для запуска тех же самых OCI-совместимых контейнеров. Контейнеры, собранные через Docker, продолжают прекрасно работать в Kubernetes, потому что стандарт их запуска един.

    Образы и Контейнеры: Чертеж и Здание

    Чтобы окончательно закрепить фундамент, необходимо провести строгую границу между образом (Image) и контейнером (Container).

    Docker Image (Образ) — это неизменяемый (read-only) шаблон, содержащий файловую систему, библиотеки, зависимости и сам скомпилированный код приложения. Образ можно сравнить с архитектурным чертежом. Вы не можете жить в чертеже, и чертеж не меняется от того, что вы на него смотрите. Образы строятся слоями: базовый слой ОС (например, Alpine Linux), затем слой с установленным Python, затем слой с вашим кодом.

    Docker Container (Контейнер) — это запущенный экземпляр образа. Это реальное здание, построенное по чертежу. В нем кипит жизнь: работают процессы, потребляется оперативная память. При создании контейнера поверх неизменяемых слоев образа добавляется тонкий слой для чтения и записи (Read-Write layer). Если приложение внутри контейнера создает лог-файл или меняет конфигурацию, эти изменения происходят только в этом верхнем RW-слое.

    Следствие этой архитектуры — эфемерность контейнеров. Если вы удалите контейнер, его Read-Write слой будет уничтожен навсегда, вместе со всеми накопленными внутри данными. Образ при этом останется нетронутым. Если вы запустите новый контейнер из того же образа, он будет абсолютно чистым, как при первом запуске. Эта особенность заставляет инженеров проектировать приложения как stateless (не хранящие состояние локально), а для постоянных данных (например, файлов базы данных PostgreSQL) использовать внешние механизмы хранения, которые подключаются к контейнеру извне.

    Контейнеризация изменила ландшафт ИТ-инфраструктуры, сместив фокус с управления серверами на управление процессами. Понимая, что контейнер — это не магическая коробка, а обычный процесс Linux, ограниченный через Namespaces и cgroups, вы сможете эффективно диагностировать проблемы нехватки ресурсов, сетевой недоступности и прав доступа, с которыми ежедневно сталкивается DevOps-инженер в production-среде.

    2. Анатомия Dockerfile: создание оптимизированных образов и управление слоями

    Анатомия Dockerfile: создание оптимизированных образов и управление слоями

    Два инженера упаковывают одно и то же приложение на Python. У первого получается образ размером 1.2 ГБ, который собирается десять минут. У второго — образ весом 115 МБ, а повторная сборка занимает три секунды. Исходный код приложения идентичен байт в байт. Разница заключается исключительно в понимании того, как Docker читает инструкции, формирует файловую систему и работает с кэшем.

    Слоистая файловая система и Copy-on-Write

    Docker-образ не является монолитным файлом, как ISO-образ диска. Это стопка независимых, доступных только для чтения слоев (read-only layers). Каждый слой представляет собой набор отличий (diff) от предыдущего состояния файловой системы. За объединение этих слоев в единое дерево каталогов, которое видит приложение, отвечает механизм UnionFS (Union File System), реализуемый через драйверы хранения, такие как overlay2.

    Когда инструкция в Dockerfile изменяет файловую систему (добавляет, удаляет или модифицирует файлы), Docker создает новый слой. Инструкции, которые не меняют файлы (например, ENV или EXPOSE), создают метаданные, но не увеличивают физический размер образа на диске.

    !Слоистая архитектура Docker-образа и контейнера

    Такая архитектура опирается на стратегию Copy-on-Write (CoW). Если нескольким контейнерам нужен один и тот же базовый образ (например, ubuntu:22.04), Docker хранит его слои на диске хоста в единственном экземпляре. При запуске контейнера поверх неизменяемых слоев образа добавляется тонкий слой для чтения и записи (Read-Write layer).

    Если процесс внутри контейнера пытается изменить файл, находящийся в нижнем read-only слое, срабатывает механизм CoW:

  • Docker находит файл в нижних слоях.
  • Копирует этот файл в верхний Read-Write слой контейнера.
  • Процесс модифицирует уже скопированную версию.
  • Это объясняет, почему удаление файлов в Dockerfile не всегда уменьшает размер образа. Если в одном слое вы скачали архив весом 500 МБ, а в следующем слое распаковали его и удалили исходный архив, образ все равно будет содержать эти 500 МБ. Файл архива навсегда остался в предыдущем слое, а в новом слое появилась лишь пометка о его удалении (whiteout file), скрывающая его от итоговой файловой системы.

    Разбор ключевых инструкций Dockerfile

    Процесс сборки образа — это последовательное выполнение команд из текстового манифеста Dockerfile. Каждая инструкция имеет свои нюансы, напрямую влияющие на безопасность и производительность.

    FROM: Выбор фундамента

    Сборка всегда начинается с базового образа. Выбор FROM определяет размер отправной точки и поверхность атаки (attack surface).
  • ubuntu или debian — содержат привычный набор утилит, размер около 70–120 МБ. Удобны для отладки, но избыточны для продакшена.
  • alpine — минималистичный дистрибутив на базе musl libc и BusyBox. Весит около 5 МБ. Отличный выбор, но требует осторожности при компиляции C/C++ расширений (например, для Python или Node.js), так как они обычно линкуются с glibc.
  • scratch — абсолютно пустой образ (0 байт). Используется для запуска статически скомпилированных бинарных файлов (например, написанных на Go или Rust), не требующих системных библиотек.
  • RUN: Искусство цепочек

    Инструкция RUN выполняет команды в оболочке (shell) и фиксирует результат в новом слое. Из-за механики слоев, описанной выше, менеджеры пакетов могут стать причиной раздувания образов.

    Плохая практика:

    Каждый RUN создаст отдельный слой. Кэш apt-get update навсегда останется в образе, занимая место. Более того, если позже добавить RUN apt-get install -y git, Docker использует закэшированный слой с update, который мог устареть несколько месяцев назад, и установка завершится ошибкой из-за несовпадения версий пакетов.

    Правильная практика:

    Здесь обновление индексов, установка пакетов и очистка временных файлов происходят в рамках одной инструкции. Создается один оптимизированный слой, не содержащий мусора.

    COPY против ADD

    Обе инструкции копируют файлы с хост-системы внутрь образа. Разница в функциональности: COPY делает ровно то, что от нее ожидают — копирует локальные файлы. ADD обладает скрытой магией: она умеет скачивать файлы по URL и автоматически распаковывать локальные tar-архивы.

    В инженерной практике принято использовать COPY в 99% случаев. Непредсказуемость ADD (например, скачивание файла по URL не позволяет удалить его в том же слое) нарушает прозрачность сборки. Если нужно скачать файл, лучше использовать RUN curl ... && tar ... && rm ....

    CMD и ENTRYPOINT: Точка входа

    Понимание разницы между этими инструкциями — обязательный критерий на технических собеседованиях. Обе инструкции определяют, что запустится при старте контейнера, но работают по-разному.

    Существует две формы записи:

  • Shell-форма: CMD python app.py. Docker запустит это как /bin/sh -c "python app.py". Процесс оболочки получит PID 1, а само приложение станет дочерним процессом. Это ломает обработку сигналов (например, SIGTERM при остановке контейнера), так как /bin/sh не передает сигналы потомкам.
  • Exec-форма: CMD ["python", "app.py"]. Docker запускает исполняемый файл напрямую. Приложение получает PID 1 и корректно обрабатывает системные сигналы.
  • Разделение ролей: ENTRYPOINT ["executable"] задает неизменяемую команду, которая должна выполниться всегда. CMD ["param1", "param2"] задает аргументы по умолчанию, которые пользователь может легко переопределить при запуске docker run.

    Пример идеальной связки:

    Если запустить контейнер без аргументов, выполнится nginx -g daemon off;. Если запустить docker run my-nginx -t, выполнится nginx -t (проверка конфигурации).

    Управление кэшем сборки

    При выполнении docker build демон Docker проверяет каждую инструкцию: есть ли в кэше слой, созданный точно такой же командой на базе точно такого же родительского слоя? Если да, инструкция пропускается, и берется готовый слой.

    Для инструкций COPY и ADD Docker вычисляет контрольную сумму (checksum) каждого копируемого файла. Если хотя бы один байт в файле изменился, кэш инвалидируется.

    Главное правило: как только кэш инвалидирован для одного слоя, все последующие слои в этом Dockerfile будут собираться с нуля.

    !Инвалидация кэша при изменении исходного кода

    Именно поэтому порядок инструкций критически важен. Часто изменяемые данные должны находиться как можно ближе к концу файла. Рассмотрим сборку Node.js приложения.

    Неоптимизированный порядок:

    Здесь копируется весь проект (COPY . .), включая исходный код. Если разработчик изменит одну строчку в server.js, контрольная сумма изменится. Docker инвалидирует этот слой и заново выполнит долгий npm install, хотя зависимости (package.json) не менялись.

    Оптимизированный порядок:

    Теперь мы сначала копируем только манифесты зависимостей и устанавливаем их. Изменение исходного кода в server.js инвалидирует только второй COPY . ., а тяжелый слой с node_modules будет мгновенно взят из кэша.

    Контекст сборки и .dockerignore

    Команда docker build -t my-app . отправляет текущую директорию (точка в конце) демону Docker. Это называется контекстом сборки (build context). Если в директории лежат гигабайты логов, локальная папка node_modules или история .git, все это будет запаковано в tar-архив и отправлено демону (даже если демон работает на той же машине). Это замедляет старт сборки и может привести к случайному попаданию секретов в образ через COPY . ..

    Файл .dockerignore работает аналогично .gitignore. В него обязательно нужно включать:

  • .git
  • node_modules, venv, __pycache__
  • *.log, временные файлы
  • .env файлы с локальными паролями
  • Многоэтапная сборка (Multi-stage builds)

    Для компилируемых языков (Go, Java, C++) или фронтенд-приложений (React, Vue) существует проблема: для создания приложения нужны компиляторы, SDK и исходный код, но для работы готового приложения в production нужен только скомпилированный бинарный файл или статика и легкий веб-сервер.

    До появления multi-stage сборок инженерам приходилось писать сложные bash-скрипты, которые собирали артефакт в одном контейнере, извлекали его на хост и копировали в другой, чистый образ. Многоэтапная сборка решает это изящно внутри одного Dockerfile за счет использования нескольких инструкций FROM.

    !Схема передачи артефактов в многоэтапной сборке

    Пример многоэтапной сборки Go-сервиса:

    В итоговый образ попадут только слои из второго этапа. Огромный образ golang (около 800 МБ) с исходным кодом и кэшем модулей останется на сборочном сервере. Итоговый production-образ будет весить ровно столько, сколько весит сам бинарный файл /app (обычно 10-20 МБ), и не будет содержать даже оболочки /bin/sh, что сводит поверхность атаки практически к нулю.

    3. Сетевое взаимодействие и постоянство данных: Docker Volumes и Network

    Сетевое взаимодействие и постоянство данных: Docker Volumes и Network

    Запуск контейнера с базой данных PostgreSQL, создание таблиц, запись тысяч строк и внезапная потеря всей информации после перезапуска контейнера — классическая ситуация при первом знакомстве с контейнеризацией. Эфемерность контейнеров, обеспечиваемая Read-Write слоем, идеальна для stateless-приложений (например, веб-серверов, которые только отдают статику), но становится критической проблемой, когда речь заходит о сохранении состояния.

    Анатомия хранения данных: обход UnionFS

    Как только контейнер удаляется, его Read-Write слой уничтожается вместе со всеми накопленными изменениями. Кроме того, запись данных через драйвер хранилища (storage driver), реализующий концепцию Copy-on-Write, сопряжена с серьезными накладными расходами на операции ввода-вывода (I/O). Для баз данных, активно пишущих на диск, прохождение каждого байта через абстракцию слоистой файловой системы приводит к деградации производительности.

    Чтобы решить эту проблему, Docker предоставляет механизмы монтирования, которые позволяют контейнеру писать данные напрямую в файловую систему хоста, полностью минуя UnionFS. Существует два основных подхода: Bind Mounts и Docker Volumes.

    Bind Mounts: Прямой проброс директорий

    Bind Mount позволяет привязать конкретную директорию или файл на хост-машине к определенному пути внутри контейнера. Любые изменения, сделанные хостом в этой директории, мгновенно отражаются в контейнере, и наоборот.

    При запуске контейнера это задается флагом -v с указанием абсолютного пути: docker run -v /home/user/project/conf:/etc/nginx/conf.d nginx

    Этот подход незаменим в процессе разработки. Например, при написании кода на Python можно пробросить директорию с исходниками внутрь контейнера. Разработчик сохраняет файл в своей любимой IDE на хосте, а процесс внутри контейнера тут же подхватывает изменения (live reload), избавляя от необходимости пересобирать образ после каждой строчки кода.

    Однако в production-среде Bind Mounts таят в себе серьезные риски:

  • Зависимость от структуры хоста. Контейнер перестает быть переносимым. Если на другом сервере нет директории /home/user/project/conf, запуск завершится ошибкой.
  • Проблема прав доступа (UID/GID). Процесс внутри контейнера работает от имени пользователя с определенным идентификатором (например, UID 1000). Если директория на хосте принадлежит пользователю root (UID 0) с правами только на чтение, приложение в контейнере получит ошибку Permission denied при попытке записи. Разрешение таких конфликтов часто приводит к небезопасной выдаче прав chmod 777 на хосте.
  • Docker Volumes: Управляемое хранилище

    Docker Volume — это логический том, который полностью управляется самим демоном Docker. При создании тома Docker выделяет для него место в своей служебной директории на хосте (обычно это /var/lib/docker/volumes/ в Linux).

    !Сравнение Bind Mount и Docker Volume

    Создание и использование тома выглядит иначе: docker volume create pg_data docker run -v pg_data:/var/lib/postgresql/data postgres:15

    В этом случае разработчику не нужно знать, где физически лежат данные на хосте. Docker сам заботится о правах доступа, инициализации пустой директории и изоляции.

    Ключевые преимущества Volumes для production-окружений:

  • Безопасность и изоляция. Другие процессы на хосте не имеют прямого случайного доступа к данным базы, так как они скрыты в служебной директории Docker.
  • Управление жизненным циклом. Тома существуют независимо от контейнеров. При выполнении docker rm -f my_db том pg_data останется нетронутым. Его можно легко подключить к новому контейнеру с обновленной версией СУБД.
  • Резервное копирование и миграция. Тома легко бэкапить с помощью временных контейнеров, которые монтируют том и архивируют его содержимое в tar-файл.
  • Сетевые хранилища. С помощью плагинов Volumes могут ссылаться не на локальный диск, а на удаленные хранилища (NFS, AWS EBS, MinIO), обеспечивая распределенное хранение данных.
  • Сетевая изоляция и драйверы Docker

    По умолчанию каждый контейнер запускается в собственном сетевом пространстве имен (Network Namespace). У него есть свой изолированный сетевой стек: собственные интерфейсы (например, eth0), таблица маршрутизации и правила межсетевого экрана. Чтобы изолированные контейнеры могли общаться друг с другом и с внешним миром, Docker использует концепцию сетевых драйверов.

    Драйвер bridge и сеть по умолчанию

    Когда Docker устанавливается на хост, он создает виртуальный сетевой мост с именем docker0 (драйвер bridge). Это программный коммутатор (switch), работающий на канальном уровне. Каждому новому контейнеру, если не указано иное, Docker выдает внутренний IP-адрес из подсети этого моста (например, 172.17.0.2, 172.17.0.3 и так далее).

    Контейнеры в этой сети по умолчанию могут пинговать друг друга по выданным IP-адресам. Однако здесь кроется критическая проблема: IP-адреса выдаются динамически. Если контейнер с базой данных перезапустится, он может получить адрес 172.17.0.5 вместо старого 172.17.0.2. Если backend-приложение было жестко сконфигурировано на старый IP, связь оборвется.

    Пользовательские сети и встроенный DNS

    Для решения проблемы динамических IP-адресов Docker предлагает создавать пользовательские сети (User-defined bridge networks).

    docker network create my_app_net

    Когда контейнеры подключаются к пользовательской сети, активируется встроенный в Docker DNS-сервер. Этот сервер автоматически регистрирует имена контейнеров и связывает их с текущими IP-адресами.

    !Топология пользовательской сети и DNS-разрешение

    Если запустить базу данных с именем: docker run --name db --network my_app_net postgres И backend-приложение: docker run --name backend --network my_app_net my_backend

    Код внутри контейнера backend может обращаться к базе данных просто по хостнейму db (например, строка подключения jdbc:postgresql://db:5432/main). Встроенный DNS-сервер Docker перехватит этот запрос и вернет актуальный внутренний IP-адрес контейнера db. В стандартной сети docker0 этот механизм DNS отключен из соображений обратной совместимости, поэтому для связи контейнеров всегда следует создавать собственные сети.

    Взаимодействие с внешним миром

    Внутренняя сеть решает задачу общения контейнеров между собой, но как клиенту из интернета попасть на веб-сервер Nginx, работающий внутри изолированного контейнера с внутренним IP 172.18.0.4?

    Публикация портов (Port Mapping)

    Для маршрутизации внешнего трафика внутрь контейнера используется механизм проброса портов, который опирается на правила NAT (Network Address Translation) в утилите iptables ядра Linux.

    При запуске контейнера указывается флаг -p <порт_хоста>:<порт_контейнера>: docker run -p 8080:80 nginx

    !Прохождение пакета через NAT при пробросе порта

    Когда внешний запрос приходит на публичный IP-адрес сервера на порт 8080, ядро Linux сверяется с правилами iptables, созданными Docker. Пакет перехватывается, его целевой порт подменяется с 8080 на 80, а целевой IP-адрес — с адреса хоста на внутренний IP контейнера. Затем пакет маршрутизируется через виртуальный мост прямо в сетевой интерфейс контейнера.

    Важно понимать, что процесс Nginx внутри контейнера «думает», что он слушает стандартный 80-й порт. Он ничего не знает о порте 8080. Трансляция происходит прозрачно на уровне ядра хост-системы.

    Альтернативные сетевые режимы: host и none

    Помимо bridge, Docker поддерживает и другие сетевые драйверы для специфических задач:

    Режим host (--network host) полностью отключает сетевую изоляцию для контейнера. Контейнер не получает собственного Network Namespace, а использует сетевой стек хост-машины. Если в таком режиме запустить приложение на порту 80, оно займет порт 80 самого сервера. Этот режим обеспечивает максимальную сетевую производительность, так как исключает накладные расходы на NAT и виртуальные мосты. Однако он лишает гибкости: запустить два одинаковых контейнера на одном порту в режиме host невозможно — возникнет конфликт портов.

    Режим none (--network none) создает контейнер с полностью изолированным сетевым стеком, в котором есть только интерфейс обратной петли (loopback, lo). Контейнер не имеет доступа ни к внешнему миру, ни к другим контейнерам. Этот режим применяется для параноидально защищенных задач, например, для одноразовых контейнеров, выполняющих криптографические вычисления или генерацию ключей, где любой сетевой доступ является вектором атаки.

    Управление множеством контейнеров через CLI-команды docker run быстро становится неконтролируемым. Если проект состоит из базы данных, кэша Redis, backend-сервера и frontend-клиента, администратору придется вручную создавать сети, тома и писать длинные скрипты запуска с десятками флагов -v, -p и --network. Поддержание правильного порядка запуска (сначала БД, потом backend) и обновление конфигураций превращается в рутину, требующую перехода к декларативному описанию инфраструктуры.