Основы сети 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¶
-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.
Просмотр реальной сетевой конфигурации контейнера¶
Показывает реальный IP-адрес(а) контейнера, шлюз и к каким сетям он подключён — полезно, когда docker network ls подтверждает существование сети, но нужно убедиться, что конкретный контейнер реально на ней.
Трафик между контейнерами вообще без проброса порта на хост¶
Два контейнера в одной пользовательской сети могут общаться друг с другом на любом порту, который слушающий открывает, без флага -p вообще — -p нужен только для открытия порта хосту/внешнему миру. Контейнер базы данных, до которого нужен доступ только контейнеру API, не должен иметь -p для своего порта БД — проброс его всё равно открывает доступ всему, что достижимо с хоста, а не только контейнеру API.
Различия Ubuntu/Debian и RHEL-подобных систем¶
Сетевая модель Docker идентична во всех дистрибутивах (её реализует сам демон Docker, не дистрибутив). Практическое различие — во взаимодействии с файрволом: на хосте RHEL-семейства с firewalld манипуляция Docker теми же правилами nftables/iptables может точно так же обходить правила зон firewalld — тот же сюрприз «моё правило файрвола как будто не применяется к этому контейнерному порту» применим и там.
Практическое задание¶
- Создайте пользовательскую сеть и запустите в ней два контейнера (например,
nginxи простойalpineсsleep infinity). - Изнутри alpine-контейнера (
docker exec -it <name> sh) выполнитеwget -qO- http://nginx(используя имя контейнера, не IP) и убедитесь, что имя разрешается и приходит ответ. - Запустите третий контейнер, не подключённый к этой сети, и убедитесь, что тот же
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так же, как к открытию порта файрвола, потому что по сути это оно и есть.
Связанные статьи¶
Контрольные вопросы¶
- Почему сеть bridge по умолчанию не предоставляет разрешение имён между контейнерами, а пользовательская bridge-сеть — предоставляет?
- Почему правило
ufw deny <port>может не иметь эффекта на порт, проброшенный Docker? - В чём практическая разница между драйверами
bridgeиhostв плане изоляции? - Когда НЕ стоит пробрасывать порт контейнера через
-p, даже если порт реально используется?