Learning Platform
Глоссарий Troubleshooting
Урок 11.03 · 22 мин
Средний
dockernetworkingdnsservice-discoverycompose

DNS и service discovery в user-defined bridge

Когда ты пишешь в compose host: postgres для подключения к БД — это работает не магией, а через встроенный DNS-сервер Docker. На user-defined bridge каждому контейнеру доступен резолвер по адресу 127.0.0.11, который отвечает на запросы по container name, по compose service name и по сетевому alias. На default bridge этого нет — там только IP. В этом уроке разбираем механику.


DNS-резолюция — путь от example.com до IP

Default bridge vs user-defined: главное отличие

Запустим два контейнера на default bridge:

docker run -d --name a alpine sleep 1000
docker run -d --name b alpine sleep 1000

docker exec a ping -c 2 b
# ping: bad address 'b'

DNS-резолв по имени не работает. На default bridge нет embedded DNS. Это сохранено для совместимости с очень старыми скриптами, написанными до того, как Docker добавил DNS-сервер.

Теперь на user-defined bridge:

docker network create mynet
docker run -d --name a --network mynet alpine sleep 1000
docker run -d --name b --network mynet alpine sleep 1000

docker exec a ping -c 2 b
# PING b (172.18.0.3): 56 data bytes
# 64 bytes from 172.18.0.3: seq=0 ttl=64 time=0.07 ms

Резолв работает. Внутри контейнера в /etc/resolv.conf:

docker exec a cat /etc/resolv.conf
# nameserver 127.0.0.11
# search ...

127.0.0.11 — embedded DNS resolver Docker’а. Он слушает только внутри netns контейнера, наружу не виден.


Embedded DNS: что он умеет

Docker daemon держит карту network -> {container_name, aliases} -> IP. Когда контейнер делает DNS-запрос:

  1. Запрос на 127.0.0.11 (через резолвер контейнера).
  2. Docker daemon проверяет: знает ли он это имя в той же сети?
  3. Если знает — возвращает IP контейнера.
  4. Если нет — форвардит запрос наверх (как правило, к DNS хоста, например 8.8.8.8 или корпоративному DNS).
DNS-резолв в user-defined bridge
Container A: getent hosts bИз контейнера A запущена команда ping b или DNS-резолв через library glibc
udp/53
127.0.0.11 (embedded resolver)Внутри netns контейнера live UDP-listener на 127.0.0.11:53. На самом деле это userspace-resolver Docker daemon'а, доступный через netns-magic
Сеть mynet: lookup bResolver проверяет, есть ли в сети mynet контейнер с именем b. Также проверяет aliases (--network-alias) и compose service names
Найдено: 172.18.0.3Resolver отдаёт IP контейнера b. Если бы имени не было в сети — форвард к upstream DNS (host /etc/resolv.conf)
App шлёт пакет на 172.18.0.3Дальше работает обычный bridge: пакет через veth, docker0, парный veth, eth0 контейнера b
Контейнер b отвечаетb обрабатывает запрос. Поскольку оба в одной сети, NAT не применяется, пакеты идут напрямую через docker bridge

Что резолвится

Embedded DNS отвечает на несколько вариантов имени:

  1. Container name. Имя из --name. Если контейнер --name db, то db резолвится.
  2. Container ID short. Первые 12 символов ID тоже работают как имя.
  3. Network alias. Заданный через --network-alias=foo (или в compose aliases: [foo]).
  4. Compose service name. В compose-стеке имя сервиса в YAML — это alias на сети.
docker network create mynet
docker run -d --name db --network mynet --network-alias primary-db --network-alias master alpine sleep 1000

docker run --rm --network mynet alpine sh -c 'getent hosts db; getent hosts primary-db; getent hosts master'
# 172.18.0.2 db
# 172.18.0.2 primary-db
# 172.18.0.2 master

Один контейнер — несколько имён. Полезно, если в коде приложения захардкожен один host, а ты хочешь подменить, на какой контейнер он попадает.


Compose: автоматический setup

В compose YAML каждый сервис — это alias на default network проекта. Минимальный пример:

services:
  postgres:
    image: postgres:17
    environment:
      POSTGRES_PASSWORD: secret

  app:
    image: python:3.13-slim
    command: python -c "import socket; print(socket.gethostbyname('postgres'))"
    depends_on: [postgres]

docker compose up создаёт сеть <project>_default, оба сервиса подключаются туда. В app команда socket.gethostbyname('postgres') вернёт IP контейнера postgres. Никаких -p или DNS-конфигов руками не пишем.

docker compose up -d
docker compose logs app
# app-1  |  172.18.0.2

postgres — это имя сервиса. На сети-default оно резолвится в IP. Если ты переименуешь сервис в db — в коде надо тоже db. Поэтому имена сервисов — это контракт между сервисами.


Multi-network: контейнер в нескольких сетях

Контейнер может быть подключен к нескольким сетям одновременно. Полезный паттерн — frontend/backend сегрегация:

services:
  proxy:
    image: nginx
    networks: [frontend, backend]

  app:
    image: myapp
    networks: [backend]

  postgres:
    image: postgres:17
    networks: [backend]

networks:
  frontend:
  backend:
  • proxy видит app и postgres через backend, плюс наружу через frontend.
  • app и postgres друг друга видят через backend.
  • app не достучится во frontend-network — изоляция.

Embedded DNS работает per-network: app может резолвить postgres (общая backend), но не резолвит контейнеры из frontend, к которому не подключен.


Кастомные DNS-настройки

docker run --rm \
  --dns 8.8.8.8 \
  --dns-search example.com \
  alpine sh -c 'cat /etc/resolv.conf'
# search example.com
# nameserver 8.8.8.8

Полезно, если корпоративный DNS перехватывает запросы и ты хочешь обойти. В compose:

services:
  app:
    image: myapp
    dns:
      - 8.8.8.8
      - 1.1.1.1
    dns_search:
      - corp.example.com

Также --add-host name:IP — добавить строку в /etc/hosts контейнера:

docker run --rm --add-host host.docker.internal:host-gateway alpine \
  getent hosts host.docker.internal
# 172.17.0.1 host.docker.internal

host.docker.internal — специальное имя для доступа к хосту изнутри контейнера. На macOS и Windows работает из коробки, на Linux — нужно явно --add-host=host.docker.internal:host-gateway.


TTL и кэш

Внутри контейнера glibc-resolver кэширует DNS-ответы. Если контейнер db пересоздался с новым IP, а приложение уже сделало lookup — оно будет долбиться на старый IP. Поэтому:

  • Если возможно, не пересоздавай контейнеры в long-running компонентах compose-стенда без перезапуска dependent-сервисов.
  • В production-сценариях с динамическими endpoint’ами обычно перед DNS стоит service mesh с health-checking.
WARNING

Embedded DNS Docker’а отдаёт TTL = 600 сек. Если контейнер db упал и заменился (другой IP) — приложение, которое заresolvило db минуту назад, ещё 9 минут может ходить на мёртвый IP. На уровне Junior DE достаточно знать о существовании этой проблемы: при странных «иногда не подключается» дебажить через getent hosts db внутри приложения.


Попробуй сам

# 1. User-defined bridge + DNS.
docker network create demo
docker run -d --name redis --network demo redis:7-alpine
docker run -d --name app --network demo \
  --network-alias api alpine sleep 1000

docker exec app ping -c 2 redis
# работает

# Alias тоже резолвится:
docker exec redis ping -c 2 api
# работает

# 2. Контейнер вне сети — не видит.
docker run --rm alpine ping -c 2 redis
# ping: bad address — потому что не в demo network.

# 3. Подключим существующий контейнер к demo on-the-fly.
docker run -d --name observer alpine sleep 1000
docker exec observer ping -c 1 redis
# fail
docker network connect demo observer
docker exec observer ping -c 1 redis
# success!

# Цена: контейнер теперь в двух сетях, у него два eth.
docker exec observer ip addr | grep eth
# eth0 — изначальный bridge
# eth1 — demo

# 4. host.docker.internal на Linux.
docker run --rm --add-host host.docker.internal:host-gateway \
  alpine sh -c 'getent hosts host.docker.internal; echo "---"; nc -zv host.docker.internal 22'
# Видишь IP хоста + sshd если он есть на 22.

# Cleanup.
docker rm -f redis app observer
docker network rm demo
TIP

Compose: чтобы дать сервису кастомное имя, помимо service name можно использовать container_name: pg — тогда вне compose видно как pg. Но это убивает scale (--scale postgres=2 не сработает, потому что container_name уникален). В compose-сценариях лучше не задавать container_name без необходимости.

В следующем уроке — troubleshooting сетей: как читать docker network inspect, что значит localhost внутри контейнера, и почему Postgres из compose доступен по postgres:5432, а не localhost:5432.


Проверка знанийKnowledge check
Compose-стек содержит сервисы api и db . В коде api идёт подключение по адресу db:5432 — работает. Объясни, какие два механизма Docker'а делают это возможным, и что произойдёт если переподключить api к ещё одной отдельной сети где нет db .
ОтветAnswer
Два механизма: (1) Compose автоматически создаёт сеть <project>_default (user-defined bridge) и подключает все сервисы туда; (2) на user-defined bridge у контейнеров доступен embedded DNS-resolver Docker (127.0.0.11), который знает mapping service-name -> container-IP в рамках своей сети. Имя сервиса из docker-compose.yml становится network-alias. Лукап "db" внутри api идёт на 127.0.0.11, тот возвращает IP контейнера db, дальше пакет идёт через bridge напрямую. Если api подключить ко второй отдельной сети (которой не подключен db) — api будет в двух сетях одновременно, у него появится второй eth-интерфейс. Резолв "db" продолжит работать через default-сеть, потому что embedded DNS отвечает per-network, и в нужной сети db есть. Не сработает только если db убрать из общей сети — тогда лукап db в default network не найдёт его, форварднется upstream, и упадёт.

Проверьте понимание

Результат: 0 из 0
Концептуальный
Вопрос 1 из 4. Как embedded DNS Docker позволяет в compose резолвить service name в IP?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 4