Контейнер не видит соседа по имени. Порт опубликовал, а снаружи глухо. Поднял firewalld - и весь проброс портов рассыпался, хотя docker network ls показывает всё на месте. Подсеть Docker внезапно совпала с офисной 172.17.0.0/16, и VPN до прода отвалился у всей команды. Знакомо? Сеть Docker выглядит как магия ровно до того момента, пока что-то не сломается, и тогда внезапно надо знать, что такое veth, DNAT и почему DNS живёт на 127.0.0.11.
Хорошая новость: никакой магии тут нет. Docker network - это тонкая обёртка над тремя примитивами ядра Linux: network namespaces (изолированный сетевой стек на каждый контейнер), veth-пары (виртуальный кабель между namespace и хостом) и iptables/nftables (NAT, фильтрация, проброс портов). Разберёшь эти три кирпича - и вся docker сеть станет прозрачной: ты будешь чинить её через ip, brctl и iptables -t nat, а не методом тыка.

Драйверы сети: bridge, host, none, macvlan/ipvlan, overlay
Драйвер - это способ, которым контейнер подключается к внешнему миру. Их немного, и каждый решает свою задачу.
bridge - дефолт для одного хоста. Контейнер получает приватный IP во внутренней подсети, наружу ходит через NAT. Это то, что включается само, если ты не указал --network. Именно про docker bridge дальше будет самый подробный разбор.
host - контейнер шарит сетевой namespace хоста. Никакой изоляции, никакого NAT: процесс слушает порт прямо на хостовом интерфейсе, как будто запущен без Docker. Максимальная пропускная и нулевой оверхед, но порт 80 в контейнере = порт 80 на хосте, и два таких контейнера подерутся. На Docker Desktop (Mac/Windows) host работает иначе - там контейнеры крутятся в VM, так что "хост" - это linux-VM, а не твоя macOS.
none - есть только loopback, наружу ноль. Для air-gapped задач: посчитать что-то и записать в volume, без единого сетевого пакета.
macvlan/ipvlan - контейнер получает свой собственный MAC и IP прямо в физической сети. Для остальной LAN он выглядит как отдельная железка, а не как процесс за NAT. docker macvlan берут, когда легаси-софт хочет быть полноценным узлом сети (DHCP, обнаружение по броадкасту, прослушка VLAN). ipvlan - брат-близнец, но раздаёт IP без раздачи отдельных MAC (режим L2/L3), что спасает, когда у свитча включён port security или провайдер лимитирует число MAC на порту.
overlay - мультихостовая сеть поверх нескольких машин. docker overlay строит виртуальный L2-сегмент через VXLAN-туннели, так что контейнеры на разных серверах общаются по приватным IP, будто в одном свитче. Это основа сети Docker Swarm и кластеров; вне Swarm overlay тоже можно поднять, но нужен внешний key-value store, и в 2026 так почти никто не делает - либо Swarm, либо уже Kubernetes со своим CNI.
bridge под капотом: docker0, veth-пары и namespace
Разберём дефолтную docker сеть по косточкам. После установки Docker на хосте появляется мост docker0 - это виртуальный коммутатор в ядре. Когда ты запускаешь контейнер, Docker создаёт veth-пару: два виртуальных интерфейса, соединённых как два конца одного патч-корда. Один конец уезжает внутрь network namespace контейнера и там зовётся eth0, второй остаётся на хосте и втыкается в docker0.
Посмотрим на хост. Поднимем контейнер и глянем мост:
Код: Выделить всё
docker run -d --name web nginx:alpine
ip -br link show master docker0
# vethadf3c1a@if12 UP 6a:1f:0c:8e:22:b4 <BROADCAST,MULTICAST,UP,LOWER_UP>
Код: Выделить всё
docker exec web ip -br addr show eth0
# eth0@if13 UP 172.17.0.2/16
Дефолтный мост против user-defined. Запомни главное практическое правило: на стандартном docker0 нет DNS по именам - контейнеры видят друг друга только по IP. Как только ты создаёшь свою сеть через docker network create mynet, включается встроенный DNS и контейнеры резолвят друг друга по имени. Поэтому в реальной эксплуатации почти всегда делают свою bridge-сеть, а не сидят на docker0.
Встроенный DNS на 127.0.0.11 - как контейнеры находят друг друга
Внутри каждого контейнера в user-defined сети Docker поднимает встроенный DNS-сервер на адресе 127.0.0.11. Загляни в resolv.conf:
Код: Выделить всё
docker network create app-net
docker run -d --name db --network app-net postgres:16-alpine
docker run --rm --network app-net alpine cat /etc/resolv.conf
# nameserver 127.0.0.11
# options ndots:0
Грабля номер один тут: 127.0.0.11 работает только на user-defined сетях. На голом docker0 этого DNS нет, и --link - это древний костыль, забудь про него. Вторая грабля - ndots:0 в примере выше (поведение зависит от версии): неаккуратные ndots в больших инсталляциях порождают лавину лишних DNS-запросов, потому что резолвер дописывает search-домены к каждому короткому имени.
Публикация портов: docker сеть, iptables, DNAT и docker-proxy
Теперь самое тёмное место - почему -p 8080:80 пускает мир в контейнер. Контейнер сидит в приватной подсети, снаружи его IP не маршрутизируется. Чтобы внешний запрос на хостовый 8080 дошёл до контейнерного 80, Docker вставляет правило DNAT (destination NAT) в таблицу nat. docker iptables - это не пугающая абстракция, это набор цепочек, которые ты можешь прочитать глазами:
Код: Выделить всё
docker run -d -p 8080:80 --name pub nginx:alpine
iptables -t nat -L DOCKER -n
# Chain DOCKER (2 references)
# target prot opt source destination
# RETURN all -- 0.0.0.0/0 0.0.0.0/0
# DNAT tcp -- 0.0.0.0/0 0.0.0.0/0 tcp dpt:8080 to:172.17.0.2:80
А что за docker-proxy? Рядом с DNAT Docker запускает пользовательский процесс docker-proxy на каждый опубликованный порт. Многие думают, что трафик идёт через него - это миф. В нормальном случае работает DNAT в ядре, а docker-proxy нужен для краевых случаев: обращения с самого хоста на localhost:8080 (где PREROUTING не срабатывает в части конфигураций) и хост-нетворкинга на старых ядрах. Увидеть его легко:
Код: Выделить всё
ps aux | grep docker-proxy
# /usr/bin/docker-proxy -proto tcp -host-ip 0.0.0.0 -host-port 8080 -container-ip 172.17.0.2 -container-port 80
Изоляция, --internal сети, MTU и доступ в host-сеть
Изоляция. Контейнеры в разных user-defined сетях по умолчанию не видят друг друга - между мостами стоит DROP в цепочке DOCKER-ISOLATION. Контейнеры в одной сети видят друг друга по всем портам напрямую, без всякого -p (публикация нужна только для доступа снаружи хоста). Это частая путаница новичков: чтобы web ходил в db, порт публиковать НЕ надо, достаточно общей сети.
--internal. Если создать сеть с флагом --internal, Docker не добавляет маршрут наружу и MASQUERADE - контейнеры в ней общаются между собой, но в интернет не выйдут. Идеально для бэкенд-сегмента (база, кеш), который не должен иметь исходящего доступа:
Код: Выделить всё
docker network create --internal backend
Доступ из контейнера в host-сеть. Иногда контейнеру надо достучаться до сервиса, который крутится на самом хосте (БД на хосте, dev-сервер). Внутри контейнера используй специальное имя host.docker.internal - на Docker Desktop оно резолвится автоматически, на Linux его надо явно прокинуть: --add-host host.docker.internal:host-gateway. Это безопаснее, чем сажать контейнер в --network host.
Типичные грабли и антипаттерны
- Конфликт подсетей. Docker по умолчанию выедает 172.17.0.0/16 и дальше из пула 172.16-172.31. Если у тебя VPN или офисная сеть в этом диапазоне - маршруты дерутся, и пропадает связь. Лечение: задать свои диапазоны в daemon.json через default-address-pools, а не ловить совпадения по факту.
- firewalld и nftables против Docker. Docker сам пишет свои правила в iptables/nftables. Перезапуск firewalld или ручной iptables -F вычищает докеровские цепочки, и проброс портов умирает до docker restart или docker network reconnect. Не трогай руками цепочки DOCKER, DOCKER-USER, DOCKER-ISOLATION - для своих правил есть специально оставленная цепочка DOCKER-USER, которая выполняется ПЕРЕД докеровскими. Туда и вешай ограничения доступа.
- -p 8080:80 публикует на 0.0.0.0. То есть порт виден всему миру, а не только локально. На сервере с белым IP это дыра. Привязывай явно: -p 127.0.0.1:8080:80, если сервис только для локального reverse-proxy.
- macvlan: хост не видит свои контейнеры. Фундаментальное ограничение драйвера - сам хост НЕ может общаться со своими macvlan-контейнерами (трафик уходит на физику и не возвращается). Это не баг, это дизайн. Нужна связь хост-контейнер - городи дополнительный macvlan-интерфейс на хосте или используй другой драйвер.
- Сидеть на docker0 в проде. Нет DNS по именам, нет нормальной изоляции. Всегда создавай user-defined сети под каждый стек (Compose делает это сам).
Минимизируй атакуемую поверхность. Не публикуй порты, которые нужны только внутри стека - общая сеть и так даёт связь между контейнерами. Бэкенд-сегменты делай --internal, чтобы база не имела исходящего доступа в интернет (это режет утечки при компрометации). Вешай ограничения в DOCKER-USER, а не поверх докеровских цепочек. Для публикации наружу биндись на конкретный интерфейс, а не на 0.0.0.0. И помни: --network host снимает сетевую изоляцию полностью - применяй точечно и осознанно, а не "чтобы заработало".
Мини-лаба: собери и препарируй сеть руками
- Создай свою сеть с фиксированной подсетью: docker network create --subnet 10.55.0.0/24 lab. Подними два контейнера alpine в ней (--network lab, с именами a и b).
- Из контейнера a пингани b по имени: docker exec a ping -c2 b. Убедись, что встроенный DNS на 127.0.0.11 отдал IP (загляни в /etc/resolv.conf внутри).
- Опубликуй порт у nginx (-p 8080:80 --network lab) и найди DNAT-правило: iptables -t nat -L DOCKER -n. Разбери поля dpt и to: - какой порт куда переписывается.
- Найди veth-пару: на хосте ip -br link show master br-... (имя моста для своей сети возьми из ip link), внутри контейнера docker exec a ip -br addr show eth0. Сопоставь индексы @ifN.
- Сделай вторую сеть и контейнер в ней, проверь, что он НЕ пингует контейнеры из lab (изоляция мостов). Потом подключи его второй сетью: docker network connect lab othercontainer - и убедись, что связь появилась.
- Чем отличается поведение DNS на дефолтном docker0 и на user-defined bridge, и какой адрес отвечает за резолв имён внутри контейнера?
- Что происходит с пакетом при -p 8080:80: какая таблица и какое правило его переписывают, и зачем рядом живёт docker-proxy?
- Почему overlay требует уменьшенного MTU и как проявляется проблема, если этого не сделать?
- Куда вешать свои firewall-правила, чтобы их не стёр Docker, и почему нельзя править цепочку DOCKER напрямую?
Сеть Docker - это namespace плюс veth плюс iptables, и больше ничего волшебного. bridge для одного хоста, host для скорости без изоляции, macvlan/ipvlan чтобы стать узлом физической сети, overlay для мультихоста. Имена резолвит встроенный DNS на 127.0.0.11, но только в своих сетях. Порты пробрасывает DNAT в таблице nat. Знаешь эти механизмы - и docker network перестаёт быть чёрным ящиком: ты читаешь её через ip, iptables -t nat и docker network inspect, а не гадаешь.