Как контейнеры находят друг друга по имени и почему опубликованный порт виден из интернета
Сразу после установки у 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.
Контейнеры без параметра --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 по умолчанию оставлена для совместимости; для своих приложений создают свою сеть.
В сети, созданной 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) имя сервиса работает именно как такой псевдоним.
Контейнеры в разных сетях не видят друг друга ни по имени, ни по адресу. Контейнер можно подключить к нескольким сетям сразу — у него появится интерфейс в каждой:
$ 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.
Снаружи контейнер в сети 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 вовсе, если к ним ходят только другие контейнеры.
Частая история: на сервере настроен 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.
Что делать:
-p 127.0.0.1:5432:5432 или без -p.DOCKER-USER — Docker её создаёт, не трогает и проверяет раньше своих правил.nmap или nc -zv сервер порт с другой машины, а не по списку правил.FORWARD на DROP. Если сервер служил маршрутизатором (VPN-шлюз, маршрутизация между сетями), после установки Docker пересылка трафика остановится. Разрешения для такого трафика добавляют в DOCKER-USER.Иногда контейнеру нужна служба, работающая на самом хосте: база, установленная пакетом, или локальный прокси. 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 работает без параметров.
Сеть с подключёнными контейнерами не удалить. Неиспользуемые сети убирает 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.11bridge по умолчанию имена не работают, а адреса меняются-p обходит ufw: публиковать на 127.0.0.1, ограничения — в DOCKER-USER, проверять снаружи--add-host host.docker.internal:host-gateway