Главная/Docker Networking/Урок

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
sidecarContainer, разделяющий namespace с основным
fixed-cidr-v6Подсеть IPv6 для сети по умолчанию

Теория

Сводка режимов

bridgehostnonecontainer:<имя>
Свой network namespaceДаНетДаНет, общий с другим
Интерфейсыlo, eth0Все интерфейсы hostТолько loИнтерфейсы соседа
localhost означаетСам containerHostСам containerОбщий namespace
NATДаНетКак у соседа
-p работаетДаИгнорируетсяИгнорируетсяЗадаётся у соседа
Изоляция портовПолнаяОтсутствуетПолнаяОбщая с соседом
DNS DockerДа, в user-definedРезолвер hostКак у соседа
Накладные расходыМинимальныеНулевыеНулевые

host: что даёт и что отнимает

bash
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: сеть отсутствует

bash
docker run --network none myimage

Namespace создан, но в нём только lo. Ни одного маршрута, ни одного внешнего интерфейса.

Применение: вычислительные задачи без сети, обработка недоверенных данных, запуск кода, который не должен иметь возможности обратиться наружу.

Это самый простой и надёжный способ гарантировать отсутствие сетевого доступа — в отличие от правил фильтрации, которые можно ошибиться настроить.

container:<имя>: общий namespace

bash
docker run --network container:app nicolaka/netshoot ss -tlnp

Второй container не получает своего сетевого стека — он использует стек первого. Они видят одни и те же интерфейсы, один и тот же localhost, одни и те же порты.

Применения:

СценарийПояснение
ДиагностикаЗапустить образ с сетевыми утилитами в namespace нужного container'а
SidecarПрокси или сборщик метрик рядом с приложением
Модель pod в KubernetesContainer'ы одного 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.

Правильный способ:

bash
docker run --add-host host.docker.internal:host-gateway myimage

Значение host-gateway — специальное: Docker разворачивает его в адрес шлюза для этой сети. Настраивается на уровне демона параметром host-gateway-ip.

В Compose:

yaml
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'ы на разных машинах в одну логическую сеть.

text
    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 требует явных действий:

json
{
  "ipv6": true,
  "fixed-cidr-v6": "fd00:dead:beef::/48",
  "experimental": false
}

Либо на уровне отдельной сети:

bash
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

bash
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)"

Ожидаемый вывод:

text
═══ 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 игнорируется

bash
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

Ожидаемый вывод:

text
═══ попытка опубликовать порт в режиме 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

bash
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

Ожидаемый вывод:

text
═══ поднимаем сервис на 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: правильный способ

bash
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

Ожидаемый вывод:

text
═══ сервис на 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 перестанет работать.

Проверка второго условия

bash
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

Ожидаемый вывод:

text
═══ сервис на 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 в вашей системе привязался шире, чем ожидалось, либо действует перенаправление.

Проверьте у себя:

bash
sudo ss -tlnp | grep 18203

Правило остаётся: сервис на host должен слушать адрес, доступный из docker-сети, — 0.0.0.0 или конкретно адрес docker0.

Общий namespace: диагностика без установки утилит

bash
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

Ожидаемый вывод:

text
═══ в образе 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 и её гарантии

bash
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'))
"

Ожидаемый вывод:

text
═══ попытки выйти наружу из --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: что нужно для запуска

bash
echo "═══ попытка создать overlay без Swarm ═══"
docker network create --driver overlay failnet 2>&1 | tail -2 | sed 's/^/  /'

echo "═══ состояние Swarm ═══"
docker info --format '  Swarm: {{.Swarm.LocalNodeState}}'

Ожидаемый вывод:

text
═══ попытка создать 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.

Требования:

  1. Одно и то же приложение запущено в режимах bridge и host; показано, чем отличается доступ к нему.
  2. Показано, что -p в режиме host не создаёт правила DNAT.
  3. Container в режиме bridge обращается к сервису на host через host.docker.internal.
  4. Показано, что без --add-host это имя не разрешается.
  5. Container в режиме none не может обратиться никуда; ошибка приходит мгновенно, а не по таймауту.
  6. Диагностика container'а выполнена через --network container:<имя> без установки утилит в его образ.

Подсказки

Подсказка 1

Требование 5: сравните время до ошибки для none и для недостижимого адреса в bridge.

Подсказка 2

Требование 3 требует, чтобы сервис на host слушал не только loopback.

Подсказка 3

Для требования 6 подойдёт alpine с установкой утилит внутри одноразового container'а.

Решение

Показать решение
bash
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"

Ожидаемый вывод:

text
═══ Требование 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 быстрее» в уроке приводится со ссылкой на документацию и с указанием сценариев, где разница проявляется, а не как результат этого замера.

Проверка результата

bash
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 по умолчаниюЛогично предположитьТребует явного включения

Контрольные вопросы

На понимание:

  1. Почему -p игнорируется в режиме host?
  2. Что означает localhost в каждом из четырёх режимов?
  3. Почему сервис host на 127.0.0.1 доступен container'у в режиме host?
  4. Почему host.docker.internal не работает в Linux без настройки?
  5. Чем ошибка в режиме none отличается от недоступности адреса в bridge?

На применение:

  1. Как обратиться из container'а к базе, работающей на host?
  2. Как выполнить tcpdump для container'а, в образе которого его нет?
  3. Когда режим host действительно оправдан? Назовите два случая.

На диагностику:

  1. Приложение перенесли с Docker Desktop на Linux — перестало находить host.docker.internal. Что делать?
  2. Два container'а не запускаются одновременно, оба в режиме host. Причина?

Краткое резюме

  1. bridge — умолчание: свой namespace, veth pair, NAT, публикация портов.
  2. host — namespace host: нет изоляции, нет NAT, -p игнорируется.
  3. В режиме host container видит все интерфейсы host и его сервисы на 127.0.0.1.
  4. none — только lo: ошибка приходит мгновенно, маршрута нет вовсе.
  5. container:<имя> — общий стек с другим container'ом: те же адреса и тот же localhost.
  6. Общий namespace — штатный способ диагностики без изменения production-образа.
  7. localhost внутри container'а никогда не означает host, кроме режима host.
  8. host.docker.internal в Linux требует --add-host host.docker.internal:host-gateway.
  9. Сервис на host должен слушать адрес, доступный из docker-сети, а не только loopback.
  10. Overlay объединяет container'ы разных машин через VXLAN и требует Swarm.
  11. Инкапсуляция VXLAN уменьшает MTU — источник сбоев на крупных пакетах.
  12. IPv6 требует явного включения на уровне демона или сети.

Официальные источники

ИсточникСсылкаЧто подтверждает
Docker: network drivershttps://docs.docker.com/engine/network/drivers/Обзор всех режимов
Docker: host networkhttps://docs.docker.com/engine/network/drivers/host/Отсутствие изоляции, игнорирование -p
Docker: none networkhttps://docs.docker.com/engine/network/drivers/none/Полное отключение сети
Docker: overlay networkshttps://docs.docker.com/engine/network/drivers/overlay/VXLAN, требования Swarm, порты
Docker: docker run referencehttps://docs.docker.com/reference/cli/docker/container/run/#add-host--add-host, значение host-gateway
Docker: IPv6https://docs.docker.com/engine/daemon/ipv6/Включение, fixed-cidr-v6
Docker: --network container:https://docs.docker.com/engine/network/#container-networksОбщий namespace

Навигация

← Предыдущий материал
Вернуться к разделу
Следующий материал → Диагностика
Главное оглавление

Markdown на GitHub ↗