Перейти к содержанию

Основы сети Docker

«Почему эти два контейнера не могут связаться друг с другом» и «почему я не могу достучаться до этого контейнера с хоста» — два самых частых вопроса про контейнеры, и оба сводятся к пониманию сетевых драйверов Docker.

Как это устроено

Сетевые драйверы

$ docker network ls
NETWORK ID     NAME      DRIVER    SCOPE
abc123def456   bridge    bridge    local
def456abc789   host      host      local
789abc123def   none      null      local

Любая установка Docker поставляется с тремя встроенными сетями:

  • bridge (по умолчанию) — контейнеры получают собственный приватный IP на виртуальном bridge-интерфейсе (docker0 на хосте) и выходят во внешний мир через NAT. В сети bridge по умолчанию контейнеры достигают друг друга по IP-адресу, но не по имени; пользовательская bridge-сеть добавляет автоматическое разрешение имён контейнеров, поэтому запуск всего в сети по умолчанию с ручным жонглированием IP считается плохой практикой.
  • host — контейнер напрямую разделяет сетевое пространство имён хоста, без изоляции, без проброса портов (и это невозможно — контейнер привязывает порты хоста напрямую). Быстрее, но ноль сетевой изоляции от хоста.
  • none — без внешней сети: у контейнера есть только loopback-интерфейс.

Пользовательские bridge-сети (практический выбор по умолчанию для многоконтейнерных

приложений)

docker network create app_net
docker run -d --name db --network app_net postgres:16
docker run -d --name api --network app_net -e DB_HOST=db myapp

В пользовательской сети Docker запускает встроенный DNS-сервер, автоматически разрешающий имена контейнеров в их IP — контейнер api может достучаться до базы данных по имени хоста db, без ручной настройки IP или правки /etc/hosts. Это главная практическая причина всегда создавать явную сеть, а не полагаться на bridge по умолчанию.

Проброс портов: разбор -p

docker run -d -p 8080:80 nginx

-p <host_port>:<container_port> — это NAT, не прямая привязка: трафик, попадающий на host_ip:8080, перенаправляется на порт 80 внутри контейнера. Критично важно: Docker напрямую манипулирует правилами iptables/nftables хоста для реализации этого — правило ufw/firewalld, которое выглядит блокирующим порт, может не иметь эффекта, потому что собственные правила Docker проверяются первыми, в другой цепочке. Это очень частый источник путаницы: оператор настраивает ufw deny 8080, подтверждает через ufw status, что запрещено, а порт всё равно доступен, потому что правила ufw не покрывают автоматически собственные цепочки DOCKER/DOCKER-USER Docker. Чтобы опубликовать порт только для самого хоста, привяжите его к loopback: docker run -d -p 127.0.0.1:8080:80 nginx.

docker port <container>              # текущие проброшенные порты работающего контейнера

Просмотр реальной сетевой конфигурации контейнера

docker inspect <container> --format '{{json .NetworkSettings.Networks}}' | python3 -m json.tool

Показывает реальный IP-адрес(а) контейнера, шлюз и к каким сетям он подключён — полезно, когда docker network ls подтверждает существование сети, но нужно убедиться, что конкретный контейнер реально на ней.

Трафик между контейнерами вообще без проброса порта на хост

Два контейнера в одной пользовательской сети могут общаться друг с другом на любом порту, который слушающий открывает, без флага -p вообще — -p нужен только для открытия порта хосту/внешнему миру. Контейнер базы данных, до которого нужен доступ только контейнеру API, не должен иметь -p для своего порта БД — проброс его всё равно открывает доступ всему, что достижимо с хоста, а не только контейнеру API.

Различия Ubuntu/Debian и RHEL-подобных систем

Сетевая модель Docker идентична во всех дистрибутивах (её реализует сам демон Docker, не дистрибутив). Практическое различие — во взаимодействии с файрволом: на хосте RHEL-семейства с firewalld манипуляция Docker теми же правилами nftables/iptables может точно так же обходить правила зон firewalld — тот же сюрприз «моё правило файрвола как будто не применяется к этому контейнерному порту» применим и там.

Практическое задание

  1. Создайте пользовательскую сеть и запустите в ней два контейнера (например, nginx и простой alpine с sleep infinity).
  2. Изнутри alpine-контейнера (docker exec -it <name> sh) выполните wget -qO- http://nginx (используя имя контейнера, не IP) и убедитесь, что имя разрешается и приходит ответ.
  3. Запустите третий контейнер, не подключённый к этой сети, и убедитесь, что тот же wget по имени проваливается — доказывая, что именно членство в сети, а не просто «Docker работает», даёт доступность.

Как проверить, что всё сделано правильно

  • Шаг 2: wget должен успешно выполниться и вывести дефолтную HTML-страницу nginx, без единого IP-адреса в команде.
  • Шаг 3: та же команда из неподключённого контейнера должна провалиться с ошибкой разрешения DNS (имя nginx ничего не значит за пределами встроенного DNS именно этой пользовательской сети).

Типичные ошибки

  • Предположение, что правило deny ufw/firewalld блокирует проброшенный Docker-порт. Часто это не так — собственные правила iptables Docker (цепочки DOCKER/DOCKER-USER) проверяются независимо. Если нужно, чтобы файрвол хоста реально управлял Docker-проброшенными портами, правила должны идти именно в цепочку DOCKER-USER, а не в общую INPUT.
  • Проброс порта через -p для чисто межконтейнерного взаимодействия, которое вообще не нуждалось в открытии хосту/внешнему миру.
  • Опора на сеть bridge по умолчанию с жёстко прописанными IP контейнеров, которые меняются при пересоздании — DNS по имени в пользовательской сети полностью этого избегает.

⚠ Опасные команды

  • docker network prune удаляет все сети, не используемые ни одним контейнером прямо сейчас — если сеть определена, но её контейнеры временно остановлены (не удалены), саму сеть всё равно можно прунить, что потребует пересоздать её перед следующим запуском тех контейнеров.
  • Проброс порта базы данных или админ-панели напрямую через -p <port>:<port> без правила файрвола хоста, ограничивающего источники, открывает его всей сети, достижимой до хоста — относитесь к любому -p так же, как к открытию порта файрвола, потому что по сути это оно и есть.

Связанные статьи

Контрольные вопросы

  1. Почему сеть bridge по умолчанию не предоставляет разрешение имён между контейнерами, а пользовательская bridge-сеть — предоставляет?
  2. Почему правило ufw deny <port> может не иметь эффекта на порт, проброшенный Docker?
  3. В чём практическая разница между драйверами bridge и host в плане изоляции?
  4. Когда НЕ стоит пробрасывать порт контейнера через -p, даже если порт реально используется?