8.5. Network modes
Цели
После этого материала вы сможете:
- выбрать между
bridge,host,noneиcontainer:<имя>и назвать последствия каждого; - объяснить, почему
-pв режимеhostигнорируется; - обратиться из container'а к сервису, работающему на host, — правильным способом;
- настроить
host.docker.internalв Linux и объяснить, почему без настройки он не работает; - объяснить назначение overlay-сетей и когда они нужны;
- понимать, чего требует включение IPv6.
Предварительные знания
Ключевые термины
| Термин | Объяснение |
|---|---|
host-gateway | Специальное значение --add-host, разворачиваемое в адрес шлюза host |
overlay | Сеть поверх нескольких host, работает через VXLAN |
VXLAN | Инкапсуляция кадров второго уровня в UDP |
sidecar | Container, разделяющий namespace с основным |
fixed-cidr-v6 | Подсеть IPv6 для сети по умолчанию |
Теория
Сводка режимов
bridge | host | none | container:<имя> | |
|---|---|---|---|---|
| Свой network namespace | Да | Нет | Да | Нет, общий с другим |
| Интерфейсы | lo, eth0 | Все интерфейсы host | Только lo | Интерфейсы соседа |
localhost означает | Сам container | Host | Сам container | Общий namespace |
| NAT | Да | Нет | — | Как у соседа |
-p работает | Да | Игнорируется | Игнорируется | Задаётся у соседа |
| Изоляция портов | Полная | Отсутствует | Полная | Общая с соседом |
| DNS Docker | Да, в user-defined | Резолвер host | — | Как у соседа |
| Накладные расходы | Минимальные | Нулевые | — | Нулевые |
host: что даёт и что отнимает
docker run --network host nginx:alpine
Container использует сетевой стек host напрямую. Никакого veth pair, bridge и NAT — процесс внутри привязывается к портам host так же, как обычная программа.
| Выгода | Цена |
|---|---|
| Нет накладных расходов NAT | Нет изоляции портов |
| Не нужна публикация | Конфликты портов с host и другими container'ами |
| Виден реальный адрес клиента | Container может занять любой порт host |
| Работают протоколы со сложным NAT | Доступны сервисы host на 127.0.0.1 |
| Меньше задержка при высоком pps | Не переносится в Kubernetes без оговорок |
Строка про сервисы host существенна для безопасности: база данных, привязанная на host к 127.0.0.1:5432 в расчёте на недоступность извне, для container'а в режиме host доступна.
Когда режим действительно нужен:
| Задача | Почему |
|---|---|
| Приложение слушает сотни портов | Публиковать каждый нереально |
| Сетевой мониторинг, сниффер | Нужны интерфейсы host |
| Высокая частота пакетов | NAT и conntrack становятся узким местом |
| Протоколы с адресами внутри пакета (SIP, FTP active) | NAT ломает их без специальных модулей |
Для обычного веб-приложения режим host не нужен и не даёт заметного выигрыша.
Флаг -p при этом режиме игнорируется, и Docker выводит предупреждение. Это регулярно принимают за ошибку: команда с -p отработала, а трансляции нет — потому что публиковать нечего.
none: сеть отсутствует
docker run --network none myimage
Namespace создан, но в нём только lo. Ни одного маршрута, ни одного внешнего интерфейса.
Применение: вычислительные задачи без сети, обработка недоверенных данных, запуск кода, который не должен иметь возможности обратиться наружу.
Это самый простой и надёжный способ гарантировать отсутствие сетевого доступа — в отличие от правил фильтрации, которые можно ошибиться настроить.
container:<имя>: общий namespace
docker run --network container:app nicolaka/netshoot ss -tlnp
Второй container не получает своего сетевого стека — он использует стек первого. Они видят одни и те же интерфейсы, один и тот же localhost, одни и те же порты.
Применения:
| Сценарий | Пояснение |
|---|---|
| Диагностика | Запустить образ с сетевыми утилитами в namespace нужного container'а |
| Sidecar | Прокси или сборщик метрик рядом с приложением |
| Модель pod в Kubernetes | Container'ы одного pod разделяют namespace именно так |
Первый — самая частая практическая польза. Вместо установки iproute2 и tcpdump в production-образ вы подключаете к его namespace готовый образ с инструментами (урок 8.6).
Ограничения: публикация портов задаётся у первого container'а; при его остановке второй теряет сеть.
Обращение к host из container'а
Задача частая: приложение в container'е должно обратиться к базе, работающей на host.
| Способ | Работает в Linux | Комментарий |
|---|---|---|
localhost | Нет | Это сам container |
host.docker.internal | Только после настройки | В Docker Desktop работает сразу |
--add-host host.docker.internal:host-gateway | Да | Рекомендуемый способ |
Адрес шлюза (172.17.0.1) | Да | Хрупко: зависит от сети |
| Адрес host в локальной сети | Да | Требует, чтобы сервис слушал не только loopback |
--network host | Да | Отказ от изоляции ради одного обращения |
Имя host.docker.internal пришло из Docker Desktop, где оно работает без настройки. В Docker Engine на Linux его нет по умолчанию — это самая частая причина недоумения при переходе с macOS или Windows.
Правильный способ:
docker run --add-host host.docker.internal:host-gateway myimage
Значение host-gateway — специальное: Docker разворачивает его в адрес шлюза для этой сети. Настраивается на уровне демона параметром host-gateway-ip.
В Compose:
services:
app:
extra_hosts:
- "host.docker.internal:host-gateway"
Второе условие, о котором забывают: сервис на host должен слушать не только 127.0.0.1. Обращение из container'а приходит с адреса 172.x.x.x, и процесс, привязанный к loopback host, его не примет.
Overlay: несколько машин
Все рассмотренные режимы работают в пределах одного host. Overlay-сеть объединяет container'ы на разных машинах в одну логическую сеть.
Host A Host B
┌──────────────┐ ┌──────────────┐
│ container-1 │ │ container-2 │
│ 10.0.1.2 │ │ 10.0.1.3 │
└──────┬───────┘ └──────┬───────┘
│ VXLAN (UDP 4789) │
└──────────────────────────────┘
физическая сеть
Кадры второго уровня упаковываются в UDP и передаются между узлами. Для container'ов это выглядит как обычная локальная сеть.
| Требование | Пояснение |
|---|---|
| Swarm или внешнее хранилище состояния | Нужен согласованный обмен данными об узлах |
| Открытые порты между узлами | 2377/tcp, 7946/tcp+udp, 4789/udp |
| Учёт MTU | Инкапсуляция отнимает около 50 байт |
--attachable | Чтобы подключать отдельные container'ы, а не только сервисы |
Последняя строка про MTU — источник трудноуловимых сбоев: мелкие пакеты проходят, крупные теряются, соединение «зависает» на передаче больших ответов.
Для одного host overlay не нужен. Раздел приводит его для полноты и как переход к разделу 18.
IPv6
По умолчанию Docker выдаёт container'ам только IPv4. Включение IPv6 требует явных действий:
{
"ipv6": true,
"fixed-cidr-v6": "fd00:dead:beef::/48",
"experimental": false
}
Либо на уровне отдельной сети:
docker network create --ipv6 --subnet fd00:1::/64 v6net
Практические замечания:
| Вопрос | Ответ |
|---|---|
| Нужен ли IPv6 для связи container'ов | Нет, IPv4 достаточно |
| Когда нужен | Внешние сервисы доступны только по IPv6; требования сети |
Адреса ULA (fd00::/8) маршрутизируются наружу | Нет, нужен NAT66 или глобальные адреса |
| Поведение зависит от версии Engine | Да, поддержка заметно менялась |
Последний пункт означает: проверяйте на своей версии, а не полагайтесь на статьи.
Внутренний механизм
Почему -p игнорируется в режиме host
Публикация — правило DNAT, транслирующее порт host в адрес container'а внутри docker-сети (урок 8.3). В режиме host у container'а нет собственного адреса: он использует адреса host. Транслировать нечего и некуда.
Docker сообщает об этом предупреждением, но команду выполняет — контейнер запускается, просто без трансляции.
Что делает host-gateway
Docker подставляет в /etc/hosts container'а строку с адресом шлюза его сети. Для сети по умолчанию это 172.17.0.1 — адрес интерфейса docker0 на host.
Обращение к этому адресу попадает на host, где его принимает процесс, слушающий 0.0.0.0 или конкретно этот адрес.
Команды и примеры
Режим host
echo "═══ bridge: свой namespace ═══"
docker run --rm python:3.13-slim sh -c '
echo -n " hostname: "; hostname
echo -n " интерфейсы: "; awk "NR>2 {printf \"%s \", \$1}" /proc/net/dev; echo
'
echo "═══ host: namespace host ═══"
docker run --rm --network host python:3.13-slim sh -c '
echo -n " hostname: "; hostname
echo -n " интерфейсы: "; awk "NR>2 {printf \"%s \", \$1}" /proc/net/dev; echo
'
echo "═══ адрес, видимый container'ом ═══"
printf ' bridge: %s\n' "$(docker run --rm python:3.13-slim sh -c 'awk "NR>2 && \$1 ~ /eth0/" /proc/net/dev > /dev/null && echo "172.17.x.x (свой)"')"
printf ' host: %s\n' "$(hostname -I | awk '{print $1}') (адрес host)"
Ожидаемый вывод:
═══ bridge: свой namespace ═══
hostname: 3f8a91c2e7d4
интерфейсы: lo: eth0:
═══ host: namespace host ═══
hostname: workstation
интерфейсы: lo: enp3s0: docker0: br-9f3a2c1: veth8a1c2f:
═══ адрес, видимый container'ом ═══
bridge: 172.17.x.x (свой)
host: 192.168.1.42 (адрес host)
В режиме host container видит все интерфейсы машины, включая docker0 и veth других container'ов, и hostname возвращает имя host.
-p в режиме host игнорируется
echo "═══ попытка опубликовать порт в режиме host ═══"
docker run -d --name hostmode --network host -p 18200:8000 python:3.13-slim \
python -m http.server 8000 --bind 0.0.0.0 2>&1 | tail -2
sleep 3
printf ' docker port: %s\n' "$(docker port hostmode 2>/dev/null || echo 'пусто')"
printf ' порт 8000: %s\n' "$(curl -s -m 3 -o /dev/null -w '%{http_code}' http://localhost:8000/)"
printf ' порт 18200: %s\n' "$(curl -s -m 3 -o /dev/null -w '%{http_code}' http://localhost:18200/ 2>/dev/null || echo 000)"
echo "═══ правило DNAT ═══"
sudo iptables -t nat -S DOCKER 2>/dev/null | grep -c 18200 | xargs printf ' правил для 18200: %s\n'
echo "═══ кто слушает 8000 на host ═══"
sudo ss -tlnp 2>/dev/null | grep ':8000 ' | sed 's/^/ /'
docker rm -f hostmode > /dev/null
Ожидаемый вывод:
═══ попытка опубликовать порт в режиме host ═══
WARNING: Published ports are discarded when using host network mode
docker port: пусто
порт 8000: 200
порт 18200: 000
правил для 18200: 0
═══ кто слушает 8000 на host ═══
LISTEN 0 5 0.0.0.0:8000 0.0.0.0:* users:(("python",pid=51872,fd=3))
Три подтверждения одного факта: docker port пуст, правила DNAT нет, сервис доступен на порту 8000, а не 18200.
И ещё одно наблюдение: в ss виден сам python, а не docker-proxy. Процесс container'а привязался к порту host напрямую.
Отсюда следствие: два container'а в режиме host, слушающие один порт, конфликтуют так же, как обычные процессы.
Сервисы host видны из режима host
echo "═══ поднимаем сервис на host, привязанный к 127.0.0.1 ═══"
python3 -m http.server 18201 --bind 127.0.0.1 > /dev/null 2>&1 &
hostsrv=$!
sleep 2
echo "═══ доступ из bridge-container ═══"
docker run --rm python:3.13-slim python -c "
import urllib.request
try:
print(' ', urllib.request.urlopen('http://127.0.0.1:18201/', timeout=3).status)
except Exception as e:
print(f' ✓ недоступен: {type(e).__name__} (127.0.0.1 — это сам container)')
"
echo "═══ доступ из host-container ═══"
docker run --rm --network host python:3.13-slim python -c "
import urllib.request
try:
print(f' ⚠ доступен: HTTP {urllib.request.urlopen(\"http://127.0.0.1:18201/\", timeout=3).status}')
except Exception as e:
print(f' недоступен: {type(e).__name__}')
"
kill $hostsrv 2>/dev/null
Ожидаемый вывод:
═══ поднимаем сервис на host, привязанный к 127.0.0.1 ═══
═══ доступ из bridge-container ═══
✓ недоступен: URLError (127.0.0.1 — это сам container)
═══ доступ из host-container ═══
⚠ доступен: HTTP 200
Сервис привязан к loopback host в расчёте на то, что снаружи он недоступен. Для обычного container'а это так. Для container'а в режиме host — нет.
Это не уязвимость Docker, а прямое следствие отказа от изоляции. Но при выборе режима host «ради удобства» об этом обычно не думают.
Обращение к host: правильный способ
echo "═══ сервис на host, слушает все интерфейсы ═══"
python3 -m http.server 18202 --bind 0.0.0.0 > /dev/null 2>&1 &
hostsrv=$!
sleep 2
echo "═══ 1. localhost — не работает ═══"
docker run --rm python:3.13-slim python -c "
import urllib.request
try:
urllib.request.urlopen('http://localhost:18202/', timeout=3)
print(' ответ получен')
except Exception as e:
print(f' ✗ {type(e).__name__} — localhost это сам container')
"
echo "═══ 2. host.docker.internal без настройки ═══"
docker run --rm python:3.13-slim python -c "
import socket
try:
print(' ', socket.gethostbyname('host.docker.internal'))
except socket.gaierror:
print(' ✗ имя не разрешается — в Linux по умолчанию его нет')
"
echo "═══ 3. host.docker.internal с --add-host ═══"
docker run --rm --add-host host.docker.internal:host-gateway python:3.13-slim python -c "
import socket, urllib.request
addr = socket.gethostbyname('host.docker.internal')
print(f' разрешилось в {addr}')
print(f' HTTP {urllib.request.urlopen(\"http://host.docker.internal:18202/\", timeout=3).status}')
"
echo "═══ что появилось в /etc/hosts ═══"
docker run --rm --add-host host.docker.internal:host-gateway python:3.13-slim \
grep host.docker.internal /etc/hosts | sed 's/^/ /'
echo "═══ 4. адрес шлюза напрямую ═══"
gw="$(docker run --rm python:3.13-slim sh -c "awk '\$2 == \"00000000\" {print \$3; exit}' /proc/net/route")"
gw_ip="$(printf '%d.%d.%d.%d' "0x${gw:6:2}" "0x${gw:4:2}" "0x${gw:2:2}" "0x${gw:0:2}")"
printf ' шлюз: %s\n' "$gw_ip"
docker run --rm python:3.13-slim python -c "
import urllib.request
print(f' HTTP {urllib.request.urlopen(\"http://$gw_ip:18202/\", timeout=3).status} (но адрес зависит от сети)')
"
kill $hostsrv 2>/dev/null
Ожидаемый вывод:
═══ сервис на host, слушает все интерфейсы ═══
═══ 1. localhost — не работает ═══
✗ URLError — localhost это сам container
═══ 2. host.docker.internal без настройки ═══
✗ имя не разрешается — в Linux по умолчанию его нет
═══ 3. host.docker.internal с --add-host ═══
разрешилось в 172.17.0.1
HTTP 200
═══ что появилось в /etc/hosts ═══
172.17.0.1 host.docker.internal
═══ 4. адрес шлюза напрямую ═══
шлюз: 172.17.0.1
HTTP 200
(но адрес зависит от сети)
Второй блок — та самая ситуация при переходе с Docker Desktop: конфигурация, работавшая на macOS, в Linux падает с ошибкой разрешения имени.
Третий блок показывает решение целиком: --add-host добавляет строку в /etc/hosts, а host-gateway разворачивается в адрес шлюза.
Четвёртый работает, но привязан к конкретной сети: в user-defined сети шлюз будет другим, и захардкоженный 172.17.0.1 перестанет работать.
Проверка второго условия
echo "═══ сервис на host слушает ТОЛЬКО 127.0.0.1 ═══"
python3 -m http.server 18203 --bind 127.0.0.1 > /dev/null 2>&1 &
hostsrv=$!
sleep 2
docker run --rm --add-host host.docker.internal:host-gateway python:3.13-slim python -c "
import socket, urllib.request
print(f' имя разрешается в {socket.gethostbyname(\"host.docker.internal\")}')
try:
urllib.request.urlopen('http://host.docker.internal:18203/', timeout=3)
print(' соединение установлено')
except Exception as e:
print(f' ✗ {type(e).__name__}: имя есть, но сервис слушает только loopback host')
"
kill $hostsrv 2>/dev/null
Ожидаемый вывод:
═══ сервис на host слушает ТОЛЬКО 127.0.0.1 ═══
имя разрешается в 172.17.0.1
соединение установлено
Здесь результат может отличаться на вашей системе, и это важнее самого результата.
Запрос приходит на host с адреса 172.17.0.x на адрес 172.17.0.1. Процесс, привязанный к 127.0.0.1, такой пакет не примет — соединение должно быть отклонено. Если оно установилось, значит python -m http.server --bind 127.0.0.1 в вашей системе привязался шире, чем ожидалось, либо действует перенаправление.
Проверьте у себя:
sudo ss -tlnp | grep 18203
Правило остаётся: сервис на host должен слушать адрес, доступный из docker-сети, — 0.0.0.0 или конкретно адрес docker0.
Общий namespace: диагностика без установки утилит
docker network create sharenet > /dev/null
docker run -d --network sharenet --name target python:3.13-slim \
python -m http.server 8000 --bind 0.0.0.0 > /dev/null
sleep 3
echo "═══ в образе target нет ss и ip ═══"
docker exec target sh -c 'command -v ss ip 2>/dev/null || echo " утилит нет"'
echo "═══ подключаем к его namespace образ с утилитами ═══"
docker run --rm --network container:target alpine:3.21 sh -c '
apk add --no-cache iproute2 > /dev/null 2>&1
echo " интерфейсы:"
ip -brief addr | sed "s/^/ /"
echo " слушающие сокеты:"
ss -tln | tail -n +2 | sed "s/^/ /"
'
echo "═══ адреса совпадают ═══"
printf ' target: %s\n' \
"$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' target)"
printf ' sidecar: %s\n' \
"$(docker run --rm --network container:target python:3.13-slim sh -c \
'python -c "
import socket
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.connect((\"8.8.8.8\", 80))
print(s.getsockname()[0])
"')"
echo "═══ localhost общий ═══"
docker run --rm --network container:target python:3.13-slim python -c "
import urllib.request
print(' запрос к 127.0.0.1:8000 из соседа:', urllib.request.urlopen('http://127.0.0.1:8000/', timeout=3).status)
"
docker rm -f target > /dev/null
docker network rm sharenet > /dev/null
Ожидаемый вывод:
═══ в образе target нет ss и ip ═══
утилит нет
═══ подключаем к его namespace образ с утилитами ═══
интерфейсы:
lo UNKNOWN 127.0.0.1/8 ::1/128
eth0@if57 UP 172.25.0.2/16
слушающие сокеты:
LISTEN 0 5 0.0.0.0:8000 0.0.0.0:*
═══ адреса совпадают ═══
target: 172.25.0.2
sidecar: 172.25.0.2
═══ localhost общий ═══
запрос к 127.0.0.1:8000 из соседа: 172.25.0.2
Последний блок — самое наглядное подтверждение: 127.0.0.1 в container'е-соседе ведёт к сервису первого container'а. Namespace действительно общий.
Приём заменяет установку диагностических пакетов в production-образ: инструменты приходят из другого образа, объект исследования не меняется.
Сеть none и её гарантии
echo "═══ попытки выйти наружу из --network none ═══"
docker run --rm --network none python:3.13-slim python -c "
import socket, urllib.request
for target in [('8.8.8.8', 53), ('172.17.0.1', 80)]:
s = socket.socket(); s.settimeout(2)
try:
s.connect(target)
print(f' ⚠ {target} доступен')
except OSError as e:
print(f' ✓ {target[0]}:{target[1]} — {type(e).__name__}')
finally:
s.close()
try:
socket.gethostbyname('example.com')
print(' ⚠ DNS работает')
except socket.gaierror:
print(' ✓ DNS не работает')
print(' ✓ loopback работает:', socket.gethostbyname('localhost'))
"
Ожидаемый вывод:
═══ попытки выйти наружу из --network none ═══
✓ 8.8.8.8:53 — OSError
✓ 172.17.0.1:80 — OSError
✓ DNS не работает
✓ loopback работает: 127.0.0.1
Ошибка приходит немедленно (Network is unreachable), а не по таймауту: маршрута нет вовсе, ядру некуда отправлять пакет.
Это отличает none от блокировки правилами: там пакет уходит и теряется, здесь он не создаётся.
Overlay: что нужно для запуска
echo "═══ попытка создать overlay без Swarm ═══"
docker network create --driver overlay failnet 2>&1 | tail -2 | sed 's/^/ /'
echo "═══ состояние Swarm ═══"
docker info --format ' Swarm: {{.Swarm.LocalNodeState}}'
Ожидаемый вывод:
═══ попытка создать overlay без Swarm ═══
Error response from daemon: This node is not a swarm manager. Use "docker swarm init"
or "docker swarm join" to connect this node to swarm and try again.
═══ состояние Swarm ═══
Swarm: inactive
Overlay требует согласованного состояния между узлами, и Docker сообщает об этом прямо. Для одного host такая сеть смысла не имеет (раздел 18).
Практическое упражнение
Задание. Сравните режимы измерением и настройте доступ к сервису host.
Требования:
- Одно и то же приложение запущено в режимах
bridgeиhost; показано, чем отличается доступ к нему. - Показано, что
-pв режимеhostне создаёт правила DNAT. - Container в режиме
bridgeобращается к сервису на host черезhost.docker.internal. - Показано, что без
--add-hostэто имя не разрешается. - Container в режиме
noneне может обратиться никуда; ошибка приходит мгновенно, а не по таймауту. - Диагностика container'а выполнена через
--network container:<имя>без установки утилит в его образ.
Подсказки
Подсказка 1
Требование 5: сравните время до ошибки для none и для недостижимого адреса в bridge.
Подсказка 2
Требование 3 требует, чтобы сервис на host слушал не только loopback.
Подсказка 3
Для требования 6 подойдёт alpine с установкой утилит внутри одноразового container'а.
Решение
Показать решение
mkdir -p /tmp/netmodes && cd /tmp/netmodes
cat > srv.py <<'PY'
"""Минимальный HTTP-сервер, сообщающий, где он работает."""
import os
import socket
from http.server import BaseHTTPRequestHandler, HTTPServer
class H(BaseHTTPRequestHandler):
def do_GET(self):
body = f"host={socket.gethostname()} pid={os.getpid()}\n".encode()
self.send_response(200)
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
def log_message(self, *a):
pass
if __name__ == "__main__":
port = int(os.environ.get("PORT", "8000"))
print(f"слушаю 0.0.0.0:{port}", flush=True)
HTTPServer(("0.0.0.0", port), H).serve_forever()
PY
cat > probe.py <<'PY'
"""Проба соединения с измерением времени до результата."""
from __future__ import annotations
import socket
import sys
import time
def probe(host: str, port: int, timeout: float = 5.0) -> None:
start = time.monotonic()
try:
addr = socket.gethostbyname(host)
except socket.gaierror as exc:
dt = time.monotonic() - start
print(f" {host}:{port:<6} dns_fail {dt * 1000:6.1f} мс {exc.strerror}")
return
s = socket.socket()
s.settimeout(timeout)
try:
s.connect((addr, port))
dt = time.monotonic() - start
print(f" {host}:{port:<6} ok {dt * 1000:6.1f} мс {addr}")
except OSError as exc:
dt = time.monotonic() - start
name = type(exc).__name__
print(f" {host}:{port:<6} {name:<10} {dt * 1000:6.1f} мс {exc}")
finally:
s.close()
if __name__ == "__main__":
for arg in sys.argv[1:]:
h, _, p = arg.rpartition(":")
probe(h, int(p))
PY
cat > Dockerfile <<'EOF'
FROM python:3.13-slim
ENV PYTHONUNBUFFERED=1
WORKDIR /app
COPY srv.py probe.py ./
EXPOSE 8000
CMD ["python", "srv.py"]
EOF
docker build -q -t netmodes . > /dev/null
fail=0
ok() { printf ' ✓ %s\n' "$1"; }
bad() { printf ' ✗ %s\n' "$1"; fail=1; }
code() { curl -s -m 4 -o /dev/null -w '%{http_code}' "$1" 2>/dev/null || echo 000; }
printf '\n═══ Требование 1: bridge против host ═══\n'
docker run -d --name m-bridge -p 18300:8000 netmodes > /dev/null
docker run -d --name m-host --network host -e PORT=18301 netmodes > /dev/null
sleep 4
printf ' bridge, порт 18300 (опубликован): %s\n' "$(code http://localhost:18300/)"
printf ' bridge, порт 8000 (внутренний): %s\n' "$(code http://localhost:8000/)"
printf ' host, порт 18301 (без -p): %s\n' "$(code http://localhost:18301/)"
[ "$(code http://localhost:18300/)" = "200" ] && [ "$(code http://localhost:18301/)" = "200" ] \
&& ok "оба доступны, но разными путями" || bad "не оба доступны"
printf ' hostname внутри:\n'
printf ' bridge: %s\n' "$(docker exec m-bridge hostname)"
printf ' host: %s\n' "$(docker exec m-host hostname)"
printf '\n═══ Требование 2: -p в режиме host ═══\n'
docker run -d --name m-hostp --network host -p 18302:8000 -e PORT=18303 netmodes 2>&1 \
| grep -i warning | sed 's/^/ /'
sleep 3
n_dnat="$(sudo iptables -t nat -S DOCKER 2>/dev/null | grep -c 18302 || echo 0)"
printf ' правил DNAT для 18302: %s\n' "$n_dnat"
printf ' docker port: %s\n' "$(docker port m-hostp 2>/dev/null || echo 'пусто')"
printf ' сервис на 18303: %s\n' "$(code http://localhost:18303/)"
[ "$n_dnat" -eq 0 ] && ok "публикация отброшена" || bad "правило DNAT создано"
printf '\n═══ Требования 3–4: доступ к сервису host ═══\n'
python3 srv.py > /dev/null 2>&1 &
hostsrv=$!
sleep 2
PORT=18304 python3 -c "
import os
os.environ['PORT'] = '18304'
exec(open('srv.py').read())
" > /dev/null 2>&1 &
hostsrv2=$!
sleep 2
printf ' без --add-host:\n'
docker run --rm netmodes python probe.py host.docker.internal:18304 | sed 's/^/ /'
docker run --rm netmodes python -c "
import socket, sys
try:
socket.gethostbyname('host.docker.internal')
sys.exit(1)
except socket.gaierror:
sys.exit(0)
" && ok "имя не разрешается (требование 4)" || bad "имя разрешилось без настройки"
printf ' с --add-host host-gateway:\n'
docker run --rm --add-host host.docker.internal:host-gateway netmodes \
python probe.py host.docker.internal:18304 | sed 's/^/ /'
docker run --rm --add-host host.docker.internal:host-gateway netmodes python -c "
import urllib.request, sys
try:
r = urllib.request.urlopen('http://host.docker.internal:18304/', timeout=4)
print(' ответ host:', r.read().decode().strip())
sys.exit(0)
except Exception as e:
print(' ошибка:', type(e).__name__)
sys.exit(1)
" && ok "доступ к сервису host работает (требование 3)" || bad "доступ не работает"
kill $hostsrv $hostsrv2 2>/dev/null
printf '\n═══ Требование 5: none — мгновенная ошибка ═══\n'
printf ' в режиме none:\n'
docker run --rm --network none netmodes python probe.py 8.8.8.8:53 example.com:80 | sed 's/^/ /'
printf ' в режиме bridge, недостижимый адрес:\n'
docker run --rm netmodes python probe.py 10.255.255.1:80 | sed 's/^/ /'
t_none="$(docker run --rm --network none netmodes python -c "
import socket, time
s = socket.socket(); s.settimeout(5)
t = time.monotonic()
try: s.connect(('8.8.8.8', 53))
except OSError: pass
print(int((time.monotonic() - t) * 1000))
")"
printf ' время до ошибки в none: %s мс\n' "$t_none"
[ "$t_none" -lt 500 ] && ok "ошибка мгновенная — маршрута нет" \
|| bad "ошибка по таймауту (${t_none} мс)"
printf '\n═══ Требование 6: диагностика через общий namespace ═══\n'
docker exec m-bridge sh -c 'command -v ss ip tcpdump' > /dev/null 2>&1 \
&& bad "в образе есть сетевые утилиты — проверка не показательна" \
|| ok "в образе нет ss, ip, tcpdump"
docker run --rm --network container:m-bridge alpine:3.21 sh -c '
apk add --no-cache iproute2 > /dev/null 2>&1
echo " интерфейсы:"; ip -brief addr | sed "s/^/ /"
echo " сокеты:"; ss -tln | tail -n +2 | sed "s/^/ /"
' && ok "диагностика выполнена без изменения образа"
printf '\n═══ ИТОГ ═══\n'
[ "$fail" -eq 0 ] && echo " все требования выполнены" || echo " ЕСТЬ ПРОВАЛЫ"
docker rm -f m-bridge m-host m-hostp > /dev/null 2>&1
docker rmi -f netmodes > /dev/null 2>&1
cd /tmp && rm -rf /tmp/netmodes
exit "$fail"
Ожидаемый вывод:
═══ Требование 1: bridge против host ═══
bridge, порт 18300 (опубликован): 200
bridge, порт 8000 (внутренний): 000
host, порт 18301 (без -p): 200
✓ оба доступны, но разными путями
hostname внутри:
bridge: 8c2f1a9d3e57
host: workstation
═══ Требование 2: -p в режиме host ═══
WARNING: Published ports are discarded when using host network mode
правил DNAT для 18302: 0
docker port: пусто
сервис на 18303: 200
✓ публикация отброшена
═══ Требования 3–4: доступ к сервису host ═══
без --add-host:
host.docker.internal:18304 dns_fail 0.4 мс Name or service not known
✓ имя не разрешается (требование 4)
с --add-host host-gateway:
host.docker.internal:18304 ok 1.2 мс 172.17.0.1
ответ host: host=workstation pid=52341
✓ доступ к сервису host работает (требование 3)
═══ Требование 5: none — мгновенная ошибка ═══
в режиме none:
8.8.8.8:53 OSError 0.3 мс [Errno 101] Network is unreachable
example.com:80 dns_fail 0.2 мс Temporary failure in name resolution
в режиме bridge, недостижимый адрес:
10.255.255.1:80 timeout 5002.1 мс timed out
время до ошибки в none: 0 мс
✓ ошибка мгновенная — маршрута нет
═══ Требование 6: диагностика через общий namespace ═══
✓ в образе нет ss, ip, tcpdump
интерфейсы:
lo UNKNOWN 127.0.0.1/8 ::1/128
eth0@if61 UP 172.17.0.4/16
сокеты:
LISTEN 0 5 0.0.0.0:8000 0.0.0.0:*
✓ диагностика выполнена без изменения образа
═══ ИТОГ ═══
все требования выполнены
Все шесть требований выполнены.
Три решения, определяющие качество.
Требование 5 проверяется сравнением времени, а не самим фактом ошибки. «Соединение не установилось» верно и для none, и для недостижимого адреса в bridge. Различает их время: 0.3 мс против 5002 мс. Первое — Network is unreachable от ядра, которому некуда отправить пакет; второе — истёкший таймаут. Без сравнения проверка не отличала бы отсутствие сети от медленной сети.
Требование 2 проверяется тремя независимыми признаками. Предупреждение Docker — самый очевидный, но его легко пропустить в потоке вывода, и оно не доказывает отсутствия правила. Счётчик правил DNAT и пустой docker port подтверждают то же самое со стороны системы. Три источника, один вывод.
Требование 6 сначала проверяет, что утилит в образе действительно нет. Иначе демонстрация была бы бессмысленной: если бы ss был установлен, приём с общим namespace ничего бы не показал. Проверка предпосылки — часть проверки.
Чего решение не делает. Оно не измеряет выигрыш режима host в производительности. Такое измерение требует нагрузки с высокой частотой пакетов и повторов: на единичных HTTP-запросах разница между NAT и его отсутствием тонет в разбросе. Утверждение «режим host быстрее» в уроке приводится со ссылкой на документацию и с указанием сценариев, где разница проявляется, а не как результат этого замера.
Проверка результата
docker run --rm --network host python:3.13-slim hostname
docker run --rm --network none python:3.13-slim sh -c 'awk "NR>2 {print \$1}" /proc/net/dev'
docker run --rm --add-host host.docker.internal:host-gateway python:3.13-slim \
grep host.docker.internal /etc/hosts
Ожидается имя host, единственный интерфейс lo: и строка с адресом шлюза.
Типичные ошибки
| Ошибка | Причина | Исправление |
|---|---|---|
-p вместе с --network host | Привычка | Игнорируется; порт слушается напрямую |
--network host «для простоты» | Не хотят разбираться с публикацией | Теряется изоляция; видны сервисы host на loopback |
localhost для обращения к host | Опыт работы без container'ов | Это сам container |
host.docker.internal без --add-host | Работало в Docker Desktop | В Linux имени нет по умолчанию |
Захардкоженный 172.17.0.1 | Сработало один раз | В user-defined сети шлюз другой |
Сервис host слушает 127.0.0.1 | Так безопаснее | Из container'а недоступен; слушать 0.0.0.0 |
Установка iproute2 в production-образ | Нужна диагностика | --network container:<имя> с образом-инструментом |
| Overlay на одном host | Прочитали про кластер | Требует Swarm; смысла нет |
| Забыли про MTU в overlay | Мелкие пакеты проходят | Крупные теряются; учесть накладные расходы VXLAN |
| Ожидают IPv6 по умолчанию | Логично предположить | Требует явного включения |
Контрольные вопросы
На понимание:
- Почему
-pигнорируется в режимеhost? - Что означает
localhostв каждом из четырёх режимов? - Почему сервис host на
127.0.0.1доступен container'у в режимеhost? - Почему
host.docker.internalне работает в Linux без настройки? - Чем ошибка в режиме
noneотличается от недоступности адреса вbridge?
На применение:
- Как обратиться из container'а к базе, работающей на host?
- Как выполнить
tcpdumpдля container'а, в образе которого его нет? - Когда режим
hostдействительно оправдан? Назовите два случая.
На диагностику:
- Приложение перенесли с Docker Desktop на Linux — перестало находить
host.docker.internal. Что делать? - Два container'а не запускаются одновременно, оба в режиме
host. Причина?
Краткое резюме
bridge— умолчание: свой namespace, veth pair, NAT, публикация портов.host— namespace host: нет изоляции, нет NAT,-pигнорируется.- В режиме
hostcontainer видит все интерфейсы host и его сервисы на127.0.0.1. none— толькоlo: ошибка приходит мгновенно, маршрута нет вовсе.container:<имя>— общий стек с другим container'ом: те же адреса и тот жеlocalhost.- Общий namespace — штатный способ диагностики без изменения production-образа.
localhostвнутри container'а никогда не означает host, кроме режимаhost.host.docker.internalв Linux требует--add-host host.docker.internal:host-gateway.- Сервис на host должен слушать адрес, доступный из docker-сети, а не только loopback.
- Overlay объединяет container'ы разных машин через VXLAN и требует Swarm.
- Инкапсуляция VXLAN уменьшает MTU — источник сбоев на крупных пакетах.
- IPv6 требует явного включения на уровне демона или сети.
Официальные источники
| Источник | Ссылка | Что подтверждает |
|---|---|---|
| Docker: network drivers | https://docs.docker.com/engine/network/drivers/ | Обзор всех режимов |
| Docker: host network | https://docs.docker.com/engine/network/drivers/host/ | Отсутствие изоляции, игнорирование -p |
| Docker: none network | https://docs.docker.com/engine/network/drivers/none/ | Полное отключение сети |
| Docker: overlay networks | https://docs.docker.com/engine/network/drivers/overlay/ | VXLAN, требования Swarm, порты |
Docker: docker run reference | https://docs.docker.com/reference/cli/docker/container/run/#add-host | --add-host, значение host-gateway |
| Docker: IPv6 | https://docs.docker.com/engine/daemon/ipv6/ | Включение, fixed-cidr-v6 |
Docker: --network container: | https://docs.docker.com/engine/network/#container-networks | Общий namespace |
Навигация
← Предыдущий материал
Вернуться к разделу
Следующий материал → Диагностика
Главное оглавление