8.4. Docker DNS
Цели
После этого материала вы сможете:
- объяснить, что такое
127.0.0.11и почему этот адрес виден только внутри container'а; - назвать все имена, по которым доступен container, и порядок их разрешения;
- использовать network aliases, в том числе один псевдоним на несколько container'ов;
- диагностировать сбой разрешения имени без установки
digиnslookup; - объяснить, почему приложение продолжает обращаться к старому адресу после перезапуска соседа;
- настроить внешние DNS-серверы и поисковые домены.
Предварительные знания
- 8.2. Bridge networks — DNS работает только в user-defined сетях;
- 8.1. Основы Docker networking.
Ключевые термины
| Термин | Объяснение |
|---|---|
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
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'ов:
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'а:
nameserver 127.0.0.11
options ndots:0
Параметр ndots:0 означает: любое имя считать полным и не дописывать к нему суффиксы. Это ускоряет разрешение и убирает класс ошибок, знакомый по Kubernetes, где ndots:5 заставляет проверять несколько вариантов перед правильным.
Если host настроен с поисковыми доменами, Docker переносит их в container. Задать явно:
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) |
Отличать первый случай от второго критично: они лечатся по-разному, а выглядят как «сервис недоступен».
Внутренний механизм
Как запрос попадает к резолверу
- Приложение вызывает
getaddrinfo("db", ...). libcчитает/etc/resolv.confи отправляет UDP-запрос на127.0.0.11:53.- Правило внутри network namespace container'а перенаправляет пакет на порт, который слушает резолвер Docker.
- Резолвер ищет имя в таблице сетей, где состоит container.
- Найдено — возвращает адрес; не найдено — пересылает вышестоящему серверу.
Шаг 3 объясняет, почему 127.0.0.11 не виден ни с host, ни из другого container'а: перенаправление действует только в этом namespace.
Что происходит при подключении к сети
docker network connect регистрирует имя и псевдонимы container'а в зоне этой сети и добавляет их в зоны для остальных участников. Изменение вступает в силу немедленно, перезапуск не нужен (урок 8.2).
При docker network disconnect записи удаляются — имя перестаёт разрешаться сразу.
Команды и примеры
Диагностика имён без установки утилит
В python:3.13-slim нет ни dig, ни nslookup, ни ping. Всё нужное даёт стандартная библиотека.
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
Ожидаемый вывод:
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 виден только изнутри
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/^/ /'
Ожидаемый вывод:
═══ из 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 их нет — потому резолвер оттуда и недоступен.
Псевдонимы
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
Ожидаемый вывод:
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'ов
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
Ожидаемый вывод:
═══ сколько адресов возвращает имя 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.
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
Ожидаемый вывод:
═══ после остановки api-2 ═══
адресов: 2: ['172.23.0.5', '172.23.0.7']
Docker убрал адрес остановленного container'а — это лучше, чем можно было ожидать. Но проверки живости приложения нет: container, который запущен и не отвечает, из выдачи не исчезнет.
Именно поэтому псевдоним — не замена балансировщику.
Адрес меняется при пересоздании
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)
"
Ожидаемый вывод:
═══ адрес web сейчас ═══
172.23.0.2
═══ занимаем следующий адрес и пересоздаём web ═══
было: 172.23.0.2 стало: 172.23.0.8
═══ DNS отдаёт новый адрес немедленно ═══
разрешение web → 172.23.0.8
═══ но приложение, разрешившее имя однажды, — нет ═══
по старому адресу: timeout — сервис «недоступен»
по имени: 200
Это и есть механизм «после перезапуска базы приложение не может подключиться». Docker всё сделал правильно и отдаёт актуальный адрес; проблема в приложении, которое разрешило имя один раз.
Отсюда практическое требование: в конфигурации хранят имена, а не адреса, и соединения устанавливают по имени.
DNS не работает между сетями
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
Ожидаемый вывод:
═══ 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 не изменился — изменилось членство в сети. Зона резолвера собирается из сетей спрашивающего.
Внешние серверы имён
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/^/ /'
Ожидаемый вывод:
═══ 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'ов сам.
Это правильное поведение: иначе имена соседей перестали бы работать.
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
Практическое упражнение
Задание. Постройте конфигурацию, устойчивую к пересозданию сервисов, и докажите устойчивость.
Требования:
- Приложение обращается к базе и кэшу по именам, адреса нигде не записаны.
- За базой стоит псевдоним, не совпадающий с именем container'а.
- После пересоздания базы с другим адресом приложение продолжает работать без перезапуска.
- Диагностический скрипт различает четыре исхода: имя не разрешается, порт закрыт, таймаут, успех.
- Показано, что имя не разрешается из container'а другой сети.
- Показано, что псевдоним, назначенный двум container'ам, возвращает два адреса.
Подсказки
Подсказка 1
Требование 3 проверяется без перезапуска приложения: соединение устанавливается заново при каждом обращении.
Подсказка 2
Чтобы база получила другой адрес, займите её прежний адрес другим container'ом до пересоздания.
Подсказка 3
Четыре исхода в требовании 4 различаются типами исключений: gaierror, ConnectionRefusedError, TimeoutError/OSError.
Решение
Показать решение
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
Ожидаемый вывод:
═══ Требования 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 и повторные попытки — но это уже свойство клиента, а не сети.
Проверка результата
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 |
Контрольные вопросы
На понимание:
- Что такое
127.0.0.11и почему он недоступен с host? - По каким именам доступен container? Назовите четыре источника имён.
- Почему
--hostnameне добавляет DNS-записи? - Почему приложение обращается к старому адресу после пересоздания соседа?
- Чем псевдоним на нескольких container'ах отличается от настоящей балансировки?
На применение:
- Как проверить разрешение имени в образе без сетевых утилит?
- Как развязать имя в конфигурации от имени container'а?
- Как отличить «имя не разрешается» от «порт закрыт»?
На диагностику:
Name or service not knownпри обращении к соседу. Три версии?- Имя разрешается, соединение не устанавливается. Где искать?
Краткое резюме
- В user-defined сети
/etc/resolv.confсодержит единственный сервер —127.0.0.11. - Адрес принадлежит namespace container'а; с host и из других container'ов недоступен.
- Внутри namespace правило перенаправляет запрос на порт резолвера Docker.
- Резолвер знает имена container'ов тех сетей, в которых состоит спрашивающий.
- Имена дают
--name,--network-aliasи имена сервисов Compose;--hostname— нет. - Псевдоним развязывает имя в конфигурации от имени container'а.
- Один псевдоним у нескольких container'ов возвращает несколько адресов с чередованием порядка.
- Проверки живости у этого механизма нет — балансировщик он не заменяет.
- Docker отдаёт актуальный адрес сразу после пересоздания container'а.
- Приложение, разрешившее имя один раз, об изменении не узнает — разрешать нужно перед соединением.
- В конфигурации хранят имена, а не адреса.
--dnsзадаёт вышестоящий сервер для127.0.0.11, а не заменяет его.
Официальные источники
| Источник | Ссылка | Что подтверждает |
|---|---|---|
| Docker: DNS services | https://docs.docker.com/engine/network/#dns-services | Встроенный резолвер, 127.0.0.11, поведение в разных сетях |
| Docker: bridge networks | https://docs.docker.com/engine/network/drivers/bridge/ | Разрешение имён в user-defined сетях |
Docker: docker network connect | https://docs.docker.com/reference/cli/docker/network/connect/ | --alias, регистрация имён |
Docker: docker run reference | https://docs.docker.com/reference/cli/docker/container/run/#dns | --dns, --dns-search, --dns-option, --hostname |
| Compose: networking | https://docs.docker.com/compose/how-tos/networking/ | Имена сервисов как DNS-имена |
Linux: resolv.conf(5) | https://man7.org/linux/man-pages/man5/resolv.conf.5.html | ndots, search, порядок разрешения |
Python: socket | https://docs.python.org/3/library/socket.html#socket.getaddrinfo | Разрешение имён без кэширования |
Навигация
← Предыдущий материал
Вернуться к разделу
Следующий материал → Network modes
Главное оглавление