DNS у контейнерах ламається найдеморалізуючим способом: не постійно. Ви відправляєте імідж, перевірки здоров’я зелені, потім додаток дві хвилини не може вирішити db.internal, відновлюється, знову падає — і хронологія інциденту перетворюється на інтерпретативний танець.
На сучасному Linux це частіше не «Docker DNS», а «у вашого хоста /etc/resolv.conf вказує на системний stub-резидент (127.0.0.53), а Docker скопіював його в неймспейс мережі, де ця адреса не означає те, що ви думаєте». Виправити раз — легко. Зробити так, щоб це трималось через перезавантаження, зміну VPN, поновлення DHCP і оновлення дистрибутива — ось справжнє завдання.
Швидка діагностика: план дій
Ось порядок, який я використовую, коли хтось каже «DNS в Docker не працює». Він призначений швидко виявити вузьке місце, а не задовольнити філософську цікавость.
Перше: це тільки в контейнері чи на хості теж?
- Вирішіть з хоста те саме ім’я, на якому контейнер падає.
- Вирішіть з чистого контейнера, приєднаного до тієї ж мережі, що й проблемний сервіс.
- Порівняйте DNS-сервери в хостовому
/etc/resolv.confі в контейнерному/etc/resolv.conf.
Якщо хост теж не може вирішити ім’я, це не проблема Docker. Це проблема DNS, яку Docker просто успадковує.
Друге: причетний 127.0.0.53?
Якщо в контейнері в /etc/resolv.conf є nameserver 127.0.0.53, це найімовірніший доказ на більшості систем з systemd-resolved. У мережевому неймспейсі контейнера 127.0.0.53 означає сам контейнер, а не хостовий stub.
Третє: збій у вбудованому DNS Docker (127.0.0.11) чи в upstream?
У користувацьких bridge-мережах Docker надає вбудований DNS на 127.0.0.11, який пересилає запити вгору. Якщо контейнери не можуть вирішити імена сервісів (контейнер->контейнер) і зовнішні імена — можливо, вбудований DNS Docker або шлях iptables зламалися. Якщо служба відкривається, а зовнішнє — ні, ймовірно upstream-резолвери неправильні.
Четверте: маємо розділений DNS?
VPN і корпоративні мережі люблять split DNS: внутрішні домени йдуть на внутрішні резолвери; усе інше — у публічні. systemd-resolved підтримує це елегантно. Docker… менше. Очікуйте, що «виправлений» DNS може й досі не працювати для *.corp, поки ви явно не навчите Docker внутрішніх резолверів.
П’яте: перевірте MTU / відкат на TCP / дивності EDNS
Переривчасті таймаути, особливо через VPN, можуть бути через фрагментацію DNS-пакетів або їх відкид. Примусіть TCP як тест. Якщо TCP працює, а UDP — ні, ви в полі PMTU / брандмауера.
Що саме ламається: Docker, resolv.conf і systemd-resolved
Назвемо рухомі частини:
- systemd-resolved — менеджер локального резолвера. Він може запускати «stub listener» на
127.0.0.53, підтримувати per-link DNS-сервери й робити split DNS за доменами. - /etc/resolv.conf — файл, який читають libc-резолвери, щоб знайти nameserver-и і search-домени. Це може бути звичайний файл або симлінк, яким керує systemd-resolved.
- Docker генерує
/etc/resolv.confконтейнера на основі конфігурації резолвера хоста, з деякими відмінностями залежно від режиму мережі та перевизначень DNS. - Вбудований DNS Docker (
127.0.0.11) існує в користувацьких bridge-мережах. Він забезпечує розв’язування імен контейнерів і переадресовує запити вгору.
Класичний сценарій відмови виглядає так:
- Ваш дистрибутив вмикає systemd-resolved.
/etc/resolv.confстає симлінком на stub-конфіг, який вказує наnameserver 127.0.0.53.- Docker бачить це і записує той самий nameserver у контейнери.
- У контейнерах
127.0.0.53— не резолвер хоста. Це loopback самого контейнера. - Запити таймаутяться або повертають «connection refused». Додатки трактують це як «DNS упав». Вони мають рацію.
Існує другий, тонший режим відмови: Docker використовує 127.0.0.11 в контейнерах, який пересилає на upstream. Але upstream заповнюється з конфігу хоста під час старту Docker. Потім піднімається VPN, systemd-resolved змінює upstream для конкретного інтерфейсу, а Docker продовжує пересилати на старі. Все здається нормальним, поки не знадобиться внутрішній домен. Тоді виникає «чому мій ноутбук вирішує, а контейнер — ні?»
І так, ви можете заглушити проблему, захардкодивши 8.8.8.8 у Docker. Це інженерний еквівалент лікування болю в грудях енергетиком. Може зробити дашборд зеленим, але обходить внутрішній DNS, порушує політику, ламає split DNS і ускладнює відладку в продакшені.
Одна перефразована думка від Dan Kaminsky (дослідник DNS), яка добре витримала час: DNS обманливо простий, поки не зламається, і тоді він ламається так, що нагадує все підряд.
(перефразована ідея)
Жарт для настрою: DNS — це єдина залежність, яка може зламатися і переконати вас, що проблема в додатку. Це як бути ігнорованим IP-адресою.
Цікаві факти та історія (що пояснює дивності)
- Факт 1: Stub-listener systemd-resolved використовує
127.0.0.53за умовчанням, не через магію. Це просто loopback, а неймспейси змінюють, що означає loopback. - Факт 2: У багатьох релізах Ubuntu
/etc/resolv.confє симлінком на/run/systemd/resolve/stub-resolv.conf, коли systemd-resolved увімкнений. - Факт 3: systemd-resolved підтримує дві згенеровані версії resolv.conf: stub-файл (вказує на
127.0.0.53) і «реальний upstream» (перелічує фактичні DNS-сервери), зазвичай у/run/systemd/resolve/resolv.conf. - Факт 4: Вбудований DNS Docker на
127.0.0.11не є загальноцільовим рекурсивним резолвером. Це форвардер плюс сервіс-дискавері для контейнерів, і він залежить від правильності upstream. - Факт 5: Опція
ndotsу resolv.conf змінює, коли додаються search-домени. Великеndotsу поєднанні з довгими списками search може перетворити один запит на кілька і виглядати повільно. - Факт 6: glibc-resolver історично віддає перевагу UDP і потім відкочується на TCP. Деякі брандмауери відкидають фрагментовані UDP-відповіді, що створює повільні відновлення і таймаути, які виглядають переривчасто.
- Факт 7: До того, як вбудований DNS Docker став звичним у користувацьких мережах, поведінка DNS у контейнерах була різною, і люди часто підключали контейнери до мережі хоста, щоб «вирішити DNS», що зазвичай створювало нові проблеми.
- Факт 8: Split DNS (маршрутизація доменів до конкретних резолверів) поширена в корпоративних середовищах і є флагманською функцією systemd-resolved; Docker сам по собі не відтворює per-domain маршрутизацію, якщо ви явно цього не налаштуєте.
- Факт 9: Дивно велика кількість «DNS-аутеджів» у контейнерних парках фактично пов’язані з кешуванням: резолвер у контейнері кешує інакше, ніж хост, або додаток кешує неправильні результати й не повторює запити коректно.
Практичні завдання: команди, очікуваний вивід та що вирішувати
DNS не виправляють на підставі відчуттів. Їх виправляють за допомогою цілеспрямованих спостережень і рішень. Ось практичні завдання, які я реально запускаю, з тим, що значить вивід і що робити далі.
Завдання 1: Перевірте, що хост думає про /etc/resolv.conf
cr0x@server:~$ ls -l /etc/resolv.conf
lrwxrwxrwx 1 root root 39 Nov 18 09:02 /etc/resolv.conf -> ../run/systemd/resolve/stub-resolv.conf
Значення: Файл резолвера хоста — симлінк на stub. Очікуйте 127.0.0.53 всередині.
Рішення: Ймовірно, потрібно змусити Docker використовувати файл з реальними upstream або явно вказати DNS-сервери для Docker.
Завдання 2: Перегляньте stub-resolv.conf хоста
cr0x@server:~$ cat /run/systemd/resolve/stub-resolv.conf
# This file is managed by man:systemd-resolved(8). Do not edit.
nameserver 127.0.0.53
options edns0 trust-ad
search corp.example
Значення: Хост використовує локальний stub systemd. Добре для хоста, токсично, якщо скопіювати в контейнери.
Рішення: Не вказуйте контейнерам 127.0.0.53. Забезпечте, щоб вони використовували реальні upstream DNS-сервери.
Завдання 3: Перегляньте «реальний» список резолверів хоста
cr0x@server:~$ cat /run/systemd/resolve/resolv.conf
# This file is managed by man:systemd-resolved(8). Do not edit.
nameserver 10.20.30.40
nameserver 10.20.30.41
search corp.example
Значення: Це upstream DNS-сервери, з якими розмовляє systemd-resolved.
Рішення: Це зазвичай правильні сервери, які варто передати Docker — або через конфіг daemon, або акуратно перенаправивши /etc/resolv.conf.
Завдання 4: Підтвердіть стан systemd-resolved і stub-listener
cr0x@server:~$ systemctl status systemd-resolved --no-pager
● systemd-resolved.service - Network Name Resolution
Loaded: loaded (/lib/systemd/system/systemd-resolved.service; enabled; vendor preset: enabled)
Active: active (running) since Mon 2026-01-02 07:41:12 UTC; 2h 18min ago
...
DNS Stub Listener: yes
Значення: Resolved активний і stub-listener увімкнений.
Рішення: Якщо ви вимкнете stub-listener, переконайтесь, що система все ще має робочий resolv.conf. Бездумне відключення може спричинити відмову хоста.
Завдання 5: Подивіться per-link DNS, включно з split DNS
cr0x@server:~$ resolvectl status
Global
LLMNR setting: yes
MulticastDNS setting: no
DNSOverTLS setting: no
DNSSEC setting: no
DNSSEC supported: no
Current DNS Server: 10.20.30.40
DNS Servers: 10.20.30.40 10.20.30.41
DNS Domain: corp.example
Link 2 (ens18)
Current Scopes: DNS
Protocols: +DefaultRoute +LLMNR -mDNS -DNSOverTLS DNSSEC=no/unsupported
Current DNS Server: 10.20.30.40
DNS Servers: 10.20.30.40 10.20.30.41
DNS Domain: corp.example
Значення: Підтверджує, які DNS-сервери й домени активні. Якщо є VPN-посилання, воно може мати свої домени й сервери.
Рішення: Якщо ви залежите від split DNS, просто вказати «1.1.1.1» не вирішить проблему. Потрібно передати внутрішні резолвери в Docker.
Завдання 6: Перевірте, що Docker думає про DNS (вид демонa)
cr0x@server:~$ docker info --format '{{json .}}' | jq '.DockerRootDir, .Name, .SecurityOptions'
"/var/lib/docker"
"server"
[
"name=apparmor",
"name=seccomp,profile=default"
]
Значення: Це прямо не показує DNS, але підтверджує, що ви не в якомусь екзотичному runtime-путь. Ми встановлюємо контекст.
Рішення: Перейдіть до перевірки /etc/resolv.conf в контейнері і конфігу демона.
Завдання 7: Перегляньте DNS-конфіг всередині запущеного контейнера
cr0x@server:~$ docker exec -it web01 cat /etc/resolv.conf
nameserver 127.0.0.53
options edns0 trust-ad
search corp.example
Значення: Цей контейнер вказує сам на себе для DNS. Він впаде, якщо всередині контейнера нічого не слухає 127.0.0.53:53 (а нікого там не буде).
Рішення: Виправте вхідні дані DNS для Docker (конфіг демона) або зв’язок симлінку /etc/resolv.conf хоста так, щоб контейнери отримували реальні upstream-сервери.
Завдання 8: Швидкий функціональний тест зсередини контейнера
cr0x@server:~$ docker exec -it web01 getent ahosts example.com
getent: Name or service not known
Значення: libc-резолюція не працює. Це не «curl не дістає інтернет», це — неможливість розв’язати ім’я.
Рішення: Підтвердіть досяжність nameserver-а і чи задіяний вбудований DNS Docker.
Завдання 9: З’ясуйте, чи використовується вбудований DNS Docker (127.0.0.11)
cr0x@server:~$ docker run --rm alpine:3.19 cat /etc/resolv.conf
nameserver 127.0.0.11
options ndots:0
Значення: Цей контейнер у користувацькій мережі або Docker вирішив використати вбудований DNS. Плюс: це уникає пастки з 127.0.0.53. Мінус: все одно потрібні правильні upstream.
Рішення: Якщо зовнішнє вирішення не працює з 127.0.0.11, розслідуйте upstream DNS демона Docker, iptables або брандмауер.
Завдання 10: Перевірте досяжність upstream-резолвера з неймспейсу хоста
cr0x@server:~$ dig +time=1 +tries=1 @10.20.30.40 example.com A
; <<>> DiG 9.18.24 <<>> +time=1 +tries=1 @10.20.30.40 example.com A
;; NOERROR, id: 22031
;; ANSWER SECTION:
example.com. 300 IN A 93.184.216.34
Значення: Upstream-резолвер досяжний і відповідає.
Рішення: Якщо контейнери все ще падають, проблема, ймовірно, у пересиланні з контейнера на хост, вбудованому DNS Docker або в NAT/iptables.
Завдання 11: Перевірте досяжність upstream-резолвера зсередини контейнера
cr0x@server:~$ docker run --rm alpine:3.19 sh -c "apk add --no-cache bind-tools >/dev/null; dig +time=1 +tries=1 @10.20.30.40 example.com A"
; <<>> DiG 9.18.24 <<>> +time=1 +tries=1 @10.20.30.40 example.com A
;; connection timed out; no servers could be reached
Значення: З контейнерної мережі цей резолвер недосяжний. Може бути проблема маршрутизації, брандмауера або резолвер доступний лише з неймспейсу хоста.
Рішення: Якщо внутрішні резолвери доступні лише через маршрути хоста, які не NAT-яться для контейнерів, потрібно виправити маршрути/НАТ, використовувати host-networking для сервісу або запустити DNS-форвардер, доступний контейнерам.
Завдання 12: Перегляньте деталі Docker-мережі і DNS контейнера
cr0x@server:~$ docker inspect web01 --format '{{json .HostConfig.Dns}} {{json .NetworkSettings.Networks}}' | jq .
[
null,
{
"appnet": {
"IPAMConfig": null,
"Links": null,
"Aliases": [
"web01",
"web"
],
"NetworkID": "b3c2f5cbd7c7f0d7d3b7d7aa3d2c51a9c7bd22b9f5a3db0a3d35a8a2c4d9a111",
"EndpointID": "c66d42e9a07b6b6a8e6d6d3fb5a5a0de27b8464a9e7d0a2c4e5b11aa3aa2beef",
"Gateway": "172.18.0.1",
"IPAddress": "172.18.0.10",
"IPPrefixLen": 16,
"IPv6Gateway": "",
"GlobalIPv6Address": "",
"GlobalIPv6PrefixLen": 0,
"MacAddress": "02:42:ac:12:00:0a",
"DriverOpts": null
}
}
]
Значення: У контейнера немає явного DNS; він успадкував налаштування за замовчуванням. Мережа — користувацький bridge appnet.
Рішення: Якщо значення за замовчуванням неправильні, виправте на рівні демона або per-network/Compose. Для узгодженості краще робити на рівні демона.
Завдання 13: Перевірте файл конфігурації демона Docker
cr0x@server:~$ sudo cat /etc/docker/daemon.json
{
"log-driver": "journald"
}
Значення: Немає визначеної DNS-конфігурації. Docker буде орієнтуватись на конфіг хоста і те, що підхопив під час старту.
Рішення: Додайте явні DNS-сервери (і за потреби search/opti), якщо ваш хост використовує systemd-resolved stub або upstream часто змінюються.
Завдання 14: Перевірте, чи контейнери можуть дістатись до порту вбудованого DNS Docker
cr0x@server:~$ docker run --rm alpine:3.19 sh -c "apk add --no-cache drill >/dev/null; drill @127.0.0.11 example.com | head"
;; ->>HEADER<<- opcode: QUERY, rcode: NOERROR, id: 59030
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0
;; QUESTION SECTION:
;; example.com. IN A
Значення: Вбудований DNS відповідає. Якщо при цьому додаток і далі не резолвить, можливо проблема з опціями libc/search, а не з сирим DNS-з’єднанням.
Рішення: Перегляньте ndots, search-домени і кешування на рівні додатка.
Завдання 15: Перевірте, чи search/ndots спричиняють шторм запитів
cr0x@server:~$ docker exec -it web01 sh -c "cat /etc/resolv.conf; echo; getent hosts api"
nameserver 127.0.0.11
options ndots:5
search corp.example svc.cluster.local
getent: Name or service not known
Значення: З ndots:5 резолвер вважає короткі імена «відносними» і спочатку додає search-домени. Це може спричинити повільні відмови, якщо ці суфікси не резолвляться.
Рішення: Для не-Kubernetes Docker-навантажень тримайте ndots скромним (зазвичай 0–1) і скоротіть список search-доменів. Або вчіть додатки використовувати FQDN.
Завдання 16: Захопіть DNS-трафік, щоб довести, де він губиться
cr0x@server:~$ sudo tcpdump -ni any port 53 -c 10
tcpdump: data link type LINUX_SLL2
listening on any, link-type LINUX_SLL2 (Linux cooked v2), snapshot length 262144 bytes
10:22:01.112233 vethabc123 Out IP 172.18.0.10.45122 > 10.20.30.40.53: 12345+ A? example.com. (28)
10:22:02.113244 vethabc123 Out IP 172.18.0.10.45122 > 10.20.30.40.53: 12345+ A? example.com. (28)
10:22:03.114255 vethabc123 Out IP 172.18.0.10.45122 > 10.20.30.40.53: 12345+ A? example.com. (28)
Значення: Запити виходять з veth контейнера, але відповідей немає. Це не проблема libc. Це шлях, брандмауер або upstream, що кидає запити.
Рішення: Перевірте маршрути до резолвера з Docker-bridge, підтвердіть NAT/masquerade, перевірте, чи upstream приймає запити з цього джерела.
Стійкі виправлення (виберіть одну стратегію і дотримуйтесь)
Є кілька правильних варіантів виправлення. Неправильно — змішувати їх у довільний спосіб, поки «на моєму ноуті працює». Оберіть стратегію, що відповідає вашому середовищу: ноутбуки з частими VPN, сервери зі статичними резолверами або змішана корпоративна мережа.
Стратегія A (найпоширеніша): Налаштуйте демон Docker з явними DNS-серверами
Це грубий, але ефективний підхід: скажіть Docker, які DNS-сервери передавати контейнерам (або використовувати як upstream для вбудованого DNS). Це стабільно працює незалежно від поведінки systemd-resolved stub і не залежить від ігор із симлінками /etc/resolv.conf.
Коли застосовувати: у вас відомі IP резолверів (внутрішні або локальний форвардер) і ви хочете передбачувану поведінку.
cr0x@server:~$ sudo tee /etc/docker/daemon.json >/dev/null <<'EOF'
{
"dns": ["10.20.30.40", "10.20.30.41"],
"dns-search": ["corp.example"],
"dns-opts": ["timeout:2", "attempts:2"]
}
EOF
cr0x@server:~$ sudo systemctl restart docker
На що звернути увагу: Нові контейнери повинні відображати ці резолвери або 127.0.0.11 з правильною переадресацією. Існуючі контейнери можуть потребувати перезапуску, щоб підхопити нові налаштування.
Недоліки: Чудова узгодженість. Погано, якщо DNS-сервери динамічні (наприклад DHCP на ноутбуках). Для ноутбуків краще вказати Docker на локальний форвардер, котрий слідкує за systemd-resolved.
Стратегія B: Перенаправити /etc/resolv.conf хоста на «реальний» upstream-файл systemd-resolved
Це змусить Docker успадкувати реальні upstream-сервери, змінивши, куди вказує /etc/resolv.conf. Ефективно і швидко, але змінює поведінку хоста. Якщо ви не розумієте наслідків, не робіть цього на продакшн-нодах без плану відкату.
Коли застосовувати: коли ви хочете, щоб Docker автоматично успадковував upstream DNS і вам нормально обходити stub-listener для libc-клієнтів.
cr0x@server:~$ sudo ln -sf /run/systemd/resolve/resolv.conf /etc/resolv.conf
cr0x@server:~$ ls -l /etc/resolv.conf
lrwxrwxrwx 1 root root 32 Jan 2 10:31 /etc/resolv.conf -> /run/systemd/resolve/resolv.conf
Значення: Ви зробили так, що /etc/resolv.conf перелічує реальні upstream-сервери, а не 127.0.0.53.
Рішення: Перезапустіть Docker, щоб він перечитав конфіг резолвера хоста; потім перезапустіть уражені контейнери.
cr0x@server:~$ sudo systemctl restart docker
Недоліки: Split DNS може стати менш елегантним, якщо ви покладалися на systemd-resolved для per-domain маршрутизації для локальних stub-клієнтів. Деякі додатки говорять з resolved через NSS-модулі; багато хто просто читає resolv.conf. Знайте свій стек.
Стратегія C: Запустіть локальний DNS-форвардер, доступний контейнерам, і вкажіть Docker на нього
Якщо у вас split DNS, часті зміни VPN або «хост бачить внутрішні резолвери, а контейнери — ні», локальний форвардер — нудне, але правильне рішення. Форвардер слухає на адресі, доступній із контейнерів (часто gateway Docker-bridge) і пересилає до systemd-resolved або upstream.
Коли застосовувати: якщо IP резолверів часто змінюються або внутрішні резолвери доступні лише з неймспейсу хоста.
Практичний патерн: запустіть dnsmasq або unbound на хості, що слухає на 172.18.0.1 (gateway Docker-bridge) і форвардить на upstream systemd-resolved. Потім вкажіть Docker демону "dns": ["172.18.0.1"].
Чому це тримається: Docker отримує стабільну IP для DNS. Форвардер може відслідковувати зміни systemd-resolved або перезавантажуватися при зміні лінків.
Стратегія D: Використовуйте DNS-override на рівні Compose/сервісів економно
Compose дозволяє вказати dns, dns_search, dns_opt для сервісу. Це корисно для винятків, але не є стратегією для всього парку. Ви через півроку забудете про спецвипадок і знайдете його знову під час інциденту.
Коли використовувати: одну службу потрібно тимчасово спрямувати на інший резолвер або під час міграції.
Стратегія E: Host network mode для навантажень, чутливих до DNS (останній засіб)
Так, --network host вирішує багато DNS-проблем, бо контейнер ділить неймспейс хоста. Але він також прибирає мережеву ізоляцію і збільшує blast radius. Це підходить для дебаг-контейнера. У продакшені — останній засіб, якщо нема вагомих причин.
Другий жарт: використання host-networking для вирішення DNS — це як латання протікаючого крану шляхом демонтажу всієї сантехніки. Технічно витоків більше немає.
Розділений DNS, VPN і корпоративні мережі
Split DNS — це місце, де народжуються більшість тикетів «на хості працює, а в Docker — ні». systemd-resolved може маршрутизувати запити за доменними суфіксами на конкретні лінки і сервери. Модель Docker простіша: контейнери отримують resolv.conf зі списком nameserver-ів та опціональними search-доменами. У resolv.conf немає нативної семантики «відправляй corp.example резолверу A, а все інше резолверу B» поза порядком серверів і списком search.
Що відбувається на практиці:
- Ваш хост резолвить
git.corp.example, бо systemd-resolved знає, що цей домен належить VPN-інтерфейсу. - Ваш контейнер використовує
10.20.30.40, бо Docker захопив «глобальний» стан під час старту, а він не знає про резолвер VPN, що з’явився пізніше. - Ви захардкодили публічний резолвер у Docker, і тепер внутрішні імена ніколи не резолвяться.
У такому світі «правильна» відповідь зазвичай — запустити DNS-форвардер на хості, який робить split routing, і змусити контейнери питати цього форвардера. systemd-resolved може бути «мозком», але контейнери не повинні безпечно питати stub на 127.0.0.53. Отже або:
- Виставити resolved на ненульовий адресу (не loopback) — мені це не надто подобається, або
- Використовувати форвардер, який питає resolved через upstream-файл або D-Bus-інтеграцію, або
- Запхати ті ж внутрішні резолвери в Docker і погодитись, що, можливо, доведеться перезапускати Docker при зміні VPN.
Для ноутбуків «перезапуск Docker при підключенні VPN» — не елегантно, але чесно. Автоматизуйте це через NetworkManager dispatcher-скрипт, якщо потрібно. Для серверів віддавайте перевагу стабільним резолверам або локальному форвардеру.
Три короткі історії з корпоративного світу
1) Інцидент через хибне припущення: «127.0.0.53 завжди резолвер хоста»
Середня SaaS-компанія мала флот Ubuntu-хостів з Docker. Платформна команда стандартизувала systemd-resolved, бо він добре працював з VPN-клієнтами і тримав стан резолверів. Хтось помітив, що /etc/resolv.conf вказує на 127.0.0.53 і впевнено вирішив, що всі локальні процеси — включно з контейнерами — повинні його використовувати.
Вони побудували базовий імідж з healthcheck: виконати getent hosts api.corp.example. У staging це проходило час від часу — вони інтерпретували це як «DNS нестабільний». Підвищили таймаути. Додали повтори. Звинувачували DNS-команду. Класика.
Інцидент у продакшені прийшов у понеділок вранці: кілька сервісів не могли підключитись до внутрішніх API. Зовнішній DNS іноді працював (дякувати кешуванню і деяким сервісам, що використовують прямі IP). Внутрішні імена стабільно падали. Перші респондери перезапускали контейнери і бачили, що частина відновлюється, що гірше, ніж послідовний збій: це натякало, ніби проблема в додатку.
Виправлення зайняло 20 хвилин після того, як хтось подивився /etc/resolv.conf всередині контейнера і зрозумів, що там 127.0.0.53. В неймспейсі контейнера це loopback. Нікого не було, хто слухав порт 53. Вони оновили DNS-налаштування демона Docker на upstream-рішення і покатили рестарти. Постмортем був болючий, але корисний: припущення про loopback-адреси не переживають неймспейси.
2) Оптимізація, що повернулась бумерангом: «захардкодимо публічні резолвери, щоб зменшити латентність»
Велика корпоративна команда мала розробницькі робочі станції з налаштуваннями Docker Desktop-подібними на Linux. Їх внутрішні DNS-сервери інколи були повільні під навантаженням. Добросердечний інженер запропонував «оптимізацію»: встановити DNS демона Docker на публічні резолвери, щоб пришвидшити вирішення зовнішніх залежностей (репозиторії пакетів, реєстри контейнерів).
Тиждень виглядав як перемога. Збирання трохи швидші. Люди перестали жалітись на повільні apt resolves. Потім запустили новий внутрішній сервіс з іменем, що існує лише в внутрішньому DNS. Контейнери почали падати при резолюванні цього імені, тоді як хост міг його вирішити.
Дебаг став взаємним перекладом відповідальності між командами додатків і інфра. Зрештою хтось помітив, що демон Docker був прикріплений до публічного DNS, обходячи split DNS повністю. Оптимізація тихо забрала доступ до внутрішнього резолювання для всіх контейнерів і в деяких місцях порушила політику щодо логування DNS і контролю витоку даних.
Відкат був простим: повернули DNS Docker на внутрішні резолвери і додали локальний кешуючий форвардер, щоб зменшити латентність без обходу корпоративних контролів. Урок практичний: обирати «швидкий» резолвер — це пастка, якщо організація використовує DNS для маршрутизації та політик.
3) Нудна, але правильна практика, що врятувала день: «уніфіковані DNS-тести в CI і на хостах»
Платіжна компанія постраждала від періодичних DNS-збоїв під час попередньої міграції. Вони відповіли нудним планом: на кожному вузлі був локальний діагностичний імідж, і кожен пайплайн деплою запускав набір тестів DNS до і після релізу.
Набір був простим: перевірити, чи контейнери можуть вирішити публічне ім’я, внутрішнє ім’я і ім’я сервісу в Docker-мережі. Також перевіряли наявність 127.0.0.53 у контейнерному resolv.conf — бо вони вже бачили цей фільм. Пайплайн швидко падав, якщо щось було не так.
Через шість місяців оновлення дистрибутива змінило, як /etc/resolv.conf керується в новому базовому іміджі для нод. На половині нових нод Docker почав підсовувати 127.0.0.53 у контейнери. Тести зупинили це до масового розгортання. Інцидент не став катастрофою: невелика партія нод не увійшла в сервіс, поки не була виправлена.
Це не гламурна інженерія. Але це та, яка дозволяє спати спокійно. Уніфіковані діагностичні тести не запобігають усім відмовам, але зупиняють тупі повторення помилок.
Поширені помилки: симптом → корінна причина → виправлення
1) «Контейнери не можуть нічого вирішити; хост — може»
Симптом: getent hosts example.com падає в контейнері; на хості працює.
Корінна причина: У /etc/resolv.conf контейнера записано nameserver 127.0.0.53, успадкований від systemd-resolved stub.
Виправлення: Налаштуйте Docker daemon DNS на реальні upstream-сервери або перенаправте /etc/resolv.conf хоста на /run/systemd/resolve/resolv.conf і перезапустіть Docker.
2) «Зовнішній DNS працює, внутрішні домени падають (особливо після підключення VPN)»
Симптом: example.com вирішується; git.corp.example падає в контейнерах.
Корінна причина: Docker захопив DNS-сервери до того, як VPN додав split DNS; контейнери не бачать резолверів, які надає VPN.
Виправлення: Використовуйте локальний DNS-форвардер і вкажіть Docker на нього, або перезапускайте Docker при зміні стану VPN (і приймайте зупинку), або явно вкажіть внутрішні DNS-сервери в конфіг Docker.
3) «Переривчасті таймаути DNS; повтори допомагають»
Симптом: Деякі запити таймаутяться, потім вдаються; логи показують сплески помилок DNS.
Корінна причина: UDP-фрагментація/проблеми MTU через VPN; EDNS0-відповіді відкидаються; брандмауер блокує великі UDP-пакети DNS.
Виправлення: Тестуйте TCP-запити; відрегулюйте MTU; розгляньте відключення EDNS0 для конкретних шляхів; або запустіть локальний форвардер, який коректно працює по TCP upstream.
4) «Серіс-дискавері всередині Docker-мережі не працює»
Симптом: Контейнер A не може вирішити контейнер B по імені в користувацькій мережі.
Корінна причина: Ланцюг вбудованого DNS зламаний, або контейнер на дефолтному bridge без вбудованих DNS-семантик, або конфліктуючий --dns вимикає очікування сервіс-дискавері залежно від налаштувань.
Виправлення: Використовуйте користувацький bridge; уникайте перезапису DNS на контейнері, якщо не потрібно; перевірте стан драйвера Docker мережі; інспектуйте iptables.
5) «Запити повільні; додаток пікає CPU під час DNS»
Симптом: Зростає латентність; потоки додатка блокуються на DNS; високий обсяг запитів.
Корінна причина: Надмірний список search-доменів + велике ndots, що спричиняє множинні запити на один lookup; додаток використовує короткі імена.
Виправлення: Скоротіть search-домени; встановіть відповідне ndots; використовуйте FQDN у конфігурації; розгляньте локальний кешуючий резолвер.
6) «Ми змінили DNS у Docker і деякі додатки все ще використовують старі резолвери»
Симптом: Після зміни daemon.json деякі контейнери все ще мають старий resolv.conf.
Корінна причина: Існуючі контейнери зберігають згенерований resolv.conf, доки їх не пересоздадуть/не перезапустять (залежно від поведінки runtime та bind-mount-ів).
Виправлення: Перезапустіть/пересоздайте контейнери; переконайтесь, що ви ненавмисно не змонтували /etc/resolv.conf; перевірте через docker exec cat /etc/resolv.conf.
Чеклисти / покроковий план
Чеклист 1: Підтвердіть режим відмови за 5 хвилин
- З хоста: вирішіть проблемне ім’я через
getent hostsіdig, щоб підтвердити стан хоста. - З контейнера:
cat /etc/resolv.confі пошукайте127.0.0.53або підозрілі search/ndots. - З контейнера: опитайте відомий резолвер безпосередньо через
dig @IP, щоб відокремити «шлях DNS» від «конфігу DNS». - З хоста: запустіть
resolvectl status, щоб знайти фактичні upstream-сервери і split DNS-домени. - Вирішіть: потрібно статичне DNS (сервери) чи динамічна/split-поведінка (форвардер)?
Чеклист 2: Застосуйте стійке виправлення на серверах (рекомендований шлях)
- Оберіть DNS-сервери, що досяжні з мереж контейнерів (зазвичай внутрішні резолвери).
- Встановіть Docker daemon
dnsіdns-searchу/etc/docker/daemon.json. - Перезапустіть Docker у вікні технічного обслуговування.
- Перезапустіть/пересоздайте контейнери, щоб вони підхопили зміни.
- Запустіть невеликий DNS-тест-контейнер у CI/CD і на вузлі як readiness-gate.
Чеклист 3: Застосуйте стійке виправлення на ноутбуках з частими VPN
- Покиньте спроби утримувати Docker синхронізованим з per-link станом systemd-resolved вручну.
- Запустіть локальний DNS-форвардер, що слухає адресу, доступну контейнерам (gateway bridge або IP хоста).
- Вкажіть Docker daemon DNS на цей форвардер.
- За потреби перезавантажуйте форвардер при підключенні/відключенні VPN.
- Тримайте під рукою один діагностичний контейнер і тестуйте внутрішню + зовнішню резолюцію після змін мережі.
Чеклист 4: Управління змінами, що запобігає повторним інцидентам
- Кодуйте конфігурацію DNS Docker (daemon.json) через CM.
- Моніторте несподівані зміни цілі симлінку
/etc/resolv.conf. - Документуйте, чи покладаєтесь на split DNS і які внутрішні домени важливі.
- Додайте невеликий канарковий тест, що резолвить одне внутрішнє і одне зовнішнє ім’я з контейнера на кожному вузлі.
- При оновленні базових іміджів перевіряйте поведінку systemd-resolved перед широким розгортанням.
Поширені питання (FAQ)
Чому 127.0.0.53 працює на хості, але не в контейнерах?
Тому що контейнери працюють у власному мережевому неймспейсі. Loopback — це per-namespace. 127.0.0.53 всередині контейнера вказує на сам контейнер, а не на systemd-resolved на хості.
Чому деякі контейнери показують 127.0.0.11 замість цього?
Це вбудований DNS Docker, який зазвичай використовується в користувацьких bridge-мережах. Він забезпечує розв’язування імен контейнерів і пересилає зовнішні запити upstream.
Якщо Docker використовує вбудований DNS, навіщо мені systemd-resolved?
Бо вбудований DNS — це форвардер. Йому все одно потрібні правильні upstream. Якщо Docker захопив погані upstream-налаштування (як stub-адресу) або застарілі (до VPN), ви все одно втрачаєте резолюцію.
Чи варто вимикати systemd-resolved, щоб виправити Docker DNS?
Зазвичай ні. systemd-resolved працює добре; проблема в тому, що його stub-конфіг експортують у контейнери. Краще налаштувати DNS Docker явно або використати upstream-файл resolv.conf. Вимикання resolved може створити нові проблеми, особливо зі split DNS і сучасними network manager-ами.
Чи безпечно перенаправляти /etc/resolv.conf?
Може бути безпечно, але це змінює поведінку хоста. На деяких системах інструменти очікують stub-конфіг. Якщо робите це, перевірте поведінку хоста і робіть це як керовану зміну, а не як імпровізацію о 2:00 ранку.
Чи потрібно перезапускати Docker після зміни DNS-налаштувань?
Так, для змін на рівні демона. І зазвичай потрібно перезапустити або пересоздати контейнери, щоб вони підхопили новий resolv.conf.
Чому DNS ламається тільки після підключення VPN?
Бо VPN часто додає DNS-сервери і search-домені динамічно. systemd-resolved оновлює per-link конфіг, але Docker не завжди автоматично переналаштовує запущені контейнери або не оновлює upstream-налаштування без перезапуску.
Чи можна просто вказати Docker DNS на публічний резолвер і забути?
У корпоративному середовищі це зазвичай ламає внутрішнє резолювання і може порушити політику. Навіть поза корпоративною мережею це може приховати реальну проблему (наприклад MTU) і створити нові режими відмови.
Як зрозуміти, чи маю я проблеми MTU/фрагментації для DNS?
Симптоми — переривчасті таймаути, особливо для записів з великими відповідями. Використайте dig і порівняйте поведінку UDP проти TCP. Якщо TCP стабільно працює, а UDP таймаутиться — підозрюйте MTU/фрагментацію або брандмауер.
Який найнадійніший підхід «встановив і забув»?
На серверах: налаштуйте Docker daemon DNS на відомі внутрішні резолвери (або локальний форвардер) і тримайте це в керованій конфігурації. На ноутбуках з VPN: локальний форвардер + вказаний Docker на нього зазвичай найкраще підходить.
Висновок: наступні кроки, які можна виконати сьогодні
Загадки Docker DNS часто — просто нашарування неймспейсів і systemd-resolved. Виправлення — перестати дозволяти контейнерам успадковувати stub-адресу loopback, і вибрати стабільне джерело істини для upstream DNS.
- Запустіть план швидкої діагностики і підтвердіть, чи просочується
127.0.0.53в контейнери. - Оберіть стратегію: конфігурація на рівні демона (найпоширеніша), перенаправлення
/etc/resolv.confна upstream-файл (акуратно) або додавання локального форвардера для split DNS і VPN-чурну. - Закріпіть зміни: керуйте конфігом, перезапускайте Docker усвідомлено і додайте контейнерний DNS-тест як гейт, щоб цей клас відмов більше не повторювався.
Якщо зробите лише одну річ: перед перезапуском чогось подивіться у /etc/resolv.conf всередині проблемного контейнера. Дивовижно, як часто саме цей файл пояснює весь інцидент.