Модуль 05 — Сети Docker

Как контейнеры находят друг друга по имени и почему опубликованный порт виден из интернета

Прогресс курса Модуль 5 из 9

Что вы освоите в этом модуле

01

Три сети по умолчанию

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

$ docker network ls
NETWORK ID     NAME      DRIVER    SCOPE
7eaf53ae1bdb   bridge    bridge    local
1d47b2d01a56   host      host      local
583854e2c772   none      null      local
ДрайверЧто получает контейнерКогда использовать
bridgeСвой сетевой интерфейс в виртуальной сети на хосте; наружу — через NAT хостаПочти всегда; лучше — в своей сети, а не в сети bridge по умолчанию
hostСеть хоста без изоляции: порты приложения — порты хостаСетевые утилиты, мониторинг, редко — ради скорости
noneТолько lo, сети нетПакетная обработка файлов, которой сеть не нужна и опасна

Посмотрим на интерфейсы контейнера в каждом режиме. В host — интерфейсы самого сервера, в none — только петля, в обычном режиме — eth0 с адресом из сети Docker:

$ docker run --rm --network host alpine:3.22 ip -o -4 addr | awk '{print $2, $4}'
lo 127.0.0.1/8
ens18 192.168.116.34/24
docker0 172.17.0.1/16
$ docker run --rm --network none alpine:3.22 ip -o -4 addr | awk '{print $2, $4}'
lo 127.0.0.1/8
$ docker run --rm alpine:3.22 ip -o -4 addr | awk '{print $2, $4}'
lo 127.0.0.1/8
eth0 172.17.0.2/16

На хосте сеть bridge — это виртуальный коммутатор docker0 с адресом 172.17.0.1; каждый контейнер подключён к нему своей парой виртуальных интерфейсов veth.

02

Сеть по умолчанию: только по адресу

Контейнеры без параметра --network попадают в сеть bridge. Они видят друг друга по IP-адресу, но не по имени:

$ docker run -d --name web nginx:1.28-alpine
8409d7224e448330ea8b338b478663f7607fe0327d7a0cd8ccae08acbc8798cd
$ docker inspect -f "{{.NetworkSettings.IPAddress}}" web
172.17.0.2
$ docker run --rm alpine:3.22 wget -qO- -T 3 http://web
wget: bad address 'web'
$ docker run --rm alpine:3.22 sh -c "wget -qO- -T 3 http://$(docker inspect -f "{{.NetworkSettings.IPAddress}}" web) | grep -o \"<title>.*</title>\""
<title>Welcome to nginx!</title>
$ docker rm -f web
web

Адрес выдаётся при запуске и после пересоздания контейнера может стать другим, поэтому писать его в настройки приложения нельзя. Сеть bridge по умолчанию оставлена для совместимости; для своих приложений создают свою сеть.

03

Своя сеть: имена работают

В сети, созданной docker network create, работает встроенный DNS-сервер Docker. Имя контейнера — это его имя в сети:

$ docker network create appnet
50099771c0d42cd5604acba1907651d6391832cb5bae67588244539701b838f3
$ docker run -d --name web --network appnet nginx:1.28-alpine
6c742ecc1058a24fd4d90b74d09638f62ec7e0dd1a7902a11394d536e1806e76
$ docker run --rm --network appnet alpine:3.22 sh -c "wget -qO- http://web | grep -o \"<title>.*</title>\""
<title>Welcome to nginx!</title>
$ docker run --rm --network appnet alpine:3.22 cat /etc/resolv.conf
# Generated by Docker Engine.
# This file can be edited; Docker Engine will not make further changes once it
# has been modified.

nameserver 127.0.0.11
search localdomain
options ndots:0

# Based on host file: '/etc/resolv.conf' (internal resolver)
# ExtServers: [192.168.116.1]
# Overrides: []
# Option ndots from: internal
$ docker run --rm --network appnet alpine:3.22 sh -c "ping -c1 web | head -1"
PING web (172.18.0.2): 56 data bytes

DNS-сервер Docker отвечает по адресу 127.0.0.11 внутри каждого контейнера своей сети: имена контейнеров знает сам, остальные запросы пересылает DNS-серверам хоста. Поэтому в настройках приложения пишут DB_HOST=db, а не адрес, — и это работает после любого пересоздания.

Псевдоним --network-alias даёт контейнеру дополнительное имя. Так можно заменить сервис новой версией, не меняя настроек клиентов: api-v2 отвечает на имя api:

$ docker run -d --name api-v2 --network appnet --network-alias api nginx:1.28-alpine
9ceba5a039f28974ebe955ffaf2cd5d49bfd97d8003876c17e1d7e38b6ecbd9b
$ docker run --rm --network appnet alpine:3.22 sh -c "ping -c1 api | head -1"
PING api (172.18.0.3): 56 data bytes

Если псевдоним одинаковый у нескольких контейнеров, DNS вернёт все их адреса — простейшая балансировка. В Compose (модуль 06) имя сервиса работает именно как такой псевдоним.

04

Изоляция: сеть как граница

Контейнеры в разных сетях не видят друг друга ни по имени, ни по адресу. Контейнер можно подключить к нескольким сетям сразу — у него появится интерфейс в каждой:

$ docker network create othernet
e5a18bb9858f36ddac27c6f59f880584ce115445cea4966aa137f1dd8554f3fa
$ docker run --rm --network othernet alpine:3.22 wget -qO- -T 3 http://web
wget: bad address 'web'
$ docker run --rm --network othernet alpine:3.22 sh -c "wget -qO- -T 3 http://$(docker inspect -f "{{.NetworkSettings.Networks.appnet.IPAddress}}" web) >/dev/null || echo недоступен"
wget: download timed out
недоступен
$ docker network connect othernet web
$ docker run --rm --network othernet alpine:3.22 sh -c "wget -qO- http://web | grep -o \"<title>.*</title>\""
<title>Welcome to nginx!</title>
$ docker inspect -f "{{range \$k, \$v := .NetworkSettings.Networks}}{{\$k}} {{\$v.IPAddress}}{{println}}{{end}}" web
appnet 172.18.0.2
othernet 172.19.0.2

Это основа устройства многоконтейнерного приложения. Обратный прокси подключён к сети front и публикует порт наружу. Приложение — к front и back. База — только к back: прокси, а значит и взломанный через него контейнер, до базы не дотянется. Такую схему мы соберём в итоговом проекте.

  интернет ──▶ :443 proxy ──front──▶ app ──back──▶ db
                                          (db в front не видна)

Параметры сети — подсеть, шлюз, подключённые контейнеры — показывает docker network inspect:

$ docker network inspect -f "{{range .IPAM.Config}}{{.Subnet}} шлюз {{.Gateway}}{{end}}" appnet
172.18.0.0/16 шлюз 172.18.0.1
$ docker network inspect -f "{{range .Containers}}{{.Name}} {{.IPv4Address}}{{println}}{{end}}" appnet
web 172.18.0.2/16
api-v2 172.18.0.3/16

Подсети Docker берёт из диапазонов 172.17.0.0/16, 172.18.0.0/16 и дальше. Если в вашей организации такие адреса уже заняты (VPN, филиалы), контейнеры перестанут видеть эти узлы. Тогда подсеть задают явно: docker network create --subnet 10.50.0.0/24 appnet, а для всех новых сетей — параметром default-address-pools в /etc/docker/daemon.json.

05

Публикация портов

Снаружи контейнер в сети bridge недоступен, пока порт не опубликован. -p принимает адрес хоста, на котором открыть порт. Опубликуем один порт на всех адресах, второй — только на 127.0.0.1, и обратимся к обоим по адресу сервера в локальной сети:

$ docker run -d --name pub -p 8080:80 -p 127.0.0.1:8081:80 nginx:1.28-alpine
4b092bfedb7697d79ebff0f7f513dfbca813d4a1e47cab2500975643e5b2009f
$ ss -ltn | grep -E ":808[01] "
LISTEN 0      4096       127.0.0.1:8081       0.0.0.0:*   
LISTEN 0      4096         0.0.0.0:8080       0.0.0.0:*   
LISTEN 0      4096            [::]:8080          [::]:*   
$ curl -s -o /dev/null -w "%{http_code}\n" http://127.0.0.1:8081
200
$ curl -s -o /dev/null -w "%{http_code}\n" -m 3 http://192.168.116.34:8081
000
$ curl -s -o /dev/null -w "%{http_code}\n" -m 3 http://192.168.116.34:8080
200
$ docker port pub
80/tcp -> 0.0.0.0:8080
80/tcp -> 127.0.0.1:8081
80/tcp -> [::]:8080

Порт 8080 доступен всем, кто достучится до сервера; 8081 — только самому серверу. Код 000 — curl не смог соединиться. Правило для сервера: наружу публикуется только обратный прокси на 80 и 443, остальные сервисы — 127.0.0.1:… или без -p вовсе, если к ним ходят только другие контейнеры.

06

Почему межсетевой экран не помогает

Частая история: на сервере настроен ufw или nftables, разрешены только 22, 80 и 443, а база в контейнере с -p 5432:5432 всё равно доступна из интернета. Причина — в том, как Docker публикует порт. Он добавляет правила iptables:

$ sudo iptables -t nat -S PREROUTING
-P PREROUTING ACCEPT
-A PREROUTING -m addrtype --dst-type LOCAL -j DOCKER
$ sudo iptables -t nat -S DOCKER
-N DOCKER
-A DOCKER -i docker0 -j RETURN
-A DOCKER ! -i docker0 -p tcp -m tcp --dport 8080 -j DNAT --to-destination 172.17.0.2:80
$ sudo iptables -S DOCKER
-N DOCKER
-A DOCKER -d 172.17.0.2/32 ! -i docker0 -o docker0 -p tcp -m tcp --dport 80 -j ACCEPT

Пакет на порт 8080 ещё в цепочке PREROUTING получает новый адрес назначения — контейнер 172.17.0.2:80. Для ядра он теперь не входящий на хост, а проходящий насквозь: идёт через цепочку FORWARD, а не INPUT, где обычно стоят правила ufw. В FORWARD Docker сам вставил разрешение. Вывод снят на учебном сервере курса 23.10.2026 с контейнером -p 8080:80.

Что делать:

Docker меняет и политику цепочки FORWARD на DROP. Если сервер служил маршрутизатором (VPN-шлюз, маршрутизация между сетями), после установки Docker пересылка трафика остановится. Разрешения для такого трафика добавляют в DOCKER-USER.
07

Из контейнера к хосту

Иногда контейнеру нужна служба, работающая на самом хосте: база, установленная пакетом, или локальный прокси. localhost в контейнере — это сам контейнер. Адрес хоста в сети по умолчанию — шлюз 172.17.0.1, а чтобы не писать адрес, есть специальное имя:

$ docker run --rm --add-host host.docker.internal:host-gateway alpine:3.22 sh -c "grep host.docker /etc/hosts; wget -qO- -T 3 http://host.docker.internal:8080 | grep -o \"<title>.*</title>\""
172.17.0.1	host.docker.internal
<title>Welcome to nginx!</title>

host-gateway Docker заменяет адресом хоста в сети контейнера. Служба на хосте должна при этом слушать не только 127.0.0.1, а адрес docker0 или все адреса — и тогда её нужно закрыть межсетевым экраном снаружи. В Docker Desktop имя host.docker.internal работает без параметров.

08

Удаление сетей

Сеть с подключёнными контейнерами не удалить. Неиспользуемые сети убирает docker network prune:

$ docker network rm appnet
Error response from daemon: error while removing network: network appnet id 50099771c0d42cd5604acba1907651d6391832cb5bae67588244539701b838f3 has active endpoints
$ docker rm -f web api-v2 pub
web
api-v2
pub
$ docker network rm appnet othernet
appnet
othernet

Ключевые выводы модуля

Запомните главное

  • Своя сеть docker network create — и контейнеры находят друг друга по имени через DNS 127.0.0.11
  • В сети bridge по умолчанию имена не работают, а адреса меняются
  • Разные сети — изоляция; база только в «задней» сети, прокси только в «передней»
  • -p обходит ufw: публиковать на 127.0.0.1, ограничения — в DOCKER-USER, проверять снаружи
  • К службам хоста — через --add-host host.docker.internal:host-gateway
Практические задания — на учебном портале. Задания, критерии оценивания, сдача работ и оценки преподавателя — в курсе на portal.nevabit.ru. Учётную запись выдаёт преподаватель.
Модуль 04: Тома и данные Все модули Модуль 06: Docker Compose