open atlas
↑ К треку
Деплой и инфра DEP · 09 · 01

Сеть контейнеров: bridge, DNS по имени и ловушка localhost

Контейнеры общаются через сетевые драйверы — по умолчанию bridge. Пользовательский bridge добавляет DNS по имени контейнера; дефолтный — нет. Публикуй порты только для доступа снаружи и не верь localhost внутри контейнера.

DEP Middle ◷ 16 min
Уровень
ОсновыJuniorMiddleSenior

Два контейнера, api и db, оба запущены, оба здоровы. На каждый запрос api пишет в лог ECONNREFUSED 127.0.0.1:5432. Разработчик клянётся, что база поднята — docker ps это подтверждает — и сжигает полдня, проверяя конфиг Postgres, файрволы и креды. Баг не в этом. В строке подключения стоит localhost, а внутри контейнера api localhost означает сам контейнер api, где на 5432 никто не слушает. База — в одном сетевом хопе, доступна по имени db — но только если оба контейнера в правильном типе network.

За десять минут ты поймёшь, почему localhost врёт внутри контейнера, какой драйвер выбрать и как связать сервисы так, чтобы они не теряли друг друга после рестарта.

Драйверы: как контейнер вообще получает сеть

Контейнер стартует в своём network namespace — со своими интерфейсами, таблицей маршрутизации и localhost. Сетевой драйвер решает, как этот namespace соединяется со всем остальным. Драйвер ты выбираешь, создавая network или запуская контейнер.

Драйвер bridge — это умолчание. Docker создаёт на хосте приватный виртуальный коммутатор (docker0 для дефолтной сети) и подключает к нему каждый контейнер виртуальной ethernet-парой. Контейнеры получают внутренние IP в приватной подсети (часто 172.17.0.0/16), общаются друг с другом через этот bridge и достигают внешнего мира через NAT — хост маскирует их трафик за своим IP. Снаружи хоста эти IP контейнеров невидимы, пока ты не опубликуешь порт.

Драйвер host убирает изоляцию полностью: контейнер делит network namespace хоста. Нет отдельного IP контейнера, нет NAT и нет проброса портов — если приложение биндится на :8080, оно на :8080 хоста напрямую. Это самый быстрый вариант (нет bridge, нет хопа через NAT) и причина использовать его для чувствительных к задержке или высоконагруженных ворклоадов, но ты теряешь изоляцию и не можешь запустить два контейнера, которым нужен один порт.

Драйвер none даёт контейнеру namespace только с loopback — вообще без внешней связности, для полностью изолированной работы. overlay охватывает несколько хостов (сервисы Swarm, мультихостовые кластеры), туннелируя трафик контейнеров между демонами. macvlan выдаёт контейнеру собственный MAC-адрес, так что он выглядит физическим устройством в LAN, — для легаси-систем, которые этого ждут. Повседневная рабочая лошадка — bridge; остальное ситуативно.

docker run -d --name web nginx                 # дефолтный bridge
docker run -d --network host nginx             # делит стек хоста, -p не нужен
docker run -d --network none alpine sleep 1d   # только loopback, изолирован
docker network ls                              # bridge, host, none + созданные тобой

Дефолтный bridge против пользовательского bridge

Вот ключевой сеньорский момент, на котором держится хук. Существуют два вида bridge-сети, и они не равнозначны.

Дефолтный bridge (тот, что зовётся bridge, к которому подключается каждый контейнер, если не сказано иное) не имеет встроенного DNS. Контейнеры на нём достают друг друга только по сырому IP-адресу — что хрупко, потому что IP назначаются динамически и меняются между перезапусками. Старым обходом был устаревший флаг --link.

Пользовательский bridge — любая сеть, которую ты создаёшь сам — добавляет встроенный DNS-сервер. Контейнеры на нём резолвят друг друга по имени контейнера автоматически. Запусти db и api в одной пользовательской network — и api сможет подключиться к db:5432 без IP, без link, без конфига. Это service discovery, и это единственная причина всегда помещать связанные контейнеры в созданную тобой network, а не в дефолтный bridge.

docker network create appnet                              # пользовательский bridge
docker run -d --name db  --network appnet postgres:16
docker run -d --name api --network appnet my/api          # api достаёт db по имени "db"

Именно это делает за тебя docker compose: каждый проект Compose получает собственный пользовательский bridge, так что сервисы достают друг друга по имени сервиса из коробки. Руками network ты создаёшь редко, потому что Compose уже это сделал.

Почему это работает

На пользовательском bridge резолвинг имён динамичен: если db перезапустится и получит новый IP, встроенный DNS вернёт новый адрес при следующем запросе, так что api продолжит работать без перезапуска. У дефолтного bridge такого DNS нет — поэтому захардкоженный там IP контейнера ломается на следующем перезапуске. DNS Docker’а ещё и форвардит неизвестные ему имена резолверу хоста, так что внешние хостнеймы из контейнера резолвятся нормально.

Публикация портов против общения внутри сети

Два контейнера в одной пользовательской network общаются напрямую на собственном порту контейнераapi бьёт в db:5432 независимо от того, опубликован ли 5432. Публикация — только про доступ к контейнеру снаружи Docker-сети, обычно с хоста или из интернета.

-p host:container (--publish) маппит порт хоста на порт контейнера через NAT bridge’а. -p 8080:80 значит «отправь хостовый :8080 в этот контейнерный :80». Ты публикуешь сервис, обращённый наружу (веб-сервер, API-шлюз), а внутренние сервисы вроде базы оставляешь без публикации — им не нужен хостовый порт, потому что их клиенты в той же network.

Частая путаница: EXPOSE в Dockerfile ничего не публикует. Это документация — метаданные, объявляющие, какой порт слушает приложение. Он не открывает ни одного хостового порта и не меняет файрвол. Только -p (или ports: в Compose) реально публикует; EXPOSE лишь сообщает читателям и инструментам намерение.

docker run -d --network appnet -p 8080:80 --name web my/web   # host:8080 → container:80
# у db НЕТ -p: с хоста недоступен, но web достаёт его на db:5432 внутри
ДрайверИзоляцияDNS по имениПроброс портовДля чего
bridge (пользовательский)Свой namespace + IPДа (по имени контейнера)-p для доступа снаружиУмолчание для многоконтейнерных приложений
bridge (дефолтный)Свой namespace + IPНет (только IP)-p для доступа снаружиБыстрые разовые запуски; не для связки приложения
hostНет (делит стек хоста)Н/Д (резолвер хоста)Нет {без -p}Минимум задержки, один владелец порта
noneПолная (только loopback)НетНет связностиПолностью изолированные задачи
overlayСвой namespace + IPДа (сервисы Swarm)Публикуется на сервисМультихостовые / Swarm-кластеры

Ловушка localhost

Теперь хук разрешается. Внутри контейнера localhost (127.0.0.1) — это собственный loopback контейнера, не хост и не какой-либо другой контейнер. У каждого контейнера свой loopback-интерфейс в своём namespace. Поэтому api, подключаясь к localhost:5432, спрашивает Postgres у самого себя, ничего слушающего не находит и получает ECONNREFUSED.

Фиксы следуют из всего сказанного выше:

  • Чтобы достать другой контейнер, используй его имя в общей пользовательской network: db:5432. Никогда localhost, никогда захардкоженный IP.
  • Чтобы достать сервис на самой хост-машине изнутри контейнера (например, базу, запущенную нативно на твоём ноутбуке), используй host.docker.internal — специальное DNS-имя, которое Docker резолвит в IP хост-машины. (host-сеть — другой путь: тогда localhost и есть хост, потому что отдельного namespace нет.) (host-сеть — другой путь: тогда localhost и есть хост, потому что отдельного namespace нет.)
  • Чтобы достать контейнер с хоста, опубликуй порт через -p и подключайся к localhost:<hostPort> на хосте.

Вместе эти три правила означают одно: localhost — только «этот же контейнер», и без явного имени первый же рестарт в проде тихо ломает связку. В тот миг, когда ты пересекаешь границу контейнера, тебе нужно имя — db, api или host.docker.internal.

Викторина

Контейнер api подключается к localhost:5432, чтобы достать контейнер db, оба в одной пользовательской network, и получает ECONNREFUSED. Что не так?

Викторина

В Dockerfile есть EXPOSE 80, но контейнер запущен без флага -p. Достанешь ли ты приложение из браузера на хосте?

Выбери лучший вариант

Многоконтейнерное приложение (web + api + db) работает на одном хосте, и web-уровень должен быть доступен из интернета. Выбери сетевую схему.

Вспомните перед уходом
  1. 01
    Почему пользовательский bridge даёт контейнерам доставать друг друга по имени, а дефолтный — нет, и что это меняет на практике?
  2. 02
    Что означает localhost внутри контейнера и как достать (а) другой контейнер, (б) сервис на хост-машине и (в) контейнер с хоста?
Итог

Контейнер стартует в своём network namespace, и сетевой драйвер решает, как он соединяется. Bridge — умолчание: приватный виртуальный коммутатор на хосте даёт каждому контейнеру внутренний IP и NAT-ит его исходящий трафик, тогда как host-сеть делит стек хоста без изоляции и без проброса портов, none даёт только loopback, а overlay и macvlan покрывают мультихостовые случаи и присутствие в LAN. Сеньорское различие — между дефолтным bridge, у которого нет DNS и который вынуждает к хрупкой адресации по IP, и пользовательским bridge, чей встроенный DNS позволяет контейнерам резолвить друг друга по имени контейнера — автоматический service discovery, и причина, по которой docker compose создаёт network на проект, чтобы сервисы доставали друг друга по имени. Публикация через -p маппит порт хоста на порт контейнера и нужна только для доступа снаружи Docker-сети; контейнеры в одной пользовательской network уже общаются на порту контейнера напрямую, а EXPOSE — лишь документация, не публикующая ничего. Наконец, localhost внутри контейнера — это собственный loopback этого контейнера, поэтому достань соседа по имени (db:5432), хост через host.docker.internal, а контейнер с хоста — через опубликованный порт; никогда не считай, что localhost пересекает границу контейнера. Теперь, когда видишь ECONNREFUSED на 127.0.0.1, первый вопрос — в каком network namespace на самом деле работает приложение?

Практика

Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.

вспомнитьприменитьуглубить0 из 5 завершено
Связанные уроки

Что-то непонятно?

Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.

хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.