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

8.4. Docker DNS

Цели

После этого материала вы сможете:

  • объяснить, что такое 127.0.0.11 и почему этот адрес виден только внутри container'а;
  • назвать все имена, по которым доступен container, и порядок их разрешения;
  • использовать network aliases, в том числе один псевдоним на несколько container'ов;
  • диагностировать сбой разрешения имени без установки dig и nslookup;
  • объяснить, почему приложение продолжает обращаться к старому адресу после перезапуска соседа;
  • настроить внешние DNS-серверы и поисковые домены.

Предварительные знания

Ключевые термины

ТерминОбъяснение
embedded DNSРезолвер Docker, доступный container'у по адресу 127.0.0.11
network aliasДополнительное имя container'а в конкретной сети
search domainСуффикс, дописываемый к неполным именам
ndotsПорог: сколько точек должно быть в имени, чтобы считать его полным
round-robinВозврат нескольких адресов на одно имя
NXDOMAINОтвет DNS: имени не существует

Теория

Что такое 127.0.0.11

При подключении container'а к user-defined сети Docker записывает в его /etc/resolv.conf единственный сервер имён — 127.0.0.11.

Адрес находится в диапазоне loopback, то есть принадлежит самому container'у. Никакого процесса на нём внутри container'а нет: правило внутри network namespace перенаправляет запросы к резолверу, который держит демон Docker.

СвойствоЗначение
Виден с hostНет
Виден из другого container'аНет, у каждого свой
Требует процесса внутри container'аНет
Работает для образов без сетевых утилитДа, через обычный getaddrinfo

Резолвер отвечает на запросы об именах container'ов тех сетей, в которых состоит спрашивающий. Всё остальное пересылается вышестоящим серверам — тем, что настроены на host.

По каким именам доступен container

ИмяОткуда берётсяОбласть действия
Имя container'а--nameВсе user-defined сети container'а
Network alias--network-aliasКонкретная сеть
Имя сервисаComposeСеть проекта
Имя сервиса с номеромCompose при масштабированииСеть проекта
Hostname--hostnameНе разрешается другими

Последняя строка часто удивляет: --hostname меняет то, что возвращает hostname внутри container'а, но не добавляет DNS-записи. Для имён используют --name и --network-alias.

Network aliases

bash
docker run -d --network appnet --name pg-primary --network-alias db postgres:17-alpine

Теперь container отвечает и на pg-primary, и на db. Практическая ценность — развязка имени в конфигурации от имени container'а: приложение обращается к db, а за этим именем может стоять pg-primary, pg-17, pg-replica-1.

Особый случай — один псевдоним у нескольких container'ов:

bash
docker run -d --network appnet --network-alias api --name api-1 myapp
docker run -d --network appnet --network-alias api --name api-2 myapp

DNS вернёт оба адреса. Клиент обычно берёт первый, а порядок Docker меняет между запросами — получается примитивная балансировка.

Ограничения, которые нужно знать до применения:

ОграничениеСледствие
Нет проверки живостиАдрес упавшего container'а продолжит возвращаться, пока container существует
Клиент кэшируетОдно разрешение имени — весь трафик на один экземпляр
Пулы соединенийСоединение устанавливается один раз и держится
Нет учёта нагрузкиРаспределение случайное, а не по загрузке

Для настоящей балансировки нужен обратный прокси или средства оркестратора.

Поисковые домены и ndots

Типичный /etc/resolv.conf внутри container'а:

text
nameserver 127.0.0.11
options ndots:0

Параметр ndots:0 означает: любое имя считать полным и не дописывать к нему суффиксы. Это ускоряет разрешение и убирает класс ошибок, знакомый по Kubernetes, где ndots:5 заставляет проверять несколько вариантов перед правильным.

Если host настроен с поисковыми доменами, Docker переносит их в container. Задать явно:

bash
docker run --dns 8.8.8.8 --dns-search corp.example.com --dns-option ndots:1 ...

Почему приложение обращается к старому адресу

Сценарий: db перезапущен и получил новый IP, приложение продолжает стучаться по старому.

Причин две, и они разные:

ПричинаГде находитсяРешение
Адрес разрешён один раз при стартеВ коде приложенияРазрешать имя перед соединением
Соединение в пуле указывает на старый адресВ библиотеке-клиентеПроверка живости соединения, переподключение

Сам Docker здесь ни при чём: его резолвер отдаёт актуальный адрес сразу после перезапуска. Проблема в том, что никто повторно не спрашивает.

Практический вывод: устойчивость к перезапуску соседа обеспечивает приложение, а не сеть. Пулы соединений настраивают на проверку перед выдачей (pool_pre_ping в SQLAlchemy, health_check_interval в redis-py).

Python сам по себе результаты getaddrinfo не кэширует — каждое обращение идёт к резолверу.

Когда имя не разрешается

СимптомПричина
Name or service not known (gaierror -2)Container'ы в разных сетях, опечатка, или сосед не запущен
Разрешается, но соединение не устанавливаетсяИмя верное, проблема в порте или привязке
Работало и пересталоСосед удалён; запись исчезла вместе с ним
Не работает в default bridgeТам нет имён container'ов (урок 8.2)

Отличать первый случай от второго критично: они лечатся по-разному, а выглядят как «сервис недоступен».


Внутренний механизм

Как запрос попадает к резолверу

  1. Приложение вызывает getaddrinfo("db", ...).
  2. libc читает /etc/resolv.conf и отправляет UDP-запрос на 127.0.0.11:53.
  3. Правило внутри network namespace container'а перенаправляет пакет на порт, который слушает резолвер Docker.
  4. Резолвер ищет имя в таблице сетей, где состоит container.
  5. Найдено — возвращает адрес; не найдено — пересылает вышестоящему серверу.

Шаг 3 объясняет, почему 127.0.0.11 не виден ни с host, ни из другого container'а: перенаправление действует только в этом namespace.

Что происходит при подключении к сети

docker network connect регистрирует имя и псевдонимы container'а в зоне этой сети и добавляет их в зоны для остальных участников. Изменение вступает в силу немедленно, перезапуск не нужен (урок 8.2).

При docker network disconnect записи удаляются — имя перестаёт разрешаться сразу.


Команды и примеры

Диагностика имён без установки утилит

В python:3.13-slim нет ни dig, ни nslookup, ни ping. Всё нужное даёт стандартная библиотека.

bash
mkdir -p /tmp/dnstest && cd /tmp/dnstest

cat > dnscheck.py <<'PY'
"""Диагностика разрешения имён средствами стандартной библиотеки."""
from __future__ import annotations

import socket
import sys


def resolve(name: str) -> None:
    print(f"  {name}:")
    try:
        infos = socket.getaddrinfo(name, None, family=socket.AF_INET,
                                   type=socket.SOCK_STREAM)
    except socket.gaierror as exc:
        print(f"    ✗ не разрешается: {exc.strerror} (errno {exc.errno})")
        return
    addrs = sorted({info[4][0] for info in infos})
    for a in addrs:
        print(f"    → {a}")


def connect(name: str, port: int, timeout: float = 3.0) -> None:
    s = socket.socket()
    s.settimeout(timeout)
    try:
        s.connect((name, port))
        print(f"    порт {port}: соединение установлено")
    except socket.gaierror:
        print(f"    порт {port}: имя не разрешается")
    except ConnectionRefusedError:
        print(f"    порт {port}: отказ (хост есть, порт закрыт)")
    except OSError as exc:
        print(f"    порт {port}: {type(exc).__name__}")
    finally:
        s.close()


if __name__ == "__main__":
    print("  resolv.conf:")
    with open("/etc/resolv.conf") as f:
        for line in f:
            if line.strip() and not line.startswith("#"):
                print(f"    {line.rstrip()}")
    print()
    for arg in sys.argv[1:]:
        if ":" in arg:
            host, port = arg.rsplit(":", 1)
            resolve(host)
            connect(host, int(port))
        else:
            resolve(arg)
PY

docker network create dnsnet > /dev/null
docker run -d --network dnsnet --name web python:3.13-slim \
    python -m http.server 8000 --bind 0.0.0.0 > /dev/null
docker run -d --network dnsnet --name client \
    -v "$PWD/dnscheck.py:/dnscheck.py:ro" python:3.13-slim sleep 600 > /dev/null
sleep 3

docker exec client python /dnscheck.py web:8000 nonexistent example.com

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

text
  resolv.conf:
    nameserver 127.0.0.11
    options ndots:0

  web:
    → 172.23.0.2
    порт 8000: соединение установлено
  nonexistent:
    ✗ не разрешается: Name or service not known (errno -2)
  example.com:
    → 93.184.215.14

Три исхода в одном выводе: локальное имя разрешилось и порт доступен; несуществующее имя дало errno -2; внешнее имя разрешилось через пересылку вышестоящему серверу.

Скрипт различает «имя не разрешается» и «порт закрыт» — это и есть та развилка, с которой начинается любая диагностика (урок 8.6).

127.0.0.11 виден только изнутри

bash
echo "═══ из container ═══"
docker exec client python -c "
import socket
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.settimeout(2)
try:
    s.connect(('127.0.0.11', 53))
    print('  ✓ резолвер доступен')
except OSError as e:
    print('  ✗', e)
"

echo "═══ с host ═══"
timeout 3 python3 -c "
import socket
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.settimeout(2)
s.sendto(b'\x00', ('127.0.0.11', 53))
try:
    s.recvfrom(512)
    print('  резолвер отвечает')
except OSError as e:
    print('  ✗ с host недоступен:', type(e).__name__)
" 2>/dev/null || echo "  ✗ с host недоступен"

echo "═══ правило перенаправления внутри namespace ═══"
pid="$(docker inspect -f '{{.State.Pid}}' client)"
sudo nsenter -t "$pid" -n iptables -t nat -S 2>/dev/null | grep '127.0.0.11' | head -3 | sed 's/^/  /'

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

text
═══ из container ═══
  ✓ резолвер доступен
═══ с host ═══
  ✗ с host недоступен
═══ правило перенаправления внутри namespace ═══
  -A DOCKER_OUTPUT -d 127.0.0.11/32 -p udp -m udp --dport 53 -j DNAT --to-destination 127.0.0.11:37241
  -A DOCKER_POSTROUTING -s 127.0.0.11/32 -p udp -m udp --sport 37241 -j SNAT --to-source :53

Правила показывают устройство: запрос на порт 53 адреса 127.0.0.11 перенаправляется на случайный высокий порт, где слушает резолвер. Ответ проходит обратное преобразование, чтобы клиент видел ожидаемый порт 53.

Обратите внимание: правила живут внутри network namespace container'а. На host их нет — потому резолвер оттуда и недоступен.

Псевдонимы

bash
docker run -d --network dnsnet --name pg-primary --network-alias db \
    --network-alias database -e POSTGRES_PASSWORD=x postgres:17-alpine > /dev/null
sleep 8

docker exec client python /dnscheck.py pg-primary db database

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

text
  resolv.conf:
    nameserver 127.0.0.11
    options ndots:0

  pg-primary:
    → 172.23.0.4
  db:
    → 172.23.0.4
  database:
    → 172.23.0.4

Три имени, один адрес. Конфигурация приложения ссылается на db, и замена pg-primary на другой container не потребует её правки.

Один псевдоним, несколько container'ов

bash
for n in 1 2 3; do
    docker run -d --network dnsnet --name "api-$n" --network-alias api \
        python:3.13-slim sleep 600 > /dev/null
done
sleep 3

echo "═══ сколько адресов возвращает имя api ═══"
docker exec client python -c "
import socket
infos = socket.getaddrinfo('api', None, family=socket.AF_INET)
addrs = sorted({i[4][0] for i in infos})
print(f'  адресов: {len(addrs)}')
for a in addrs:
    print(f'    {a}')
"

echo "═══ порядок между запросами ═══"
for _ in 1 2 3 4; do
    docker exec client python -c "
import socket
print('   ', socket.gethostbyname('api'))
"
done

echo "═══ соответствие адресов container'ам ═══"
for n in 1 2 3; do
    printf '  api-%s: %s\n' "$n" \
        "$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' "api-$n")"
done

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

text
═══ сколько адресов возвращает имя api ═══
  адресов: 3
    172.23.0.5
    172.23.0.6
    172.23.0.7
═══ порядок между запросами ═══
    172.23.0.6
    172.23.0.7
    172.23.0.5
    172.23.0.6
═══ соответствие адресов container'ам ═══
  api-1: 172.23.0.5
  api-2: 172.23.0.6
  api-3: 172.23.0.7

Порядок адресов меняется — gethostbyname берёт первый, и обращения распределяются между экземплярами.

Проверим ограничение: остановим один container и посмотрим, что вернёт DNS.

bash
docker stop api-2 > /dev/null
sleep 2
echo "═══ после остановки api-2 ═══"
docker exec client python -c "
import socket
addrs = sorted({i[4][0] for i in socket.getaddrinfo('api', None, family=socket.AF_INET)})
print(f'  адресов: {len(addrs)}: {addrs}')
"
docker start api-2 > /dev/null
sleep 2

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

text
═══ после остановки api-2 ═══
  адресов: 2: ['172.23.0.5', '172.23.0.7']

Docker убрал адрес остановленного container'а — это лучше, чем можно было ожидать. Но проверки живости приложения нет: container, который запущен и не отвечает, из выдачи не исчезнет.

Именно поэтому псевдоним — не замена балансировщику.

Адрес меняется при пересоздании

bash
echo "═══ адрес web сейчас ═══"
old="$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' web)"
echo "  $old"

echo "═══ занимаем следующий адрес и пересоздаём web ═══"
docker run -d --network dnsnet --name filler python:3.13-slim sleep 600 > /dev/null
docker rm -f web > /dev/null
docker run -d --network dnsnet --name web python:3.13-slim \
    python -m http.server 8000 --bind 0.0.0.0 > /dev/null
sleep 3

new="$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' web)"
printf '  было: %s  стало: %s\n' "$old" "$new"

echo "═══ DNS отдаёт новый адрес немедленно ═══"
docker exec client python -c "
import socket
print('  разрешение web →', socket.gethostbyname('web'))
"

echo "═══ но приложение, разрешившее имя однажды, — нет ═══"
docker exec client python -c "
import socket, urllib.request

# ТАК ДЕЛАТЬ НЕ НУЖНО: адрес зафиксирован при старте
resolved_once = '$old'
try:
    urllib.request.urlopen(f'http://{resolved_once}:8000/', timeout=3)
    print('  по старому адресу: ответ получен')
except Exception as e:
    print(f'  по старому адресу: {type(e).__name__} — сервис «недоступен»')

# ТАК ПРАВИЛЬНО: имя разрешается при каждом обращении
print('  по имени:', urllib.request.urlopen('http://web:8000/', timeout=3).status)
"

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

text
═══ адрес web сейчас ═══
  172.23.0.2
═══ занимаем следующий адрес и пересоздаём web ═══
  было: 172.23.0.2  стало: 172.23.0.8
═══ DNS отдаёт новый адрес немедленно ═══
  разрешение web → 172.23.0.8
═══ но приложение, разрешившее имя однажды, — нет ═══
  по старому адресу: timeout — сервис «недоступен»
  по имени: 200

Это и есть механизм «после перезапуска базы приложение не может подключиться». Docker всё сделал правильно и отдаёт актуальный адрес; проблема в приложении, которое разрешило имя один раз.

Отсюда практическое требование: в конфигурации хранят имена, а не адреса, и соединения устанавливают по имени.

DNS не работает между сетями

bash
docker network create othernet > /dev/null
docker run -d --network othernet --name outsider \
    -v "$PWD/dnscheck.py:/dnscheck.py:ro" python:3.13-slim sleep 600 > /dev/null
sleep 2

echo "═══ container из другой сети ═══"
docker exec outsider python /dnscheck.py web:8000

echo "═══ подключаем его к dnsnet ═══"
docker network connect dnsnet outsider
sleep 2
docker exec outsider python /dnscheck.py web:8000

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

text
═══ container из другой сети ═══
  resolv.conf:
    nameserver 127.0.0.11
    options ndots:0

  web:
    ✗ не разрешается: Name or service not known (errno -2)
    порт 8000: имя не разрешается
═══ подключаем его к dnsnet ═══
  resolv.conf:
    nameserver 127.0.0.11
    options ndots:0

  web:
    → 172.23.0.8
    порт 8000: соединение установлено

Резолвер тот же самый, resolv.conf не изменился — изменилось членство в сети. Зона резолвера собирается из сетей спрашивающего.

Внешние серверы имён

bash
echo "═══ resolv.conf по умолчанию ═══"
docker run --rm --network dnsnet python:3.13-slim cat /etc/resolv.conf | sed 's/^/  /'

echo "═══ с явным --dns и --dns-search ═══"
docker run --rm --network dnsnet --dns 1.1.1.1 --dns-search corp.example.com \
    --dns-option ndots:2 python:3.13-slim cat /etc/resolv.conf | sed 's/^/  /'

echo "═══ в default bridge (без user-defined сети) ═══"
docker run --rm python:3.13-slim cat /etc/resolv.conf | sed 's/^/  /'

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

text
═══ resolv.conf по умолчанию ═══
  nameserver 127.0.0.11
  options ndots:0
═══ с явным --dns и --dns-search ═══
  nameserver 127.0.0.11
  search corp.example.com
  options ndots:2
═══ в default bridge (без user-defined сети) ═══
  nameserver 127.0.0.11
  options ndots:0

Обратите внимание: даже с --dns 1.1.1.1 в resolv.conf остаётся 127.0.0.11. Указанный сервер не подставляется напрямую — он становится вышестоящим для встроенного резолвера, который по-прежнему обрабатывает имена container'ов сам.

Это правильное поведение: иначе имена соседей перестали бы работать.

bash
docker rm -f web client pg-primary api-1 api-2 api-3 filler outsider > /dev/null 2>&1
docker network rm dnsnet othernet > /dev/null 2>&1
cd /tmp && rm -rf /tmp/dnstest

Практическое упражнение

Задание. Постройте конфигурацию, устойчивую к пересозданию сервисов, и докажите устойчивость.

Требования:

  1. Приложение обращается к базе и кэшу по именам, адреса нигде не записаны.
  2. За базой стоит псевдоним, не совпадающий с именем container'а.
  3. После пересоздания базы с другим адресом приложение продолжает работать без перезапуска.
  4. Диагностический скрипт различает четыре исхода: имя не разрешается, порт закрыт, таймаут, успех.
  5. Показано, что имя не разрешается из container'а другой сети.
  6. Показано, что псевдоним, назначенный двум container'ам, возвращает два адреса.

Подсказки

Подсказка 1

Требование 3 проверяется без перезапуска приложения: соединение устанавливается заново при каждом обращении.

Подсказка 2

Чтобы база получила другой адрес, займите её прежний адрес другим container'ом до пересоздания.

Подсказка 3

Четыре исхода в требовании 4 различаются типами исключений: gaierror, ConnectionRefusedError, TimeoutError/OSError.

Решение

Показать решение
bash
mkdir -p /tmp/dnsapp && cd /tmp/dnsapp

cat > netdiag.py <<'PY'
"""Диагностика связности: различает четыре исхода."""
from __future__ import annotations

import socket
import sys
from typing import Literal

Outcome = Literal["ok", "dns_fail", "refused", "timeout"]


def probe(host: str, port: int, timeout: float = 3.0) -> tuple[Outcome, str]:
    """Возвращает исход и пояснение."""
    try:
        addr = socket.gethostbyname(host)
    except socket.gaierror as exc:
        return "dns_fail", f"имя не разрешается ({exc.strerror})"

    s = socket.socket()
    s.settimeout(timeout)
    try:
        s.connect((addr, port))
        return "ok", f"соединение с {addr}:{port}"
    except ConnectionRefusedError:
        return "refused", f"{addr}:{port} отказал — хост есть, порт закрыт"
    except (socket.timeout, TimeoutError):
        return "timeout", f"{addr}:{port} не отвечает — вероятно, фильтрация"
    except OSError as exc:
        return "timeout", f"{addr}:{port} {type(exc).__name__}: {exc}"
    finally:
        s.close()


SYMBOL = {"ok": "✓", "dns_fail": "✗", "refused": "⚠", "timeout": "✗"}

if __name__ == "__main__":
    exit_code = 0
    for arg in sys.argv[1:]:
        host, _, port = arg.rpartition(":")
        outcome, detail = probe(host, int(port))
        print(f"  {SYMBOL[outcome]} {arg:<22} {outcome:<9} {detail}")
        if outcome != "ok":
            exit_code = 1
    sys.exit(exit_code)
PY

cat > app.py <<'PY'
"""Приложение, обращающееся к зависимостям ТОЛЬКО по именам."""
from __future__ import annotations

import os
import socket
import sys
import time

# Имена, не адреса: это и есть требование 1
DB_HOST = os.environ.get("DB_HOST", "db")
DB_PORT = int(os.environ.get("DB_PORT", "5432"))
CACHE_HOST = os.environ.get("CACHE_HOST", "cache")
CACHE_PORT = int(os.environ.get("CACHE_PORT", "6379"))


def check(host: str, port: int) -> str:
    """Разрешает имя при КАЖДОМ вызове — иначе не переживём пересоздание."""
    try:
        addr = socket.gethostbyname(host)
    except socket.gaierror:
        return f"{host}: имя не разрешается"
    s = socket.socket()
    s.settimeout(3)
    try:
        s.connect((addr, port))
        return f"{host} → {addr}:{port} ok"
    except OSError as exc:
        return f"{host} → {addr}:{port} {type(exc).__name__}"
    finally:
        s.close()


if __name__ == "__main__":
    rounds = int(sys.argv[1]) if len(sys.argv) > 1 else 1
    for i in range(rounds):
        print(f"  [{i + 1}] {check(DB_HOST, DB_PORT)}", flush=True)
        print(f"  [{i + 1}] {check(CACHE_HOST, CACHE_PORT)}", flush=True)
        if i < rounds - 1:
            time.sleep(1)
PY

# ── Развёртывание ──
docker network create appnet > /dev/null
docker network create farnet > /dev/null

# Требование 2: псевдоним не совпадает с именем container'а
docker run -d --network appnet --name pg-17-primary --network-alias db \
    -e POSTGRES_PASSWORD=secret postgres:17-alpine > /dev/null
docker run -d --network appnet --name redis-main --network-alias cache \
    redis:8-alpine > /dev/null
docker run -d --network appnet --name app \
    -v "$PWD/app.py:/app.py:ro" -v "$PWD/netdiag.py:/netdiag.py:ro" \
    python:3.13-slim sleep 900 > /dev/null
docker run -d --network farnet --name stranger \
    -v "$PWD/netdiag.py:/netdiag.py:ro" python:3.13-slim sleep 900 > /dev/null
sleep 10

fail=0
printf '\n═══ Требования 1–2: обращение по именам ═══\n'
docker exec app python /app.py 1 || fail=1
printf '    имя container базы: pg-17-primary, псевдоним: db\n'

printf '\n═══ Требование 4: четыре исхода ═══\n'
docker exec app python /netdiag.py db:5432 cache:6379 db:9999 nosuchhost:80 || true

printf '\n═══ Требование 5: имя недоступно из другой сети ═══\n'
docker exec stranger python /netdiag.py db:5432 || true

printf '\n═══ Требование 3: пересоздание базы с новым адресом ═══\n'
old="$(docker inspect -f '{{.NetworkSettings.Networks.appnet.IPAddress}}' pg-17-primary)"
printf '    адрес базы до:  %s\n' "$old"

# Занимаем освободившийся адрес, чтобы новый container получил другой
docker rm -f pg-17-primary > /dev/null
docker run -d --network appnet --name squatter python:3.13-slim sleep 900 > /dev/null
sleep 1
docker run -d --network appnet --name pg-17-primary --network-alias db \
    -e POSTGRES_PASSWORD=secret postgres:17-alpine > /dev/null
sleep 10

new="$(docker inspect -f '{{.NetworkSettings.Networks.appnet.IPAddress}}' pg-17-primary)"
printf '    адрес базы после: %s\n' "$new"
if [ "$old" = "$new" ]; then
    printf '    (адрес совпал — проверка менее показательна)\n'
fi

printf '    приложение НЕ перезапускалось:\n'
docker exec app python /app.py 1 || fail=1

printf '\n═══ Требование 6: псевдоним у двух container ═══\n'
docker run -d --network appnet --name redis-second --network-alias cache \
    redis:8-alpine > /dev/null
sleep 4
docker exec app python -c "
import socket
addrs = sorted({i[4][0] for i in socket.getaddrinfo('cache', None, family=socket.AF_INET)})
print(f'  имя cache → {len(addrs)} адрес(а): {addrs}')
import sys
sys.exit(0 if len(addrs) == 2 else 1)
" || fail=1
for c in redis-main redis-second; do
    printf '    %-13s %s\n' "$c" \
        "$(docker inspect -f '{{.NetworkSettings.Networks.appnet.IPAddress}}' "$c")"
done

printf '\n═══ ИТОГ ═══\n'
[ "$fail" -eq 0 ] && echo "  все требования выполнены" || echo "  ЕСТЬ ПРОВАЛЫ"

docker rm -f app pg-17-primary redis-main redis-second squatter stranger > /dev/null 2>&1
docker network rm appnet farnet > /dev/null 2>&1
cd /tmp && rm -rf /tmp/dnsapp

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

text
═══ Требования 1–2: обращение по именам ═══
  [1] db → 172.24.0.2:5432 ok
  [1] cache → 172.24.0.3:6379 ok
    имя container базы: pg-17-primary, псевдоним: db

═══ Требование 4: четыре исхода ═══
  ✓ db:5432               ok        соединение с 172.24.0.2:5432
  ✓ cache:6379            ok        соединение с 172.24.0.3:6379
  ⚠ db:9999               refused   172.24.0.2:9999 отказал — хост есть, порт закрыт
  ✗ nosuchhost:80         dns_fail  имя не разрешается (Name or service not known)

═══ Требование 5: имя недоступно из другой сети ═══
  ✗ db:5432               dns_fail  имя не разрешается (Name or service not known)

═══ Требование 3: пересоздание базы с новым адресом ═══
    адрес базы до:  172.24.0.2
    адрес базы после: 172.24.0.6
    приложение НЕ перезапускалось:
  [1] db → 172.24.0.6:5432 ok
  [1] cache → 172.24.0.3:6379 ok

═══ Требование 6: псевдоним у двух container ═══
  имя cache → 2 адрес(а): ['172.24.0.3', '172.24.0.7']
    redis-main    172.24.0.3
    redis-second  172.24.0.7

═══ ИТОГ ═══
  все требования выполнены

Все шесть требований выполнены.

Три решения, определяющие качество.

check() разрешает имя внутри себя, а не при импорте модуля. Это единственная строка, от которой зависит требование 3. Вынеси разрешение на уровень модуля — приложение зафиксировало бы адрес при старте, и после пересоздания базы обращалось бы в пустоту, хотя DNS отдаёт правильный ответ. Одна строка отделяет устойчивую конфигурацию от хрупкой.

Проверка требования 3 занимает освободившийся адрес специальным container'ом. Без этого Docker с большой вероятностью выдал бы новой базе тот же самый адрес, и проверка прошла бы, ничего не проверив: приложение с зафиксированным адресом тоже сработало бы. Скрипт печатает оба адреса и честно предупреждает, если они всё же совпали.

netdiag.py различает refused и timeout, а не сводит их к «недоступно». Разница диагностическая: refused означает, что пакет дошёл и хост ответил — проблема в порте или в том, что процесс не слушает. timeout означает, что ответа не было вовсе — фильтрация или отсутствие маршрута. Проверка db:9999 в выводе демонстрирует именно refused, подтверждая, что различение работает.

Чего решение не делает. Оно не решает вторую половину проблемы устойчивости — пулы соединений. Приложение здесь создаёт соединение заново при каждом обращении, поэтому переживает смену адреса автоматически. Реальное приложение с SQLAlchemy или redis-py держит открытые соединения, которые после пересоздания базы указывают в никуда; повторное разрешение имени им не поможет. Нужны pool_pre_ping=True и повторные попытки — но это уже свойство клиента, а не сети.

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

bash
docker network create dcheck > /dev/null
docker run -d --network dcheck --name svc --network-alias myalias alpine:3.21 sleep 60 > /dev/null
docker run --rm --network dcheck alpine:3.21 sh -c 'getent hosts svc; getent hosts myalias'
docker rm -f svc > /dev/null; docker network rm dcheck > /dev/null

Ожидаются две строки с одним и тем же адресом.

Типичные ошибки

ОшибкаПричинаИсправление
Адрес зафиксирован при старте приложенияРазрешили имя один разРазрешать перед каждым соединением
IP-адреса в конфигурацииКажется надёжнееМеняются при пересоздании; хранить имена
--hostname в расчёте на DNS-имяНазвание вводит в заблуждениеИспользовать --name или --network-alias
Ожидают DNS в default bridgeНе различают типы сетейИмена только в user-defined
Псевдоним вместо балансировщикаВозвращает несколько адресовНет проверки живости и учёта нагрузки
Устанавливают dnsutils ради диагностикиНет dig в образеsocket.getaddrinfo или getent hosts
--dns 8.8.8.8 в расчёте на прямое использованиеЛогично предположитьСтановится вышестоящим для 127.0.0.11
Считают gaierror и timeout однимОба «не работает»Разные причины и разные исправления
Ищут 127.0.0.11 с hostАдрес виден в resolv.confПеренаправление действует только в namespace

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

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

  1. Что такое 127.0.0.11 и почему он недоступен с host?
  2. По каким именам доступен container? Назовите четыре источника имён.
  3. Почему --hostname не добавляет DNS-записи?
  4. Почему приложение обращается к старому адресу после пересоздания соседа?
  5. Чем псевдоним на нескольких container'ах отличается от настоящей балансировки?

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

  1. Как проверить разрешение имени в образе без сетевых утилит?
  2. Как развязать имя в конфигурации от имени container'а?
  3. Как отличить «имя не разрешается» от «порт закрыт»?

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

  1. Name or service not known при обращении к соседу. Три версии?
  2. Имя разрешается, соединение не устанавливается. Где искать?

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

  1. В user-defined сети /etc/resolv.conf содержит единственный сервер — 127.0.0.11.
  2. Адрес принадлежит namespace container'а; с host и из других container'ов недоступен.
  3. Внутри namespace правило перенаправляет запрос на порт резолвера Docker.
  4. Резолвер знает имена container'ов тех сетей, в которых состоит спрашивающий.
  5. Имена дают --name, --network-alias и имена сервисов Compose; --hostname — нет.
  6. Псевдоним развязывает имя в конфигурации от имени container'а.
  7. Один псевдоним у нескольких container'ов возвращает несколько адресов с чередованием порядка.
  8. Проверки живости у этого механизма нет — балансировщик он не заменяет.
  9. Docker отдаёт актуальный адрес сразу после пересоздания container'а.
  10. Приложение, разрешившее имя один раз, об изменении не узнает — разрешать нужно перед соединением.
  11. В конфигурации хранят имена, а не адреса.
  12. --dns задаёт вышестоящий сервер для 127.0.0.11, а не заменяет его.

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

ИсточникСсылкаЧто подтверждает
Docker: DNS serviceshttps://docs.docker.com/engine/network/#dns-servicesВстроенный резолвер, 127.0.0.11, поведение в разных сетях
Docker: bridge networkshttps://docs.docker.com/engine/network/drivers/bridge/Разрешение имён в user-defined сетях
Docker: docker network connecthttps://docs.docker.com/reference/cli/docker/network/connect/--alias, регистрация имён
Docker: docker run referencehttps://docs.docker.com/reference/cli/docker/container/run/#dns--dns, --dns-search, --dns-option, --hostname
Compose: networkinghttps://docs.docker.com/compose/how-tos/networking/Имена сервисов как DNS-имена
Linux: resolv.conf(5)https://man7.org/linux/man-pages/man5/resolv.conf.5.htmlndots, search, порядок разрешения
Python: sockethttps://docs.python.org/3/library/socket.html#socket.getaddrinfoРазрешение имён без кэширования

Навигация

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

Markdown на GitHub ↗