8.2. Bridge networks
Цели
После этого материала вы сможете:
- назвать четыре различия default bridge и user-defined bridge;
- объяснить, почему DNS работает в одной сети и не работает в другой;
- создать сеть с заданной подсетью и понять, когда это нужно;
- изолировать группы сервисов так, чтобы база была недоступна из внешней сети;
- подключать и отключать container от сети на работающем container'е;
- прочитать вывод
docker network inspectи найти в нём нужное.
Предварительные знания
Ключевые термины
| Термин | Объяснение |
|---|---|
default bridge | Сеть с именем bridge, соответствует docker0 |
user-defined bridge | Сеть, созданная docker network create |
ICC | Inter-container communication — связь между container'ами одной сети |
--internal | Сеть без выхода наружу |
network alias | Дополнительное DNS-имя container'а в сети |
embedded DNS | Резолвер Docker на адресе 127.0.0.11 |
Теория
Четыре различия
| Свойство | Default bridge | User-defined bridge |
|---|---|---|
| Разрешение имён container'ов | Нет | Есть |
| Изоляция | Все container'ы вместе | Только участники сети |
| Подключение на лету | Есть | Есть |
| Настройка подсети | Только через daemon.json и перезапуск демона | При создании сети |
| Network aliases | Нет | Есть |
Передача переменных через --link | Есть (устаревшее) | Не поддерживается |
Первая строка — решающая. В default bridge container'ы обращаются друг к другу только по IP-адресам, которые меняются при каждом пересоздании. Единственный штатный способ имён там — флаг --link, объявленный устаревшим.
Отсюда практическое правило: default bridge не используют. Он существует для обратной совместимости; любая многосервисная конфигурация начинается с docker network create. Compose создаёт сеть автоматически, поэтому там вопрос не возникает (раздел 09).
Почему DNS работает не везде
В user-defined сети Docker подставляет в container /etc/resolv.conf собственный резолвер на адресе 127.0.0.11. Он знает имена всех container'ов этой сети и их псевдонимы; всё остальное пересылает вышестоящим серверам (урок 8.4).
В default bridge этот резолвер тоже присутствует, но не содержит имён container'ов: там работает только пересылка запросов наружу.
Это не техническое ограничение, а сознательное решение о совместимости: поведение default bridge не меняли, чтобы не сломать конфигурации, существовавшие до появления user-defined сетей.
Изоляция
Container видит только те container'ы, с которыми состоит в общей сети. Два container'а в разных user-defined сетях не имеют между собой связи — ни по имени, ни по IP.
Отсюда стандартный приём: разделение на frontend- и backend-сеть.
┌──── frontend ────┐ ┌──── backend ────┐
│ │ │ │
──► │ proxy web ──┼────────┼── web db │
│ │ │ │
└──────────────────┘ └─────────────────┘
(--internal: нет
выхода наружу)
web состоит в обеих сетях и служит единственным мостом. db находится только в backend, недостижим из frontend и, при флаге --internal, не имеет выхода в интернет.
Это даёт реальное ограничение доступа, а не декоративное: пакет из proxy к db не имеет маршрута.
Внутри одной сети изоляции нет
Важное уточнение: все container'ы одной сети видят все порты друг друга, а не только опубликованные.
| Что | Доступно внутри сети | Доступно с host |
|---|---|---|
Порт, слушающий 0.0.0.0 | Да | Только если опубликован |
Порт, слушающий 127.0.0.1 | Нет | Нет |
| Опубликованный порт | Да | Да |
Публикация нужна для доступа с host и извне, а не между container'ами. Порт базы данных публиковать не требуется, если к ней обращается только приложение из той же сети — и не следует, потому что публикация делает его доступным снаружи.
Параметр com.docker.network.bridge.enable_icc=false при создании сети запрещает связь между её участниками, оставляя только опубликованные порты. Применяется редко: обычно проще разделить сети.
Создание сети
docker network create appnet
docker network create --subnet 10.10.0.0/24 --gateway 10.10.0.1 appnet2
docker network create --internal backend
docker network create -o com.docker.network.bridge.name=br-app appnet3
| Параметр | Зачем |
|---|---|
--subnet | Фиксированная адресация; нужен для статических адресов |
--gateway | Явный шлюз внутри подсети |
--ip-range | Пул для автоматической выдачи — уже подсети |
--internal | Нет выхода наружу и нет входа снаружи |
--attachable | Разрешает подключать отдельные container'ы к overlay-сети |
-o com.docker.network.bridge.name | Читаемое имя bridge на host вместо br-a1b2c3d4 |
Задавать подсеть в большинстве случаев не требуется. Это нужно, когда адреса должны быть предсказуемы (правила на внешнем оборудовании) или когда автоматический пул конфликтует с сетью организации (урок 8.1).
Статические адреса и псевдонимы
docker run -d --network appnet --ip 10.10.0.50 --name fixed nginx:alpine
docker run -d --network appnet --network-alias db --name postgres-1 postgres:17-alpine
Статический адрес требует сети с явной --subnet. Он нужен редко: имена надёжнее и не ломаются при изменении адресации.
Псевдоним — второе имя container'а в этой сети. Практическая ценность: несколько container'ов могут иметь один и тот же псевдоним, и DNS вернёт все их адреса — простейшая балансировка (урок 8.4).
Подключение на лету
docker network connect backend web
docker network disconnect frontend web
Работает на запущенном container'е, перезапуск не нужен. Внутри появляется новый интерфейс eth1, маршруты обновляются.
Ограничение: приложение, прочитавшее адреса при старте, об изменении не узнает. DNS-имена от этого не страдают, поэтому очередной довод в пользу имён вместо адресов.
Внутренний механизм
Что создаёт docker network create
- Bridge-интерфейс на host с именем
br-<первые 12 символов ID>. - Подсеть из пула
default-address-pools, если не задана явно. - Правила netfilter:
MASQUERADEдля исходящего трафика, изоляция от других docker-сетей. - Запись во внутреннюю базу Docker.
Правило изоляции — то самое, что не даёт container'ам разных сетей общаться напрямую: трафик между двумя docker-bridge отбрасывается в цепочке DOCKER-ISOLATION-STAGE-1.
Как выглядит --internal
Для внутренней сети Docker не создаёт правило MASQUERADE и добавляет правила, отбрасывающие трафик, идущий наружу. Container сохраняет адрес и связь с соседями по сети, но не может обратиться ни к чему за её пределами — включая host.
Команды и примеры
DNS: default против user-defined
docker network create demo-net > /dev/null
echo "═══ default bridge ═══"
docker run -d --name d1 python:3.13-slim sleep 300 > /dev/null
docker run -d --name d2 python:3.13-slim sleep 300 > /dev/null
sleep 1
docker exec d1 getent hosts d2 && echo " имя разрешилось" || echo " ✗ имя d2 не разрешается"
echo "═══ user-defined bridge ═══"
docker run -d --network demo-net --name u1 python:3.13-slim sleep 300 > /dev/null
docker run -d --network demo-net --name u2 python:3.13-slim sleep 300 > /dev/null
sleep 1
docker exec u1 getent hosts u2 | sed 's/^/ /' && echo " ✓ имя разрешилось"
echo "═══ что в /etc/resolv.conf ═══"
printf ' default: %s\n' "$(docker exec d1 grep nameserver /etc/resolv.conf | head -1)"
printf ' user-defined: %s\n' "$(docker exec u1 grep nameserver /etc/resolv.conf | head -1)"
Ожидаемый вывод:
═══ default bridge ═══
✗ имя d2 не разрешается
═══ user-defined bridge ═══
172.18.0.3 u2
✓ имя разрешилось
═══ что в /etc/resolv.conf ═══
default: nameserver 127.0.0.11
user-defined: nameserver 127.0.0.11
Резолвер 127.0.0.11 подставлен в обоих случаях — но имена container'ов знает только сеть user-defined. Различие не в наличии DNS, а в содержимом его зоны.
Изоляция между сетями
docker network create net-a > /dev/null
docker network create net-b > /dev/null
docker run -d --network net-a --name in-a python:3.13-slim \
python -m http.server 8000 --bind 0.0.0.0 > /dev/null
docker run -d --network net-b --name in-b python:3.13-slim sleep 300 > /dev/null
sleep 3
ip_a="$(docker inspect -f '{{.NetworkSettings.Networks.net_a.IPAddress}}' in-a 2>/dev/null \
|| docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' in-a)"
echo " адрес in-a: $ip_a"
echo "═══ доступ по имени из другой сети ═══"
docker exec in-b getent hosts in-a > /dev/null 2>&1 \
&& echo " имя разрешилось" || echo " ✗ имя не разрешается"
echo "═══ доступ по IP из другой сети ═══"
docker exec in-b python -c "
import socket
s = socket.socket(); s.settimeout(3)
try:
s.connect(('$ip_a', 8000)); print(' соединение установлено')
except Exception as e:
print(f' ✗ недоступно: {type(e).__name__}')
"
echo "═══ подключаем in-b ко второй сети ═══"
docker network connect net-a in-b
sleep 1
docker exec in-b python -c "
import socket, urllib.request
print(' по имени:', socket.gethostbyname('in-a'))
r = urllib.request.urlopen('http://in-a:8000/', timeout=3)
print(' HTTP:', r.status)
"
echo "═══ интерфейсы in-b теперь ═══"
docker exec in-b sh -c 'awk "NR>2 {printf \" %s\n\", \$1}" /proc/net/dev'
Ожидаемый вывод:
адрес in-a: 172.19.0.2
═══ доступ по имени из другой сети ═══
✗ имя не разрешается
═══ доступ по IP из другой сети ═══
✗ недоступно: timeout
═══ подключаем in-b ко второй сети ═══
по имени: 172.19.0.2
HTTP: 200
═══ интерфейсы in-b теперь ═══
lo:
eth0:
eth1:
Изоляция работает на обоих уровнях: имя не разрешается и адрес недостижим. После подключения к общей сети появился второй интерфейс eth1, и связь заработала — без перезапуска container'а.
Обратите внимание на тип отказа: таймаут, а не Connection refused. Это подпись отбрасывания пакета правилом фильтрации, в отличие от отказа в соединении (урок 8.6).
docker rm -f d1 d2 u1 u2 in-a in-b > /dev/null
docker network rm demo-net net-a net-b > /dev/null
Разделение frontend и backend
docker network create frontend > /dev/null
docker network create --internal backend > /dev/null
# База — только в backend
docker run -d --network backend --name db \
-e POSTGRES_PASSWORD=secret -e POSTGRES_DB=appdb \
postgres:17-alpine > /dev/null
# Приложение — в обеих сетях
docker run -d --network backend --name web python:3.13-slim sleep 600 > /dev/null
docker network connect frontend web
# Прокси — только во frontend
docker run -d --network frontend --name proxy python:3.13-slim sleep 600 > /dev/null
sleep 8
echo "═══ web видит db ═══"
docker exec web python -c "
import socket
s = socket.socket(); s.settimeout(3)
try:
s.connect(('db', 5432)); print(' ✓ web → db: соединение установлено')
except Exception as e:
print(f' ✗ web → db: {type(e).__name__}')
"
echo "═══ proxy не видит db ═══"
docker exec proxy python -c "
import socket
s = socket.socket(); s.settimeout(3)
try:
s.connect(('db', 5432)); print(' ✗ proxy → db: соединение установлено (изоляции нет!)')
except socket.gaierror:
print(' ✓ proxy → db: имя не разрешается')
except Exception as e:
print(f' ✓ proxy → db: {type(e).__name__}')
"
echo "═══ proxy видит web ═══"
docker exec proxy getent hosts web | sed 's/^/ /'
echo "═══ db не имеет выхода наружу (--internal) ═══"
docker exec db sh -c 'ip route | head -2' 2>/dev/null | sed 's/^/ /'
docker exec db timeout 4 wget -q -O- https://example.com > /dev/null 2>&1 \
&& echo " ✗ выход в интернет есть" || echo " ✓ выхода в интернет нет"
echo "═══ сети container'а web ═══"
docker inspect web --format '{{range $n, $v := .NetworkSettings.Networks}} {{$n}}: {{$v.IPAddress}}
{{end}}'
Ожидаемый вывод:
═══ web видит db ═══
✓ web → db: соединение установлено
═══ proxy не видит db ═══
✓ proxy → db: имя не разрешается
═══ proxy видит web ═══
172.20.0.3 web
═══ db не имеет выхода наружу (--internal) ═══
172.21.0.0/16 dev eth0 scope link src 172.21.0.2
✓ выхода в интернет нет
═══ сети container'а web ═══
backend: 172.21.0.3
frontend: 172.20.0.3
Три уровня защиты видны в выводе.
proxy не может даже разрешить имя db — они в разных сетях, и DNS-зона у каждой своя.
У db в таблице маршрутов нет строки default: сеть создана с --internal, шлюз не назначен. Это отличается от блокировки правилом — маршрута просто не существует.
web имеет по адресу в каждой сети и служит единственным мостом.
docker rm -f db web proxy > /dev/null
docker network rm frontend backend > /dev/null
Порты видны внутри сети без публикации
docker network create portnet > /dev/null
docker run -d --network portnet --name server python:3.13-slim \
python -m http.server 8000 --bind 0.0.0.0 > /dev/null
docker run -d --network portnet --name client python:3.13-slim sleep 300 > /dev/null
sleep 3
echo "═══ опубликованные порты ═══"
docker port server 2>/dev/null | sed 's/^/ /' || echo " портов не опубликовано"
echo "═══ доступ с host ═══"
curl -s -m 3 -o /dev/null -w ' HTTP %{http_code}\n' http://localhost:8000/ 2>/dev/null \
|| echo " ✓ с host недоступен"
echo "═══ доступ из соседнего container ═══"
docker exec client python -c "
import urllib.request
print(' ✓ из сети доступен, HTTP', urllib.request.urlopen('http://server:8000/', timeout=3).status)
"
docker rm -f server client > /dev/null
docker network rm portnet > /dev/null
Ожидаемый вывод:
═══ опубликованные порты ═══
портов не опубликовано
═══ доступ с host ═══
✓ с host недоступен
═══ доступ из соседнего container ═══
✓ из сети доступен, HTTP 200
Порт не публиковался — и снаружи недоступен, а внутри сети работает. Это и есть причина не публиковать порт базы данных: приложению он доступен и так, а публикация открыла бы его всем.
Своя подсеть и читаемое имя bridge
docker network create \
--subnet 10.77.0.0/24 \
--gateway 10.77.0.1 \
--ip-range 10.77.0.128/25 \
-o com.docker.network.bridge.name=br-custom \
custom-net > /dev/null
echo "═══ параметры сети ═══"
docker network inspect custom-net --format '
подсеть: {{range .IPAM.Config}}{{.Subnet}}{{end}}
шлюз: {{range .IPAM.Config}}{{.Gateway}}{{end}}
пул: {{range .IPAM.Config}}{{.IPRange}}{{end}}
драйвер: {{.Driver}}
internal: {{.Internal}}'
echo "═══ bridge на host ═══"
ip -brief addr show br-custom 2>/dev/null | sed 's/^/ /'
echo "═══ адреса выдаются из --ip-range ═══"
for n in 1 2 3; do
docker run -d --network custom-net --name "c$n" alpine:3.21 sleep 60 > /dev/null
done
sleep 1
for n in 1 2 3; do
printf ' c%s: %s\n' "$n" \
"$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' "c$n")"
done
echo "═══ статический адрес вне пула ═══"
docker run -d --network custom-net --ip 10.77.0.10 --name static alpine:3.21 sleep 60 > /dev/null
printf ' static: %s\n' \
"$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' static)"
docker rm -f c1 c2 c3 static > /dev/null
docker network rm custom-net > /dev/null
Ожидаемый вывод:
═══ параметры сети ═══
подсеть: 10.77.0.0/24
шлюз: 10.77.0.1
пул: 10.77.0.128/25
драйвер: bridge
internal: false
═══ bridge на host ═══
br-custom UP 10.77.0.1/24
═══ адреса выдаются из --ip-range ═══
c1: 10.77.0.129
c2: 10.77.0.130
c3: 10.77.0.131
═══ статический адрес вне пула ═══
static: 10.77.0.10
Автоматические адреса берутся из --ip-range (10.77.0.128/25), а нижняя половина подсети остаётся свободной для статических назначений. Это стандартный приём, когда часть адресов должна быть фиксированной.
Имя br-custom вместо br-a1b2c3d4e5f6 заметно упрощает чтение tcpdump и правил netfilter.
Правило изоляции в netfilter
echo "═══ цепочки изоляции ═══"
sudo iptables -S DOCKER-ISOLATION-STAGE-1 2>/dev/null | head -5 | sed 's/^/ /' \
|| echo " (используется nftables-backend; см. nft list ruleset)"
Ожидаемый вывод:
═══ цепочки изоляции ═══
-N DOCKER-ISOLATION-STAGE-1
-A DOCKER-ISOLATION-STAGE-1 -i br-custom ! -o br-custom -j DOCKER-ISOLATION-STAGE-2
-A DOCKER-ISOLATION-STAGE-1 -i docker0 ! -o docker0 -j DOCKER-ISOLATION-STAGE-2
-A DOCKER-ISOLATION-STAGE-1 -j RETURN
Правило читается так: трафик, вошедший через один docker-bridge и уходящий не через него же, направляется на вторую стадию, где отбрасывается, если адресован другому docker-bridge.
Это и есть механизм изоляции сетей — обычные правила фильтрации, которые можно посмотреть и понять.
Практическое упражнение
Задание. Постройте трёхуровневую конфигурацию с проверяемой изоляцией.
Компоненты: proxy (внешний вход), api (приложение), db (база), cache (Redis).
Требования:
proxyдоступен с host по опубликованному порту.apiдоступен изproxyпо имени, но не с host.dbиcacheдоступны изapi, но не изproxyи не с host.dbиcacheне имеют выхода в интернет.- Ни один порт, кроме порта
proxy, не опубликован. - Каждое утверждение подтверждено проверкой; скрипт возвращает ненулевой код при нарушении.
Подсказки
Подсказка 1
Требования 2 и 3 задают, кто в какой сети состоит. Нарисуйте схему до написания команд.
Подсказка 2
Требование 4 — это флаг при создании сети, а не правило фильтрации.
Подсказка 3
Проверка «недоступен» должна отличать таймаут от отказа: и то и другое означает недоступность, но проверять надо оба варианта.
Решение
Показать решение
mkdir -p /tmp/netiso && cd /tmp/netiso
cat > setup.sh <<'SH'
#!/usr/bin/env bash
# Три сети: edge (вход), app (приложение), data (хранилища, без интернета).
set -euo pipefail
docker network create edge > /dev/null
docker network create app > /dev/null
docker network create --internal data > /dev/null
# db и cache — только в data
docker run -d --network data --name db \
-e POSTGRES_PASSWORD=secret -e POSTGRES_DB=appdb \
postgres:17-alpine > /dev/null
docker run -d --network data --name cache redis:8-alpine > /dev/null
# api — в app и data (мост между уровнями)
docker run -d --network app --name api python:3.13-slim \
python -m http.server 8000 --bind 0.0.0.0 > /dev/null
docker network connect data api
# proxy — в edge и app, единственный с опубликованным портом
docker run -d --network edge --name proxy -p 18080:8000 python:3.13-slim \
python -m http.server 8000 --bind 0.0.0.0 > /dev/null
docker network connect app proxy
SH
chmod +x setup.sh
cat > check.sh <<'SH'
#!/usr/bin/env bash
# Проверяет все шесть требований. Ненулевой код при нарушении изоляции.
set -uo pipefail
fail=0
ok() { printf ' ✓ %s\n' "$1"; }
bad() { printf ' ✗ %s\n' "$1"; fail=1; }
# Возвращает: ok | refused | timeout | dnsfail
probe() { # probe <container> <host> <port>
docker exec "$1" python - "$2" "$3" <<'PY' 2>/dev/null
import socket, sys
host, port = sys.argv[1], int(sys.argv[2])
s = socket.socket(); s.settimeout(3)
try:
s.connect((host, port)); print("ok")
except socket.gaierror:
print("dnsfail")
except ConnectionRefusedError:
print("refused")
except OSError:
print("timeout")
finally:
s.close()
PY
}
printf '\n═══ 1. proxy доступен с host ═══\n'
code="$(curl -s -m 5 -o /dev/null -w '%{http_code}' http://localhost:18080/)"
[ "$code" = "200" ] && ok "HTTP $code с host" || bad "получено '$code'"
printf '\n═══ 2. api: доступен из proxy, недоступен с host ═══\n'
[ "$(probe proxy api 8000)" = "ok" ] && ok "proxy → api" || bad "proxy не видит api"
if curl -s -m 3 -o /dev/null http://localhost:8000/ 2>/dev/null; then
bad "api отвечает на host:8000"
else
ok "api недоступен с host"
fi
printf '\n═══ 3. db и cache: из api — да, из proxy — нет ═══\n'
[ "$(probe api db 5432)" = "ok" ] && ok "api → db" || bad "api не видит db"
[ "$(probe api cache 6379)" = "ok" ] && ok "api → cache" || bad "api не видит cache"
r="$(probe proxy db 5432)"
[ "$r" != "ok" ] && ok "proxy ✗ db ($r)" || bad "proxy достучался до db"
r="$(probe proxy cache 6379)"
[ "$r" != "ok" ] && ok "proxy ✗ cache ($r)" || bad "proxy достучался до cache"
printf '\n═══ 4. data-сеть без выхода в интернет ═══\n'
for c in db cache; do
if docker exec "$c" timeout 4 wget -q -O- https://example.com > /dev/null 2>&1; then
bad "$c имеет выход в интернет"
else
ok "$c без выхода наружу"
fi
done
# Подтверждаем причину: отсутствие маршрута по умолчанию
for c in db cache; do
if docker exec "$c" sh -c 'ip route' 2>/dev/null | grep -q '^default'; then
bad "$c имеет маршрут по умолчанию"
else
ok "$c: маршрута по умолчанию нет (--internal)"
fi
done
printf '\n═══ 5. опубликован только порт proxy ═══\n'
published=0
for c in proxy api db cache; do
p="$(docker port "$c" 2>/dev/null)"
if [ -n "$p" ]; then
printf ' %-6s %s\n' "$c" "$(echo "$p" | tr '\n' ' ')"
published=$((published + 1))
[ "$c" = "proxy" ] || bad "$c публикует порты"
fi
done
[ "$published" -eq 1 ] && ok "публикует порты ровно один container" || bad "публикующих container'ов: $published"
printf '\n═══ карта сетей ═══\n'
for c in proxy api db cache; do
nets="$(docker inspect "$c" --format '{{range $n, $v := .NetworkSettings.Networks}}{{$n}} {{end}}')"
printf ' %-6s %s\n' "$c" "$nets"
done
printf '\n═══ ИТОГ ═══\n'
[ "$fail" -eq 0 ] && echo " изоляция подтверждена" || echo " ЕСТЬ НАРУШЕНИЯ"
exit "$fail"
SH
chmod +x check.sh
cat > teardown.sh <<'SH'
#!/usr/bin/env bash
docker rm -f proxy api db cache > /dev/null 2>&1
docker network rm edge app data > /dev/null 2>&1
SH
chmod +x teardown.sh
./setup.sh
sleep 10
./check.sh
echo "КОД: $?"
./teardown.sh
cd /tmp && rm -rf /tmp/netiso
Ожидаемый вывод:
═══ 1. proxy доступен с host ═══
✓ HTTP 200 с host
═══ 2. api: доступен из proxy, недоступен с host ═══
✓ proxy → api
✓ api недоступен с host
═══ 3. db и cache: из api — да, из proxy — нет ═══
✓ api → db
✓ api → cache
✓ proxy ✗ db (dnsfail)
✓ proxy ✗ cache (dnsfail)
═══ 4. data-сеть без выхода в интернет ═══
✓ db без выхода наружу
✓ cache без выхода наружу
✓ db: маршрута по умолчанию нет (--internal)
✓ cache: маршрута по умолчанию нет (--internal)
═══ 5. опубликован только порт proxy ═══
proxy 8000/tcp -> 0.0.0.0:18080
✓ публикует порты ровно один container
═══ карта сетей ═══
proxy app edge
api app data
db data
cache data
═══ ИТОГ ═══
изоляция подтверждена
КОД: 0
Все шесть требований выполнены.
Три решения, определяющие качество.
Функция probe различает четыре исхода, а не два. Проверка «соединение не установилось» прошла бы и при опечатке в имени container'а — тест был бы зелёным, ничего не проверяя. Возвращая dnsfail, refused или timeout, проверка сообщает причину, и в выводе видно, что изоляция сработала на уровне DNS, а не из-за ошибки в скрипте.
Требование 4 проверяется дважды: попыткой выхода и отсутствием маршрута. Первая проверка отвечает на вопрос «работает ли»; вторая — «почему». Если бы wget не сработал по другой причине (нет самого wget, нет DNS вовне), первая проверка дала бы ложноположительный результат. Отсутствие строки default в таблице маршрутов — прямое следствие --internal и подтверждает именно его.
Требование 5 проверяется обходом всех container'ов, а не только ожидаемого. Скрипт не спрашивает «публикует ли proxy порт», он спрашивает «кто вообще публикует порты» и считает их. Так обнаружится случайно оставленный -p 5432:5432 у базы — самая частая утечка в таких конфигурациях.
Чего решение не делает. Изоляция здесь действует на уровне сетей Docker и защищает от обращений между container'ами. Она не мешает процессу, получившему выполнение кода внутри api, обратиться и к db, и наружу — api состоит в обеих сетях по определению задачи. Не покрыт и доступ с host: db недоступна по опубликованному порту, но администратор host может обратиться к 172.x.x.x:5432 напрямую, поскольку маршрут к docker-сетям на host существует. Полная изоляция от host требует правил фильтрации на самом host (раздел 12).
Проверка результата
docker network create checknet > /dev/null
docker run -d --network checknet --name a alpine:3.21 sleep 60 > /dev/null
docker run -d --network checknet --name b alpine:3.21 sleep 60 > /dev/null
docker exec a getent hosts b
docker rm -f a b > /dev/null; docker network rm checknet > /dev/null
Ожидается строка с IP-адресом и именем b.
Типичные ошибки
| Ошибка | Причина | Исправление |
|---|---|---|
| Использование default bridge | Работает без настройки | Нет DNS по именам; создать свою сеть |
--link для связи container'ов | Старые руководства | Устарело; user-defined сеть плюс имена |
| Публикация порта базы данных | «Чтобы приложение достучалось» | Внутри сети порт доступен и без публикации |
| Обращение по IP вместо имени | Кажется надёжнее | Адрес меняется при пересоздании |
| Все сервисы в одной сети | Проще | Нет изоляции; разделить на уровни |
--internal для сети приложения | Хотели «побезопаснее» | Пропадёт доступ к внешним API и обновлениям |
Статический адрес без --subnet | Не знали о требовании | Docker откажет: адрес вне управляемой подсети |
| Ожидают, что изоляция защитит и от host | Считают её полной | На host есть маршрут к docker-сетям |
| Таймаут и отказ считают одним | Оба «не работает» | Таймаут — фильтрация, отказ — хост ответил |
Контрольные вопросы
На понимание:
- Назовите четыре различия default bridge и user-defined bridge.
- Почему
127.0.0.11присутствует в обеих сетях, но имена работают только в одной? - Почему порт базы данных не нужно публиковать?
- Что именно делает
--internal? - Чем таймаут отличается от
Connection refusedпри проверке изоляции?
На применение:
- Как сделать базу недоступной из внешней сети, но доступной приложению?
- Как подключить работающий container ко второй сети?
- Как задать сети конкретную подсеть и читаемое имя bridge?
На диагностику:
- Два container'а в Compose видят друг друга, а запущенные
docker run— нет. Почему? - Приложение обращается к
db:5432и получаетName or service not known. Версия?
Краткое резюме
- В default bridge нет разрешения имён container'ов — только IP или устаревший
--link. - User-defined сеть даёт DNS, изоляцию, псевдонимы и настраиваемую адресацию.
- Резолвер
127.0.0.11есть в обеих сетях; различается содержимое его зоны. - Container'ы разных сетей не видят друг друга ни по имени, ни по адресу.
- Внутри одной сети доступны все порты соседей, а не только опубликованные.
- Публикация нужна для доступа с host и извне, а не для связи между container'ами.
--internalубирает маршрут по умолчанию: нет ни выхода, ни входа.docker network connectработает на запущенном container'е и добавляет интерфейс.- Container в двух сетях служит мостом между уровнями — основа схемы frontend/backend.
--subnetтребуется для статических адресов;--ip-rangeотделяет автоматический пул.- Изоляцию сетей реализуют цепочки
DOCKER-ISOLATION-STAGE-1и-2. - Изоляция действует между container'ами, но не защищает от доступа с самого host.
Официальные источники
| Источник | Ссылка | Что подтверждает |
|---|---|---|
| Docker: bridge networks | https://docs.docker.com/engine/network/drivers/bridge/ | Различия default и user-defined, параметры создания |
| Docker: network tutorials | https://docs.docker.com/engine/network/tutorials/standalone/ | Практика с user-defined сетями |
Docker: docker network create | https://docs.docker.com/reference/cli/docker/network/create/ | --subnet, --internal, --ip-range, опции драйвера |
Docker: docker network connect | https://docs.docker.com/reference/cli/docker/network/connect/ | Подключение на лету, псевдонимы |
| Docker: packet filtering and firewalls | https://docs.docker.com/engine/network/packet-filtering-firewalls/ | Цепочки изоляции, взаимодействие с host |
| Docker: legacy container links | https://docs.docker.com/engine/network/links/ | Статус --link |
Навигация
← Предыдущий материал
Вернуться к разделу
Следующий материал → Port publishing
Главное оглавление