
Собираем docker образ DNS сервера Unbound
Приветствую!
В этой заметке мы соберем свой Docker образ с современным DNS сервером — Unbound. Рассмотрим подробно Dockerfile, а также мой скрипт подготовки окружения контейнера.
Подписывайтесь на наш телеграм @r4ven_me📱, чтобы не пропустить новые публикации на сайте😉. А если есть вопросы или желание пообщаться по тематике — заглядывайте в Вороний чат @r4ven_me_chat🧐. |
По сути это сказ о том, как я собирал контейнер🐳 для своей предыдущей статьи: Поднимаем свой DNS сервер Unbound и блокировщик рекламы Pihole в docker.
Сегодня нам понадобятся📑:
- 1️⃣Установленный и настроенный сервер на базе Linux;
- 2️⃣Доступ к серверу, например по SSH;
- 3️⃣Установленный Docker engine на сервере;
- 4️⃣Учетная запись с правами sudo!
Если все готово, то подключаемся к серверу и приступаем🏃.
Скачивание и обзор файлов проекта
Демонстрацию из статьи я буду проводить в среде дистрибутива Debian 12. Всё +- аналогичным образом может быть воспроизведено и в других системах на базе Linux🙂.
Для скачивания файлов проекта нам понадобится утилита git
из одноименной системы контроля версий. Если она у вас еще не установлена, то ставим:
sudo apt update && sudo apt install -y git
Теперь клонируем репозиторий с файлами для сборки в папку /opt
:
sudo git clone https://github.com/r4ven-me/unbound /opt/unbound
Давайте подробнее рассмотрим состав проекта (ссылки ведут на GitHub):
- docker-compose.yml — пример compose файла для запуска собранного (см.
src
) контейнера с Unbound черезdocker compose
; - unbound.service — пример systemd unit-файла, который позволяет запускать Unbound в Docker от имени сервисного пользователя через
sudo
; - unbound_sudoers — пример файла для
/etc/sudoers.d/
, который разрешает сервисному пользователю unbound запускать (docker compose up/down
) контейнер с unbound без пароля;
src — директория с исходными файлами сборки образа Unbound:
- Dockerfile — сценарий для сборки образа Docker, содержащего Unbound с необходимыми конфигурациями и зависимостями;
- unbound.sh — скрипт для запуска и управления службой Unbound;
- unbound.conf — основной конфигурационный файл для настройки параметров работы DNS сервера;
- root.hints — файл с перечнем корневых DNS-серверов, используемый Unbound для инициализации процесса разрешения имен;
- root.key — ключ для DNSSEC, используемый для проверки подлинности ответов от корневых серверов;
- forward-records.conf — пример конфигурационного файла, определяющего правила пересылки DNS-запросов к другим серверам;
- a-records.conf — пример файла конфигурации, содержащего A-записи для сопоставления доменных имен с IP-адресами;
- srv-records.conf – пример файла конфигурации, содержащего SRV-записи, которые указывают на расположение служб в сети;
- check.sh — скрипт для расчета рекомендуемых значений некоторых параметров в
unbound.conf
;
Подробнее остановимся на файлах сценарии сборки и скрипте запуска Unbound внутри контейнера (кликайте на спойлер):
Спасибо dafanasiev за твой полезный merge👍
# базовый образ - облегченный Debian 12
FROM debian:12-slim
# подавление интерактивных запросов при установке пакетов
ENV DEBIAN_FRONTEND=noninteractive
# метаданные
LABEL maintainer="Ivan Cherniy <kar-kar@r4ven.me>"
# дизейбл автоматической очистки apt-кеша в Docker
RUN rm -f /etc/apt/apt.conf.d/docker-clean; \
echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /etc/apt/apt.conf.d/keep-cache
# установка пакетов и настройка Unbound
RUN --mount=type=bind,target=/src,source=./ \ # монтирование исходников
--mount=type=cache,target=/var/cache/apt,sharing=locked \ # кеш пакетов
--mount=type=cache,target=/var/lib/apt,sharing=locked \ # кеш метаданных
--mount=type=tmpfs,target=/var/log \ # логи в память
--mount=type=tmpfs,target=/var/tmp \ # временные файлы в память
--mount=type=tmpfs,target=/var/cache/debconf \ # конфиги установки в память
--mount=type=tmpfs,target=/run \ # runtime файлы в память
--mount=type=tmpfs,target=/tmp \ # временные файлы в память
set -x && \ # включение режима отладки оболочки
apt update && \ # обновление кэша apt
# установка без рекомендуемых зависимостей
apt install --yes --no-install-recommends --no-install-suggests \
tini \ # миниатюрный init-процесс для контейнеров
unbound \ # сам DNS-сервер
unbound-anchor \ # утилита для управления корневыми ключами DNSSEC
iproute2 \ # сетевые утилиты (ip, ss и др.)
iputils-ping \ # утилита ping
ldnsutils \ # утилиты для работы с DNS (drill и др.)
bc \ # калькулятор для скриптов
less \ # pager для просмотра файлов
ca-certificates \ # SSL-сертификаты доверенных центров
curl && \ # утилита для взаимодействия с веб
# изменение UID/GID пользователя unbound
usermod -u 14956 unbound && \
groupmod -g 14956 unbound && \
# инициализация корневого ключа DNSSEC
{ unbound-anchor -a /etc/unbound/root.key; true; } && \
# загрузка списка корневых DNS-серверов от InterNIC
curl -sSL https://www.internic.net/domain/named.cache > /etc/unbound/root.hints && \
# копирование конфигов
cp /src/*.conf /etc/unbound/ && \
cp /src/*.conf /usr/share/doc/unbound/ && \
cp /src/unbound.sh /src/check.sh / && \
# копирование корневых файлов
cp /etc/unbound/root.key /etc/unbound/root.hints /usr/share/doc/unbound/ && \
# изменение владельца конфигов
chown -R unbound:unbound /etc/unbound/ && \
# очистка лишних пакетов
apt purge --yes --auto-remove
# открытие портов DNS
EXPOSE 53/tcp \
53/udp
# проверка здоровья контейнера - тестовый DNS-запрос
HEALTHCHECK --interval=5m --timeout=20s --start-period=20s \
CMD drill @127.0.0.1 opennameserver.org > /dev/null || exit 1
# скрипт-точка входа
ENTRYPOINT ["/unbound.sh"]
# команда по умолчанию: запуск Unbound через tini
CMD ["/usr/bin/tini", "--", "/usr/sbin/unbound", "-d", "-p", "-c", "/etc/unbound/unbound.conf"]
Dockerfile — инструкция📒, по которой docker собирает образ на базе Debian 12 (Bullseye), ставит туда unbound
версии 1.17.1, создаёт пользователя unbound
, копирует конфиги и скрипты в контейнер, а в CMD
запускает unbound.sh
, который уже всё настраивает и запускает сам DNS-сервер.
Если вам необходима более свежая версия Unbound, используйте в качестве базового образа Debian Sid. Ну или изучите Dockerfile из этого GitHub репозитория — там Unbound собирается из исходников на этапе сборки Docker образа🐧.
#!/bin/bash
### ПОДГОТОВКА ###
WORK_DIR="/etc/unbound" # рабочая папка unbound
ROOT_HINTS_URL="https://www.internic.net/domain/named.cache" # ссылка на корневые сервера
CONF_LIST=("unbound.conf" "forward-records.conf" "srv-records.conf" "a-records.conf") # список конфигов
# копируем конфиги из шаблонов, если отсутствуют в рабочей папке
for config in "${CONF_LIST[@]}"; do
if [[ ! -f "${WORK_DIR}"/"${config}" ]]; then
cp /usr/share/doc/unbound/"${config}" "${WORK_DIR}"
fi
done
# работа с root.key (ключ DNSSEC)
if [[ -f "${WORK_DIR}"/root.key ]]; then
# делаем бэкап перед обновлением
cp "${WORK_DIR}"/root.key{,_backup}
# пробуем обновить, если не получится - восстанавливаем бэкап
if ! unbound-anchor -a "${WORK_DIR}"/root.key; then
mv "${WORK_DIR}"/root.key{_backup,}
else
# если обновилось - удаляем бэкап
rm -f "${WORK_DIR}"/root.key_backup
fi
else
# если файла вообще нет - копируем из шаблонов
cp /usr/share/doc/unbound/root.key "$WORK_DIR"
fi
# работа с root.hints (список корневых серверов)
if [[ -f "${WORK_DIR}"/root.hints ]]; then
# аналогично делаем бэкап
cp "${WORK_DIR}"/root.hints{,_backup}
# пробуем скачать свежий список
if ! curl -sSL "$ROOT_HINTS_URL" > "${WORK_DIR}"/root.hints; then
# если не получилось - восстанавливаем
mv "${WORK_DIR}"/root.hints{_backup,}
else
# если ок - удаляем бэкап
rm -f "${WORK_DIR}"/root.hints_backup
fi
else
# если файла нет - копируем из шаблонов
cp /usr/share/doc/unbound/root.hints "$WORK_DIR"
fi
# фиксим права на рабочую папку
chown -R unbound:unbound "$WORK_DIR"
### ЗАПУСК UNBOUND ###
echo "Запускаем Unbound DNS-сервер..."
# выполняем переданные аргументы (команду из CMD)
exec "$@" || { echo "не удалось запустить :(" >&2; exit 1; }
Небольшой bash скрипт, который на старте проверяет, есть ли нужные конфиги, если нет — копирует из /usr/share/doc/unbound/
; обновляет корневой DNS-ключ (root.key
) и хинты (root.hints
), при этом делает резервные копии на случай неудачи; в конце запускает сам unbound
.
Потихоньку переходим к практике😉.
Сборка образа
Собираем наш образ с помощью вот такой команды:
sudo docker build --tag r4venme/unbound /opt/unbound/src/
Где r4venme/unbound
– произвольный тег образа. В данном случае я указал имя пользователя на Docker hub и через /
название репозитория.
По завершению проверяем:
sudo docker image ls
Образ готов👍.
Сборка под разные архитектуры
При необходимости вы можете собрать образ под разные архитектуры. Чаще всего это делается с последующим пушем в репозиторий образов, например hub.docker.com (требуется предварительная авторизация в репозитории через docker login
):
sudo docker buildx create --use
sudo docker buildx build \
--platform linux/amd64,linux/arm64,linux/arm/v7 \
--tag r4venme/unbound \
--push /opt/unbound/src
В команде выше собирается образ для платформ amd64, arm64, armv7.
Запуск и проверка Unbound в docker
Среди файлов проекта есть пример compose файла📁. Используем его для запуска контейнера на основе собранного образа🎛️.
⚠️Обратите внимание, что контейнер, описаннй в файле
docker-compose.yml
намеренно ограничен в аппаратных ресурсах (директиваdeploy
) на использованиеcpus: '0.70'
иmemory: 512M
, т.е максимально разрешенное использование CPU составляет 70% одного ядра и 512 мб RAM + резерв. А также явно заданы ограничения на хранения логов контейнеров: 5 файлов по 50 мб. При необходимости скорректируйте данные параметры в соответствии со своими потребностями. Подробнее про лимиты ресурсов сервисов при использовании docker compose читайте тут, а про логирование тут.
Стартуем контейнер в фоновом режиме (detach) и смотрим логи:
sudo docker compose --file /opt/unbound/docker-compose.yml up --detach
sudo docker logs --follow unbound
⚠️Для выхода из режима просмотра логов нажмите
Ctrl+c
.
Будут видны записи проверки healthcheck, которая раз в 5 мин. выполняет тестовый DNS запрос.
При необходимости можете отключить эту проверку в docker-compose.yml
добавив директиву к сервису:
healthcheck:
disable: true
Теперь давайте проверим работу DNS сервера не прерывая просмотр логов контейнера. Для этого открываем соседний терминал и устанавливаем утилиту для работы DNS — dig
:
sudo apt install -y dnsutils
Выполняем тестовый запрос к IP адресу контейнера (он явно задан в docker-compose.yml
):
dig @10.100.100.200 r4ven.me +short +identify
☝️Обратите внимание, что ответ на второй запрос пришел заметно быстрее — это результат работы механизма кэширования. По умолчанию он хранится в ОЗУ контейнера и сбрасывается при перезапуске. Для изменения такого поведения изучите и задайте соответствующие параметры в /opt/unbound/data/etc/unbound.conf
.
Если при запуске Unbound включить подробный режим логирования, то в выводе контейнера будут видны запросы к корневым DNS серверам при первичном резолвинге. Сделать это можно изменив команду запуска контейнера в compose файле:
command:
- /usr/sbin/unbound -vvv -d -p -c /etc/unbound/unbound.conf
Чаще всего это излишне, но иногда полезно для дебага👌.
Ну а как настроить обращения к DNS серверу в контейнере внешних клиентов я рассказывал в отдельном разделе статьи про Unbound+Pihole.
Расчет рекомендуемых значений для Unbound
Отдельно хотел рассказать про скрипт check.sh
, логику которого я позаимствовал из другого проекта по сборке Unbound (см. источники в конце).
При запуске без аргументов, на основе количества доступного ОЗУ и ядер процессора данный скрипт рассчитывает значения следующих параметров:
msg-cache-size
— объём памяти, выделенной для кэширования полных DNS-ответов (включает запрос и ответ);rrset-cache-size
— объём памяти для кэша Resource Record Sets (RRsets) — отдельных DNS-записей (A, MX, NS и т.д.);num-threads
— количество потоков (ядра CPU), которые Unbound будет использовать;msg-cache-slabs
— количество “шардов” (разделов) дляmsg-cache
;rrset-cache-slabs
— аналогично, но дляrrset-cache
.
Расположен скрипт в корне контейнера и запустить его можно так:
sudo docker exec -it unbound /check.sh
Но по причине того, что контейнеры часто ограничивают в ресурсах (см. выше сноску про deploy
), а сам контейнер не понимает, что его ограничили😅 (он видит реальные параметры хоста ОЗУ и CPU) — я добавил в скрипт возможность ручного указания параметров.
Например команда расчета рекомендаций для 4Гб ОЗУ (указывается в Мб) и 4 ядра ЦПУ выглядит так:
sudo docker exec -it unbound /check.sh -m 4096 -t 4
Полученные значения можно задать в основном конфиг-файле: /opt/unbound/data/etc/unbound.conf
.
Еще раз подчеркну, данные расчеты носят рекомендательный характер😏.
Опционально: запуск Unbound от имени сервисного пользователя с помощью Sudo и Systemd
Я предпочитаю управлять своими сервисами с помощью системы инициализации — Systemd. Поэтому далее покажу один из способов запуска контейнера от имени ограниченного в правах пользователя с помощью sudo + systemd😌.
Останавливаем ранее запущенный сервис:
sudo docker compose --file /opt/unbound/docker-compose.yml down
Создаем сервисного пользователя:
sudo addgroup --system --gid 14956 unbound
sudo adduser --system --gecos 'Unbound DNS service' \
--disabled-password --uid 14956 --ingroup unbound \
--shell /sbin/nologin --home /opt/unbound/data unbound
adgroup
– команда создания группы;
--system
– помечает группу, как системную и применяет к ней установленные политики;--gid 14956
– указывает явный идентификатор группы (GID), как в контейнере;unbound
– название создаваемой группы;
adduser
– команда создания пользователя
--system
– помечает группу, как системную и применяет к ней установленные политики;--gecos
– позволяет задать описание пользователя;--disabled-password
– отключает пароль у пользователя (в таком случае под ним нельзя авторизоваться в системе с помощью пароля);--uid 14956
– указывает явный идентификатор пользователя (UID), как в контейнере;--ingroup unbound
– добавляет пользователя в созданную ранее группу unbound;--shell /sbin/nologin
– в качестве оболочки пользователя устанавливает nologin – под ней невозможно авторизоваться в системе;--home /opt/unbound/data
– задает домашний каталог пользователя, в нашем случае это директория с файлами сервиса unbound;unbound
– имя создаваемого пользователя.
Копируем файл с перечнем ограниченных sudo
полномочий для пользователя unbound
:
sudo cp /opt/unbound/unbound_sudoers /etc/sudoers.d/90_unbound
# на всякий случай проверяем синтаксис
sudo visudo -c -f /etc/sudoers.d/90_unbound
Копируем файл сервиса Systemd, запускаем + включаем автозапуск:
sudo cp /opt/unbound/unbound.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now unbound
Проверяем статус и смотрим логи:
sudo systemctl status unbound
sudo journalctl --follow --unit unbound
Вроде все.
Не забывайте про нашу телегу📱 и чат💬 там же. Всех благ!
That should be it. If not, check the logs 🙂
