> ## Content Index
> Fetch the complete content index at: https://itsfoss.com/llms.txt
> Use this file to discover other available public pages before exploring further.

# How I Fixed the Biggest Annoyance of My Homelab
- URL: https://itsfoss.com/homelab-internal-domain-setup/
- Published: 2026-10-01T08:16:12.000Z
- Updated: 2026-10-01T08:16:12.000Z
- Description: Tired of remembering IP addresses and port numbers for your self-hosted services? Here's how I moved my homelab to clean .internal hostnames.
- Author: Abhishek Prakash
- Tags: Homelab

My homelab started small, and so did the annoyance. 

To open Jellyfin, I typed `192.168.0.x:8097`. For Home Assistant, it was another IP and `:8123`. For Karakeep, Ollama and the rest, even more IP and port combinations to either remember or bookmark.

The bookmarks weren't reliable either. My [ZimaCube](https://itsfoss.com/zimacube-2-review/) got its IP address from the router like any other device. Swap a cable or let it reconnect, and it mostly came back with a different IP, breaking every bookmark and config pointing to it. That's worse for Jellyfin because typing a full combination IP address and port number with a TV remote will give you a taste of medieval torture. 

So, one weekend I decided to fix it. I started with assigning dedicated IP address to my Zima devices ([ZimaBoard](https://amzn.to/46Z2N4v?ref=itsfoss.com) and [ZimaCube](https://www.awin1.com/cread.php?awinmid=42286&awinaffid=429005&ued=https%3A%2F%2Fwww.zimaspace.com%2Fproducts%2Fcube-personal-cloud&ref=itsfoss.com)) but ended up with a full network clean up.

Now my setup is smooth with all the regular services running with a .internal domain. Now I just type `jellyfin.internal` in the browser. No IP, no port number. Saved me a midlife crisis. 

## My setup, before and after

Here's what my network looked like before the cleanup. The router from my ISP feeds a TP-Link router, which runs the homelab network, with a OneMesh node extending the Wi-Fi. 

ZimaBoard consumes less power and runs services like Jellyfin that need to be on all the time. ZimaCube is a powerful device, and with Nvidia Ada RTX on it, I use it for local AI exploration. To cut down on my electricity bill, I only turn it on when I need it.

![My original homelab setup before the DNS changes](https://itsfoss.com/content/images/2026/10/homelab-before.png)

And here's what it looks like now. Both Zima devices have fixed IPs, the ZimaBoard handles DNS and the reverse proxy, and every service has a proper name. 

![my homelab setup after dns and reverse proxy manager](https://itsfoss.com/content/images/2026/10/homelab-after.png)

## The idea in a nutshell: AdGuard as DNS and Nginx Proxy Manager

The whole setup rests on two pieces working together. AdGuard Home, running as the network's DNS server, turns a name like `jellyfin.internal` into an IP address. 

Nginx Proxy Manager then looks at which name you asked for and forwards the request to the right port. Because AdGuard can only work on the IP address, not port numbers.

Once that's in place, adding a new service is a two-step routine: one DNS rewrite in AdGuard, one proxy host in Nginx Proxy Manager. That's it.

🚧

This is **my* setup, built around my routers, my Zima devices, and the services I run in my homelab. Take inspiration from it and use it as a reference, but don't copy it blindly. Your IP addresses, ports, router menus and commands will almost certainly be different.

## Step 1: Give the core devices fixed IPs

Everything in this setup depends on IP addresses that never change. If the DNS server's IP changes, the entire setup breaks. So the first job was setting DHCP reservations on the router.

On my TP-Link, this lives under **Advanced -> Network -> DHCP Server -> Address Reservation**. 

It actually shows me the connected device and gives the option to reserve the IP from there itself.

![reserving ip address for devices on local](https://itsfoss.com/content/images/2026/10/homelab-internal-domain-setup.png)

That may not always be the case for all the routers. So, you can find the MAC address on Linux with:

```bash
ip link show

```

That will show the MAC address of the current device. You can use a [networking command](https://itsfoss.com/basic-linux-networking-commands/) like arp to scan the mac address of other devices connected to your network. The best place still is the router for this activity because it sees all the connected devices to the network anyways.

💡

I also moved the start of the DHCP pool to `192.168.0.10`, so no phone or laptop ever grabs an IP I've reserved. 

Here's the addressing scheme I ended up with:

| Device         | IP                  | Notes                                            |
| -------------- | ------------------- | ------------------------------------------------ |
| Router         | 192.168.0.1         | Homelab network gateway                          |
| ZimaBoard      | 192.168.0.4         | Runs 24x7, hosts AdGuard and Nginx Proxy Manager |
| ZimaCube 2 Pro | 192.168.0.5         | Not always on, runs heavier services             |
| Dynamic pool   | 192.168.0.10 to 253 | Phones, laptops, everything else                 |

I have also assigned fixed IPs to Raspberry Pi and other SBCs in this setup. They are used for running local AI harnesses like Nanoclaw and Hermes agents. I am also setting up Frigate for the cameras. I will share my experience with those things in some later article.

Note that some devices may need to be rebooted or renew its DHCP lease to pick up the reserved IP.

📋

Since I used ZimaOS, things were more click click and install. For other setup, you will have to install and [setup AdGuard DNS](https://adguard-dns.io/kb/adguard-home/getting-started/?ref=itsfoss.com) and Nginx Proxy Manager. Instructions can be found on their respective websites.

## Step 2: Install AdGuard Home on the always-on box

AdGuard Home becomes the DNS server for the entire homelab network, so it has to be up all the time. My ZimaBoard runs 24x7 while the ZimaCube doesn't, so the choice was easy.

In the ZimaOS App Store, I installed the **AdGuard Home (HOST)** variant, not the regular one. Host networking lets AdGuard bind directly to port 53 and see the real IPs of clients. With Docker's default bridge network, traffic gets NATed through the container and you lose both.

The install shows a tips popup with a config script. Its `wget` command failed on my system with a "Can't be verbose and quiet at the same time" error, so I ran the same script with `curl` instead:

```bash
sudo bash -c "$(curl -fsSL https://raw.githubusercontent.com/bigbeartechworld/big-bear-scripts/master/generate-adguard-home-config/run.sh)"

```

**Don't skip `sudo` here**. Without it, the script fails to create directories but still prints a success message. 

I accepted the default config path, restarted the app from the ZimaOS dashboard, and opened `http://192.168.0.4:3000` manually. Clicking the app icon doesn't work for host-mode apps.

💡

AdGuard offers [AdGuard DNS](https://adguard-dns.io/license.html?aid=135612&promoCode=ITSFOSS20&ref=itsfoss.com) as a paid cloud service. But its open source equivalent is free to install and use on your own device. That's what I used here.

### When ports are already taken

The setup wizard asks for an admin port and a DNS port, and both clashed with something. Port 80 for the web UI was taken, so I set AdGuard's admin UI to `3786` instead.

Port 53 was more surprising. Unusual, right? Turns out I had a Pi-hole container running that I had completely forgotten about. I found it with:

```bash
sudo docker ps --format "{{.Names}}: {{.Ports}}"

```

Pi-hole and AdGuard do the same job, so there was no point running both. I removed Pi-hole.

![](https://itsfoss.com/content/images/2026/10/adguard-homelab-setup.webp)

## Step 3: Point the network at AdGuard

AdGuard was running, but no device was using it yet. On my TP Link, I went to **Advanced -> Network -> Internet**, expanded Advanced Settings, and switched DNS Address to "Use the Following DNS Addresses". 

Primary DNS is AdGuard at `192.168.0.4`, and secondary is `1.1.1.1`. Here. 192.168.0.4 is the IP address of the ZimaBoard that has AdGuard running on it.

![AdGuard setup as DNS in the router](https://itsfoss.com/content/images/2026/10/tplink-adguard-dns-setup.png)

Here's what this setting actually does. Devices on the network still use the router as their DNS server, and the router forwards their queries to AdGuard. That's why AdGuard's query log mostly shows the router as the client, not individual devices. For per-device stats, setting AdGuard's IP in the DHCP Server page should work, so that the router hands it to devices directly. 

📋

The `1.1.1.1` secondary is a safety net, so the internet doesn't go dark when the ZimaBoard is down. It comes with a trade-off, though. DNS clients don't always wait for the primary to fail before trying the secondary. When a query goes to Cloudflare instead, the ad slips through and `.internal` names don't resolve, since Cloudflare has no idea they exist. If you notice a hostname failing occasionally, this is the likely culprit.

### When a device ignores the new DNS

While testing, I manually set `192.168.0.4` as DNS on a Linux laptop through GNOME's network settings. `dig @192.168.0.4 google.com` worked, but browser traffic never showed up in AdGuard's log. Running `resolvectl status` revealed the system was still using the router, as GNOME hadn't applied the change to the live connection.

Reconnecting to Wi-Fi fixed it. The more dependable way is doing it through `nmcli`:

```bash
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>"

```

## Step 4: Create the .internal names with DNS rewrites

This is where the hostnames come to life. In AdGuard, go to **Filters -> DNS rewrites -> Add DNS rewrite**, enter a domain like `jellyfin.internal`, and point it to an IP address.

![AdGuard DNS rewrites](https://itsfoss.com/content/images/2026/10/adguard-dns-rewrite.png)

Here's the thing. ZimaBoard runs multiple services. DNS rewrite only accepts IP address, not port numbers. If I have to add jellyfin.internal and homeassistant.internal in the DNS, both will be pointed to the same 192.168.0.4 IP address. And they won't be resolved.

I mean, I could do `zimaboard.internal:8097` and that would land me on Jellyfin but what's the point? A proper `jellyfin.internal` is what I would want. We need the port numbers.

![AdGuard DNS rewrites](https://itsfoss.com/content/images/2026/10/adguard-dns-rewrites.webp)

This is why we need a proxy manager to properly map the domain names with both IP addresses and the port numbers. But a proxy manager cannot act as DNS and hence we need both AdGuard DNS in combination with a tool like [Ngnix Proxy Manager](https://nginxproxymanager.com/?ref=itsfoss.com).

📋

Why `.internal`? A good option was `.local` but it is reserved for mDNS (Bonjour, Avahi), so many devices resolve it outside your DNS server, which could lead to inconsistent results. `.lan` and `.home` aren't reserved and could become real domains someday, just like `.dev` did when Google bought it. `.home.arpa` is official but clunky to type. In 2024, [ICANN permanently reserved](https://www.icann.org/en/board-activities-and-meetings/materials/approved-resolutions-special-meeting-of-the-icann-board-29-07-2024-en?ref=itsfoss.com#section2.a) `.internal` for private networks. It's short, readable, and will never clash with a real website. And it fits the entire homelab narrative.

## Step 5: Use the port numbers with Nginx Proxy Manager

DNS only translates a name into an IP. It knows nothing about ports. To make `http://jellyfin.internal` work without `:8097`, something has to listen on port 80, check which hostname was requested, and forward it to the right port. That's a reverse proxy.

I'd have preferred [Caddy](https://caddyserver.com/?ref=itsfoss.com), as that's what I use on some of my servers. But Caddy wasn't available as a one-click app in ZimaOS and I want to keep everything in Zima ecosystem. So I opted for [Nginx Proxy Manager](https://nginxproxymanager.com/?ref=itsfoss.com) (NPM) as it does the same job with a web interface instead of a config file. *Like AdGuard, it went on the always-on ZimaBoard*.

### Port 80 issue, again

NPM needs ports 80, 443 and 81 (its own admin UI). Port 80 was taken again, this time by `zimaos-gateway`, the process serving the ZimaOS dashboard for ZimaBoard. I confirmed it with:

```bash
sudo ss -tulpn | grep :80

```

The tempting fix is giving NPM a different port, but that defeats the whole purpose. You'd be back to typing port numbers. 

Instead, I moved the ZimaOS dashboard to port `8888` from its **Settings** page. That was easy and that's why I like ZimaOS. It makes managing homelab a lot easier.

Anyways, the NPM install dialog still complained about port 80 for a while, and a full ZimaBoard reboot cleared that stale check.

### Adding proxy hosts

Once installed, NPM's admin UI is at `http://192.168.0.4:81`. Log in with the default `admin@example.com` and `changeme`, and it asks you to set new credentials right away.

To add a service, go to **Hosts -> Proxy Hosts -> Add Proxy Host** and fill in the details.

![Nginx Proxy Manager Settings](https://itsfoss.com/content/images/2026/10/ngnix-proxy-manager-settings.webp)

1. **Domain Names:** the hostname, like `jellyfin.internal`
2. **Scheme:** `http`
3. **Forward Hostname / IP:** the IP of the device running the service
4. **Forward Port:** the service's actual port, like `8097`
5. **Websockets Support:** on (Jellyfin and Home Assistant need it for live updates, and it doesn't hurt the rest so I always enable it)

📋

I left the SSL tab alone, since this traffic never leaves my home network. 

Save it, open `http://jellyfin.internal` in a new tab, and there it is. No port number.

Here's how my proxy hosts look right now:

![Nginx Proxy Manager](https://itsfoss.com/content/images/2026/10/nginx-proxy-manager-setup.webp)

💡

The "Public" access label might look scary, but it only means NPM doesn't add its own login prompt. These names resolve only through my AdGuard, so nobody outside my network can reach them. To access it from outside, a service like Tailscale should be used.

## Troubleshooting afterwards

Network setup never goes 100% trouble free. I did a face a couple of issues. Here are at least two that I recall (and have recorded):

### Home Assistant threw a 400 error

Everything worked except Home Assistant, which returned `400: Bad Request` through `homeassistant.internal`. Direct access on port 8123 was fine. Home Assistant rejects proxied requests unless it explicitly trusts the proxy, as protection against spoofed headers.

The fix goes in Home Assistant's `configuration.yaml`:

```yaml
http:
  use_x_forwarded_for: true
  trusted_proxies:
    - 172.17.0.3   # NPM container's IP on the Docker bridge network

```

I found the NPM container's IP with `sudo docker inspect nginxproxymanager | grep IPAddress`, then restarted Home Assistant with `sudo docker restart homeassistant`. 

One catch: this IP can change if the NPM container gets recreated. Trusting the whole bridge subnet (`172.17.0.0/16`) instead of a single IP is more durable.

### Netflix stopped working on the TV

Shortly after switching DNS, Netflix on my smart TV refused to connect. AdGuard's query log showed two blocked domains in red: `logs.netflix.com` and `nrdp26.logs.netflix.com`. They're telemetry endpoints, but the Netflix app treats them as part of its connectivity check.

You can unblock an entry from the query log's menu in AdGuard, or add allowlist rules under **Filters -> Custom filtering rules**:

```text
@@||logs.netflix.com^
@@||nrdp26.logs.netflix.com^

```

Restart the app on the TV and it should work again. 

💡

If something else breaks after you enable AdGuard, the query log is the first place to look.

### Port conflicts

Port conflicts came up three times during this project, so this little drill is worth keeping handy. To see which process is using a port:

```bash
sudo ss -tulpn | grep :<port>

```

The process name in the output tells you who the culprit is. If it says `docker-proxy`, check which container it belongs to:

```bash
sudo docker ps --format "{{.Names}}: {{.Ports}}"

```

Then decide whether to remove the conflicting container or move the *other* service to a different port. Just don't remap the port of the thing you're trying to make port-free, like NPM.

One more thing to keep in mind: all of this works only inside your home network. The `.internal` names exist only in your AdGuard, so they won't resolve when you're outside, unless you bring a VPN into the picture.

## Adding a new service later

This is where all the effort I put in this setup pays off. When I deployed Karakeep a few days later, giving it a proper name took just two steps:

1. In AdGuard, add a DNS rewrite: `karakeep.internal` pointing to the ZimaBoard (`192.168.0.4`).
2. In Nginx Proxy Manager, add a proxy host: `karakeep.internal` forwarding to the service's IP and port (`14592` in my case).

If the service does its own host validation, like Home Assistant, it may also need to trust the proxy.

## Wrapping up

I did all this a few months ago and it has been running smoothly so far. HTTPS for the `.internal` names would be nice to have. Perhaps I will think about implementing it some weekend.

I am sure there are other, perhaps better (?) ways of doing this. For now, this setup works for me, and I no longer have to remember a single IP address or port number in my homelab. I hope it gives you a few ideas for taming your own.

I welcome your questions and suggestions. What else could I do here? What would you like to do about a similar setup?