Домашняя лаборатория без IP и портов: домены .internal на AdGuard Home и Nginx Proxy Manager

Перевод статьи How I Fixed the Biggest Annoyance of My Homelab — Abhishek Prakash.
Моя домашняя лаборатория начиналась с малого — и раздражение поначалу тоже было небольшим. Чтобы открыть Jellyfin, я набирал 192.168.0.x:8097. Для Home Assistant — другой IP и :8123. Для Karakeep, Ollama и всего остального — ещё больше комбинаций IP и портов, которые надо было либо помнить, либо держать в закладках.

Да и закладки не спасали. Мой ZimaCube получал IP-адрес от роутера, как и любое другое устройство. Стоило переткнуть кабель или дать ему переподключиться — и он, как правило, возвращался с другим IP, ломая все закладки и конфиги, которые на него ссылались. С Jellyfin это особенно неприятно: набирать полный IP-адрес с номером порта пультом от телевизора — это настоящая средневековая пытка.
Так что однажды на выходных я решил с этим разобраться. Начал с того, что выдал фиксированные IP-адреса своим устройствам Zima (ZimaBoard и ZimaCube), а закончил полной уборкой в сети.
Теперь всё работает гладко: все мои постоянные сервисы доступны по доменам .internal. Я просто набираю в браузере jellyfin.internal. Ни IP, ни порта. Это спасло меня от кризиса среднего возраста.
Моя сеть: до и после#
Вот как выглядела моя сеть до уборки. Роутер провайдера раздаёт интернет на роутер TP-Link, который обслуживает сеть домашней лаборатории, а узел OneMesh расширяет зону Wi-Fi.
ZimaBoard потребляет мало энергии, и на нём крутятся сервисы, которые должны работать всегда, вроде Jellyfin. ZimaCube — мощное устройство, и с установленной Nvidia RTX на архитектуре Ada я использую его для экспериментов с локальным ИИ. Чтобы не раздувать счёт за электричество, включаю его только когда нужно.

Сеть до уборки
А вот как она выглядит сейчас. У обоих устройств Zima фиксированные IP, ZimaBoard отвечает за DNS и обратный прокси, а у каждого сервиса есть нормальное имя.

Сеть после уборки
Идея в двух словах: AdGuard в роли DNS и Nginx Proxy Manager#
Вся схема держится на двух компонентах, работающих вместе. AdGuard Home в роли DNS-сервера сети превращает имя вроде jellyfin.internal в IP-адрес.
Затем Nginx Proxy Manager смотрит, какое имя запросили, и перенаправляет запрос на нужный порт. Это нужно потому, что AdGuard работает только с IP-адресами, а не с номерами портов.
Когда всё настроено, добавление нового сервиса сводится к двум шагам: одна запись DNS rewrite в AdGuard и один proxy host в Nginx Proxy Manager. Вот и всё.
Важно. Это моя схема, построенная вокруг моих роутеров, моих устройств Zima и сервисов, которые я запускаю у себя. Черпайте из неё идеи и используйте как образец, но не копируйте вслепую. Ваши IP-адреса, порты, меню роутера и команды почти наверняка будут другими.
Шаг 1. Выдаём ключевым устройствам фиксированные IP#
Вся схема держится на IP-адресах, которые никогда не меняются. Если сменится IP DNS-сервера, сломается всё. Поэтому первым делом я настроил на роутере резервирование адресов DHCP.
На моём TP-Link это находится в разделе Advanced -> Network -> DHCP Server -> Address Reservation.
Он прямо показывает подключённые устройства и позволяет зарезервировать IP прямо оттуда.

Резервирование адресов на роутере TP-Link
На других роутерах так может и не быть. Тогда MAC-адрес в Linux можно узнать командой:
ip link show
Она покажет MAC-адрес текущего устройства. Чтобы узнать MAC-адреса других устройств в сети, можно воспользоваться сетевой командой вроде arp. Но лучшее место для этого всё равно роутер — он и так видит все подключённые к сети устройства.
Совет. Я также сдвинул начало пула DHCP на 192.168.0.10, чтобы ни один телефон или ноутбук никогда не занял зарезервированный мной адрес.
Вот какая схема адресации у меня получилась:
| Устройство | IP | Примечание |
|---|---|---|
| Роутер | 192.168.0.1 | Шлюз сети домашней лаборатории |
| ZimaBoard | 192.168.0.4 | Работает круглосуточно, на нём AdGuard и Nginx Proxy Manager |
| ZimaCube 2 Pro | 192.168.0.5 | Включён не всегда, на нём более тяжёлые сервисы |
| Динамический пул | 192.168.0.10–253 | Телефоны, ноутбуки и всё остальное |
Ещё я выдал фиксированные IP Raspberry Pi и другим одноплатникам в этой сети. На них работают локальные ИИ-обвязки вроде Nanoclaw и агентов Hermes. Ещё я настраиваю Frigate для камер. Об этом расскажу в одной из следующих статей.
Учтите, что некоторым устройствам может понадобиться перезагрузка или обновление аренды DHCP, чтобы получить зарезервированный IP.
На заметку. Я использовал ZimaOS, поэтому у меня всё ставилось парой кликов. В других системах AdGuard Home и Nginx Proxy Manager придётся установить и настроить самостоятельно. Инструкции есть на сайтах обоих проектов.
Шаг 2. Ставим AdGuard Home на машину, которая всегда включена#
AdGuard Home становится DNS-сервером всей сети, так что он должен работать постоянно. ZimaBoard у меня включён круглосуточно, а ZimaCube — нет, так что выбор был очевиден.
В магазине приложений ZimaOS я установил вариант AdGuard Home (HOST), а не обычный. Сетевой режим host позволяет AdGuard напрямую занять порт 53 и видеть реальные IP клиентов. В сети Docker по умолчанию (bridge) трафик проходит через NAT контейнера, и вы теряете и то и другое.
При установке появляется подсказка со скриптом настройки. Команда wget из неё у меня падала с ошибкой «Can’t be verbose and quiet at the same time», поэтому я запустил тот же скрипт через curl:
sudo bash -c "$(curl -fsSL https://raw.githubusercontent.com/bigbeartechworld/big-bear-scripts/master/generate-adguard-home-config/run.sh)"
Не забудьте sudo. Без него скрипт не сможет создать каталоги, но всё равно напишет, что всё прошло успешно.
Я согласился с путём к конфигу по умолчанию, перезапустил приложение из панели ZimaOS и вручную открыл http://192.168.0.4:3000. Клик по иконке приложения для приложений в режиме host не работает.
Совет. У AdGuard есть платный облачный сервис AdGuard DNS. Но его открытый аналог можно бесплатно установить на своё устройство и пользоваться. Именно его я здесь и использовал.
Когда порты уже заняты#
Мастер настройки спрашивает порт для админки и порт для DNS, и оба с чем-то конфликтовали. Порт 80 для веб-интерфейса был занят, поэтому админку AdGuard я повесил на 3786.
С портом 53 вышло неожиданнее. Странно, правда? Оказалось, у меня крутился контейнер Pi-hole, о котором я напрочь забыл. Нашёл я его так:
sudo docker ps --format "{{.Names}}: {{.Ports}}"
Pi-hole и AdGuard делают одно и то же, так что держать оба не было смысла. Pi-hole я удалил.

Настройка AdGuard Home
Шаг 3. Направляем сеть на AdGuard#
AdGuard заработал, но ни одно устройство им пока не пользовалось. На своём TP-Link я зашёл в Advanced -> Network -> Internet, раскрыл Advanced Settings и переключил DNS Address на «Use the Following DNS Addresses».
Основной DNS — AdGuard по адресу 192.168.0.4, резервный — 1.1.1.1. Здесь 192.168.0.4 — это IP-адрес ZimaBoard, на котором работает AdGuard.

AdGuard как DNS-сервер в настройках роутера
Вот что на самом деле делает эта настройка. Устройства в сети по-прежнему используют роутер как DNS-сервер, а роутер пересылает их запросы в AdGuard. Поэтому в журнале запросов AdGuard в качестве клиента в основном фигурирует роутер, а не отдельные устройства. Если нужна статистика по устройствам, должно сработать указание IP AdGuard на странице DHCP Server — тогда роутер будет раздавать его устройствам напрямую.
На заметку. Резервный 1.1.1.1 — это страховка, чтобы интернет не пропал, если ZimaBoard выключится. Но у неё есть обратная сторона. DNS-клиенты не всегда дожидаются отказа основного сервера, прежде чем обратиться к резервному. Если запрос уходит в Cloudflare, реклама проскакивает, а имена .internal не разрешаются — Cloudflare понятия не имеет об их существовании. Если какое-то имя время от времени не открывается, скорее всего, дело в этом.
Когда устройство игнорирует новый DNS#
Во время тестов я вручную прописал 192.168.0.4 как DNS на ноутбуке с Linux через сетевые настройки GNOME. dig @192.168.0.4 google.com работал, но трафик браузера в журнале AdGuard так и не появился. resolvectl status показал, что система по-прежнему использует роутер: GNOME не применил изменения к активному подключению.
Помогло переподключение к Wi-Fi. Более надёжный способ — через nmcli:
nmcli connection modify "<connection-name>" ipv4.dns "192.168.0.4"
nmcli connection modify "<connection-name>" ipv4.ignore-auto-dns yes
nmcli connection down "<connection-name>" && nmcli connection up "<connection-name>"
Шаг 4. Создаём имена .internal через DNS rewrites#
Здесь имена хостов и оживают. В AdGuard зайдите в Filters -> DNS rewrites -> Add DNS rewrite, введите домен вроде jellyfin.internal и укажите для него IP-адрес.

Добавляем DNS rewrite в AdGuard
Но есть нюанс. На ZimaBoard работает несколько сервисов. DNS rewrite принимает только IP-адрес, без номера порта. Если я добавлю в DNS jellyfin.internal и homeassistant.internal, оба будут указывать на один и тот же IP 192.168.0.4. И до нужного сервиса не доведут.
Конечно, можно было бы набирать zimaboard.internal:8097 и попадать в Jellyfin, но какой в этом смысл? Мне нужен нормальный jellyfin.internal. Значит, нужно как-то учитывать номера портов.

Записи DNS rewrites в AdGuard
Поэтому нам и нужен прокси-менеджер, который правильно сопоставит доменные имена и с IP-адресами, и с номерами портов. Но прокси-менеджер не может работать как DNS, так что нам нужна связка AdGuard и инструмента вроде Nginx Proxy Manager.
Почему .internal? Хорошим вариантом казался .local, но он зарезервирован под mDNS (Bonjour, Avahi), и многие устройства разрешают его в обход вашего DNS-сервера, что может давать непредсказуемые результаты. .lan и .home не зарезервированы и когда-нибудь могут стать настоящими доменами — как это случилось с .dev, когда его купил Google. .home.arpa официальный, но его неудобно набирать. В 2024 году ICANN навсегда зарезервировала .internal для частных сетей. Он короткий, читаемый и никогда не пересечётся с настоящим сайтом. И отлично вписывается в саму идею домашней лаборатории.
Шаг 5. Прячем номера портов за Nginx Proxy Manager#
DNS только переводит имя в IP. О портах он ничего не знает. Чтобы http://jellyfin.internal работал без :8097, что-то должно слушать порт 80, смотреть, какое имя хоста запросили, и пересылать запрос на нужный порт. Это и есть обратный прокси.
Я бы предпочёл Caddy — им я пользуюсь на некоторых своих серверах. Но в ZimaOS Caddy не было в виде приложения в один клик, а мне хотелось держать всё в экосистеме Zima. Поэтому я выбрал Nginx Proxy Manager (NPM): он делает то же самое, но через веб-интерфейс вместо конфиг-файла. Как и AdGuard, он отправился на круглосуточно работающий ZimaBoard.
И снова проблема с портом 80#
NPM нужны порты 80, 443 и 81 (его собственная админка). Порт 80 опять оказался занят — на этот раз zimaos-gateway, процессом, который отдаёт панель ZimaOS на ZimaBoard. Я убедился в этом командой:
sudo ss -tulpn | grep :80
Напрашивается решение дать NPM другой порт, но это убивает весь смысл затеи: вы снова будете набирать номера портов.
Вместо этого я перенёс панель ZimaOS на порт 8888 через её настройки. Это оказалось просто, и именно за это я люблю ZimaOS: она сильно упрощает управление домашней лабораторией.
Впрочем, диалог установки NPM ещё какое-то время жаловался на порт 80, и избавиться от этой устаревшей проверки помогла только полная перезагрузка ZimaBoard.
Добавляем proxy hosts#
После установки админка NPM доступна по адресу http://192.168.0.4:81. Войдите с учётными данными по умолчанию admin@example.com и changeme — NPM сразу попросит задать новые.
Чтобы добавить сервис, зайдите в Hosts -> Proxy Hosts -> Add Proxy Host и заполните поля.

Настройки proxy host в Nginx Proxy Manager
- Domain Names: имя хоста, например jellyfin.internal
- Scheme: http
- Forward Hostname / IP: IP устройства, на котором работает сервис
- Forward Port: реальный порт сервиса, например 8097
- Websockets Support: включено (Jellyfin и Home Assistant нужны вебсокеты для обновлений в реальном времени, а остальным это не мешает, так что я включаю всегда)
На заметку. Вкладку SSL я не трогал, поскольку этот трафик никогда не покидает мою домашнюю сеть.
Сохраните, откройте в новой вкладке http://jellyfin.internal — и вот он. Без номера порта.
Вот как сейчас выглядят мои proxy hosts:

Мои proxy hosts в Nginx Proxy Manager
Совет. Метка доступа «Public» может напугать, но она лишь означает, что NPM не добавляет собственный запрос логина. Эти имена разрешаются только через мой AdGuard, так что извне сети до них никто не доберётся. Для доступа снаружи стоит использовать сервис вроде Tailscale.
Устранение неполадок#
Настройка сети никогда не проходит на 100% гладко. Несколько проблем у меня всё же возникло. Вот как минимум две, которые я помню (и записал).
Home Assistant выдавал ошибку 400#
Всё работало, кроме Home Assistant: по адресу homeassistant.internal он возвращал 400: Bad Request. Напрямую на порту 8123 всё открывалось. Home Assistant отклоняет запросы через прокси, если явно не доверяет этому прокси, — это защита от подделки заголовков.
Исправляется это в configuration.yaml Home Assistant:
http:
use_x_forwarded_for: true
trusted_proxies:
- 172.17.0.3 # NPM container's IP on the Docker bridge network
IP контейнера NPM я нашёл командой sudo docker inspect nginxproxymanager | grep IPAddress, после чего перезапустил Home Assistant через sudo docker restart homeassistant.
Одна загвоздка: этот IP может поменяться, если контейнер NPM пересоздать. Надёжнее доверять всей подсети bridge (172.17.0.0/16), а не одному адресу.
На телевизоре перестал работать Netflix#
Вскоре после переключения DNS Netflix на моём смарт-ТВ отказался подключаться. В журнале запросов AdGuard красным светились два заблокированных домена: logs.netflix.com и nrdp26.logs.netflix.com. Это эндпоинты телеметрии, но приложение Netflix считает их частью проверки соединения.
Разблокировать запись можно из меню журнала запросов в AdGuard или добавить правила-исключения в Filters -> Custom filtering rules:
@@||logs.netflix.com^
@@||nrdp26.logs.netflix.com^
Перезапустите приложение на телевизоре — и всё снова должно заработать.
Совет. Если после включения AdGuard что-то ещё сломалось, первым делом смотрите в журнал запросов.
Конфликты портов#
За время этого проекта конфликты портов возникали трижды, так что этот небольшой порядок действий стоит держать под рукой. Чтобы узнать, какой процесс занял порт:
sudo ss -tulpn | grep :<port>
Имя процесса в выводе подскажет виновника. Если там docker-proxy, посмотрите, какому контейнеру он принадлежит:
sudo docker ps --format "{{.Names}}: {{.Ports}}"
Дальше решайте, удалить ли мешающий контейнер или перенести другой сервис на другой порт. Только не меняйте порт у того, что вы как раз пытаетесь избавить от портов, — например, у NPM.
И ещё одно: всё это работает только внутри вашей домашней сети. Имена .internal существуют только в вашем AdGuard, поэтому снаружи они разрешаться не будут — разве что вы подключите VPN.
Добавление нового сервиса#
Вот тут все вложенные в схему усилия и окупаются. Когда через несколько дней я развернул Karakeep, чтобы дать ему нормальное имя, понадобилось всего два шага:
- В AdGuard добавить DNS rewrite: karakeep.internal, указывающий на ZimaBoard (192.168.0.4).
- В Nginx Proxy Manager добавить proxy host: karakeep.internal с пересылкой на IP и порт сервиса (у меня 14592).
Если сервис сам проверяет имя хоста, как Home Assistant, ему, возможно, тоже придётся явно доверять прокси.
Итоги#
Я всё это сделал несколько месяцев назад, и с тех пор оно работает без проблем. Было бы неплохо добавить HTTPS для имён .internal. Может, займусь этим на каких-нибудь выходных.
Уверен, есть и другие, возможно, более удачные (?) способы это сделать. Но пока эта схема меня устраивает, и мне больше не нужно помнить ни одного IP-адреса или номера порта в своей домашней лаборатории. Надеюсь, она подскажет вам пару идей, как навести порядок в своей.
Буду рад вопросам и предложениям. Что ещё здесь можно было бы сделать? Как бы вы построили похожую схему?