Unbound в Docker Compose: готовый файл
Pi-hole и AdGuard Home режут рекламу, но сами по себе не резолвят домены — они пересылают запросы дальше, обычно на публичный DNS вроде Cloudflare или Google. То есть весь список сайтов, которые вы посещаете, всё равно уходит третьей стороне, просто уже без рекламных доменов в списке. Unbound закрывает этот пробел: это полноценный рекурсивный резолвер, который сам обходит корневые серверы и получает ответ из первых рук, без посредника, да ещё и с проверкой DNSSEC на каждом шаге. Ниже — рабочий docker-compose.yml и конфиг, с которым Unbound встаёт за час и без сюрпризов.
Содержание
Обсудить статью, задать вопрос или начать новую тему
Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.
Перейти в сообщество →Зачем ставить Unbound рядом с Pi-hole или AdGuard Home
Разница между «переслать запрос на 1.1.1.1» и «резолвить рекурсивно самому» на первый взгляд незаметна — пользователь получает тот же IP-адрес в ответ. Но механика за кулисами разная, и она имеет значение для двух вещей: приватности и доверия к ответу.
Публичный резолвер видит все ваши DNS-запросы целиком — независимо от того, что говорится в его политике конфиденциальности, технически он в состоянии сопоставить IP клиента (или хотя бы IP вашего Pi-hole/AdGuard-сервера) со списком посещённых доменов. Unbound вместо этого сам идёт к корневым DNS-серверам, потом к серверам зоны .com/.ru/что угодно, потом к авторитетному серверу конкретного домена — и каждый из этих серверов видит только часть картины, а не весь ваш browsing history разом.
Второй момент — DNSSEC. Публичные резолверы обычно валидируют DNSSEC, но вы им доверяете на слово. Unbound проверяет цепочку подписей сам, локально, и если где-то на пути подмена или испорченная зона — вернёт SERVFAIL вместо тихого редиректа на поддельный адрес.
| Вариант upstream | Приватность | DNSSEC-валидация | Скорость первого запроса |
|---|---|---|---|
| Публичный резолвер (Cloudflare, Google, Quad9) | Третья сторона видит все запросы | Есть, но «на слово» | Быстрее — у них горячий кэш миллионов пользователей |
| Unbound (рекурсивный, локальный) | Никто не видит полную картину сразу | Проверяется локально, честный SERVFAIL при подделке | Чуть медленнее на холодном кэше, дальше кэшируется сам |
Практический вывод: Unbound имеет смысл, если приватность DNS для вас не абстракция, а конкретное требование. Первый запрос к незнакомому домену иногда займёт на десятки-сотни миллисекунд больше, чем у прогретого публичного резолвера — заметной для человека эта разница почти никогда не бывает.
Что понадобится перед стартом
- VPS с 1 vCPU и 1 ГБ RAM хватает с запасом — Unbound написан на C и по требованиям к ресурсам близок к Pi-hole, а не к тяжёлым базам данных;
- установленный Docker и Docker Compose (если ещё не ставили — есть отдельный разбор установки Docker на Ubuntu 24.04);
- уже поднятый (или планируемый) Pi-hole/AdGuard Home на этом же сервере — Unbound в этой схеме почти всегда работает не сам по себе, а как upstream-резолвер для них: Pi-hole в Docker Compose и AdGuard Home в Docker Compose;
- WireGuard, если хотите пользоваться этой связкой не только дома — детали в статье про установку WireGuard на Ubuntu 24.04.
Проверьте занятость портов, которые собираетесь использовать:
sudo ss -tulpn | grep -E ':53|:5335'
Unbound в этой схеме слушает не 53-й порт, а 5335 — так он не конфликтует ни с системным systemd-resolved, ни с Pi-hole/AdGuard, которые как раз и занимают 53-й для клиентских устройств.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверГотовый docker-compose.yml и unbound.conf
Рабочая директория и структура:
mkdir -p ~/unbound/conf.d && cd ~/unbound
Файл docker-compose.yml:
services:
unbound:
container_name: unbound
image: mvance/unbound:latest
hostname: unbound
restart: unless-stopped
volumes:
- ./unbound.conf:/opt/unbound/etc/unbound/unbound.conf:ro
- ./conf.d:/opt/unbound/etc/unbound/unbound.conf.d:ro
- unbound-cache:/opt/unbound/etc/unbound/cache
ports:
# адрес docker-сети или интерфейса WireGuard — не публичный IP сервера
- "172.28.0.1:5335:53/tcp"
- "172.28.0.1:5335:53/udp"
cap_add:
- NET_BIND_SERVICE
networks:
dns_net:
ipv4_address: 172.28.0.10
networks:
dns_net:
driver: bridge
ipam:
config:
- subnet: 172.28.0.0/24
volumes:
unbound-cache:
Образ mvance/unbound — сторонний, но давно и широко используется в такой связке именно для DNSSEC-валидации; перед продакшен-использованием стоит свериться с актуальным тегом и Dockerfile на Docker Hub, а не полагаться на latest вслепую в долгосрочной эксплуатации. Альтернатива с похожей философией — klutchell/unbound.
Минимальный, но не игрушечный unbound.conf:
server:
verbosity: 1
interface: 0.0.0.0
port: 53
do-ip4: yes
do-ip6: no
do-udp: yes
do-tcp: yes
# кто вообще имеет право спрашивать
access-control: 127.0.0.1/32 allow
access-control: 172.28.0.0/24 allow
access-control: 0.0.0.0/0 refuse
# не светить версию и вообще то, что это Unbound
hide-identity: yes
hide-version: yes
# DNSSEC и качество кэша
harden-glue: yes
harden-dnssec-stripped: yes
harden-below-nxdomain: yes
use-caps-for-id: yes
qname-minimisation: yes
aggressive-nsec: yes
prefetch: yes
prefetch-key: yes
serve-expired: yes
cache-min-ttl: 300
cache-max-ttl: 86400
rrset-roundrobin: yes
# приватные диапазоны не должны резолвиться наружу
private-address: 10.0.0.0/8
private-address: 172.16.0.0/12
private-address: 192.168.0.0/16
num-threads: 1
msg-cache-size: 32m
rrset-cache-size: 64m
Ключевая строка — access-control. Она определяет, кто вообще имеет право задавать вопросы этому Unbound: только localhost внутри контейнера и подсеть dns_net, в которой сидит Pi-hole/AdGuard. Всё остальное — refuse. Порт наружу при этом всё равно замаппен на адрес docker-сети или VPN-интерфейса, а не на 0.0.0.0, — это второй, независимый уровень той же защиты.
Про hostname, restart и вынос томов наружу контейнера — те же соображения, что и в других compose-файлах этой серии: без bind-mount обновление образа mvance/unbound:latest каждый раз сбрасывало бы конфиг к дефолтному, что для рекурсивного резолвера особенно неприятно, если в конфиг были внесены правки под конкретную сеть.
DNSSEC-валидация: как работает и как проверить
Корневой сервис доверия DNSSEC — цепочка криптографических подписей от корневой зоны до конкретного домена. Образ mvance/unbound управляет этим сам: при старте контейнера актуализирует root hints и trust anchor через встроенные механизмы, отдельно вручную это обычно трогать не нужно. Если хотите убедиться, что якорь доверия на месте, — загляните в логи запуска контейнера, там видно инициализацию.
Проверка того, что валидация реально работает, — классический трюк с заведомо битой DNSSEC-подписью на тестовом домене:
dig dnssec-failed.org @172.28.0.10 -p 5335
Если Unbound валидирует честно, ответ — SERVFAIL, потому что подпись у этого домена намеренно испорчена в учебных целях. А обычный домен с корректным DNSSEC резолвится как ни в чём не бывало:
dig isc.org +dnssec @172.28.0.10 -p 5335
В выводе должен быть флаг ad (authenticated data) в секции flags — это признак того, что ответ прошёл валидацию, а не просто был возвращён как есть.
Подключаем Unbound как upstream к Pi-hole или AdGuard Home
Смысл всей связки — в том, чтобы Pi-hole/AdGuard продолжали заниматься блокировкой рекламы и раздачей DNS клиентам, а вместо публичного резолвера вида 1.1.1.1 спрашивали именно ваш Unbound.
Если оба сервиса в одном docker-compose.yml (проще всего объединить их в один файл и одну сеть dns_net) — для Pi-hole в переменных окружения меняется:
environment:
PIHOLE_DNS_: "172.28.0.10#5335"
Для AdGuard Home то же самое настраивается не через переменные окружения, а через веб-интерфейс: Settings → DNS settings → Upstream DNS servers, где вместо адресов публичных резолверов указывается:
[/#/]172.28.0.10:5335
Оба контейнера должны быть в одной docker-сети dns_net, чтобы обращение по внутреннему IP работало без лишних ухищрений. Если Pi-hole/AdGuard и Unbound подняты как раздельные стеки (разные docker-compose.yml в разных директориях), их можно соединить через external: true сеть — тогда в файле Unbound сеть объявляется как обычно, а в файле Pi-hole/AdGuard она подключается как внешняя с тем же именем.
После смены upstream проверьте резолв напрямую через Pi-hole/AdGuard, а не только через Unbound — так вы убедитесь, что вся цепочка «клиент → блокировщик → Unbound → интернет» работает целиком:
dig google.com @<IP-адрес-pihole-или-adguard>
Тюнинг: кэш, приватность и безопасность конфигурации
Несколько параметров стоит подстроить под свою нагрузку и требования, а не оставлять как есть из примера выше.
Размер кэша. msg-cache-size и rrset-cache-size в примере занижены под самый скромный VPS. На сервере с 2+ ГБ RAM, обслуживающем десятки клиентов, разумно поднять до 128m и 256m соответственно.
serve-expired: yes — Unbound отдаёт устаревшую запись из кэша, пока в фоне обновляет её, если авторитетный сервер вдруг недоступен или медленно отвечает. На стабильность работы клиентов это влияет заметно сильнее, чем кажется по описанию: без этой опции временная недоступность одного авторитетного сервера превращается в заметную задержку резолва для всех, кто в этот момент спрашивает соответствующий домен.
Логирование запросов. По умолчанию в конфиге выше verbosity: 1 — минимум шума. Для отладки временно можно поднять до 2 и добавить log-queries: yes, но держать это включённым постоянно не стоит: вырастает объём логов, и, что важнее в контексте всей затеи с приватным DNS, на диске появляется тот самый полный журнал посещённых доменов, от хранения которого вы отчасти и уходили, отказываясь от публичного резолвера.
Изоляция контейнера. Unbound резолвит на весь интернет — теоретическая поверхность атаки чуть шире, чем у сервиса без сетевого доступа наружу. Общие принципы разграничения контейнеров друг от друга и от хоста разобраны в статье про изоляцию сервисов через Docker — тот же подход применим и здесь: не давать контейнеру лишних прав сверх NET_BIND_SERVICE, не монтировать в него ничего, кроме нужных конфигов и тома кэша.
Health-check. Compose из примера выше без него — контейнер может тихо перестать отвечать на DNS-запросы, оставаясь при этом «зелёным» в docker ps. Базовая проверка через dig внутри самого контейнера закрывает этот пробел, шаблон настройки описан в статье про Docker healthcheck.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверНужны сами нейросети для контента?
Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.
Частые вопросы
Unbound может полностью заменить Pi-hole или AdGuard Home?
Нет, у них разные задачи. Unbound резолвит рекурсивно и валидирует DNSSEC, но не занимается блокировкой рекламы и трекеров по спискам — для этого нужен именно Pi-hole/AdGuard, а Unbound встаёт у них upstream'ом.
Почему Unbound слушает порт 5335, а не стандартный 53?
Чтобы не конфликтовать с Pi-hole/AdGuard, которые как раз и занимают 53-й порт для обслуживания клиентских устройств. Порт 5335 — просто внутреннее соглашение между сервисами в одной docker-сети, снаружи он вообще не должен быть доступен.
Резолв через Unbound заметно медленнее публичного DNS?
На холодном кэше — да, чуть заметнее, потому что Unbound идёт по полной цепочке до авторитетного сервера вместо обращения к уже прогретому кэшу крупного публичного резолвера. После прогрева собственного кэша разница на практике почти стирается для повторных запросов к одним и тем же доменам.
Нужно ли вручную обновлять root hints и trust anchor?
В образе mvance/unbound это делается автоматически при старте контейнера. Если используете другой образ или собираете свой — проверьте, что в нём есть аналогичный механизм, иначе трастовый якорь со временем устареет и DNSSEC-валидация начнёт давать сбои.
Можно ли добавить в Unbound локальные DNS-записи для своей сети (условный nas.home)?
Да, через директиву local-data в отдельном файле, который подключается через conf.d — например, local-data: "nas.home. IN A 192.168.1.50". Такие записи резолвятся сразу, без обращения наружу.
Что будет, если контейнер с Unbound упадёт?
Pi-hole/AdGuard потеряют upstream и начнут отвечать SERVFAIL на всё, что нет в их собственном кэше. Разумно указать резервный upstream (например, публичный DNS-over-TLS резолвер) вторым пунктом в настройках Pi-hole/AdGuard — на случай, пока restart: unless-stopped поднимает Unbound обратно.
Обсудить статью, задать вопрос или начать новую тему
Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.
Перейти в сообщество →