Writing

Using Pi-hole + Caddy for Clean Local URLs and HTTPS

Saturday. May 2, 2026

So far in this series, most of my services have been running fine on the LAN, but I’ve still been opening them the ugly way:

•	http://192.168.1.50:8096
•	http://192.168.1.50:13378
•	http://192.168.1.50:2283

It works, but it gets annoying fast. I don’t want to memorize ports, and typing IP addresses into the browser every time makes the setup feel more hacked together than it really is.

In this post, I clean that up with two pieces: • Pi-hole as local DNS, so services get readable names • Caddy as a reverse proxy, so those names can also use HTTPS

After this, I can open things like: • https://jellyfin.home.arpa • https://audiobookshelf.home.arpa • https://immich.home.arpa

Much better.

  1. What each piece is doing

The basic split is: • Pi-hole answers DNS questions on my LAN so jellyfin.home.arpa points to my server’s local IP • Caddy listens on ports 80/443 and forwards traffic to the correct app based on the hostname

So the flow becomes:

browser -> Pi-hole DNS -> Caddy -> local service

Instead of:

browser -> raw IP:port

  1. Pick the service names

I used a few simple names for the services I already had running: • jellyfin.home.arpa • audiobookshelf.home.arpa • immich.home.arpa • homepage.home.arpa

They all point to the same server IP on my LAN.

For example, if my server is at 192.168.1.50, every one of those names resolves to:

192.168.1.50

Then Caddy decides which backend to send traffic to.

  1. Add local DNS records in Pi-hole

In Pi-hole, I added local DNS records for each service name.

Go to the Pi-hole admin page and add records like: • jellyfin.home.arpa → 192.168.1.50 • audiobookshelf.home.arpa → 192.168.1.50 • immich.home.arpa → 192.168.1.50 • homepage.home.arpa → 192.168.1.50

The important part is that they all point to the same machine, since Caddy will sit in front of everything.

If your router lets you choose the LAN DNS server, point your devices to Pi-hole so they actually use those records.

Once that’s done, a quick test from another machine on the network should work:

ping jellyfin.home.arpa

or:

nslookup jellyfin.home.arpa

You should see your server’s local IP come back.

  1. Quick troubleshooting note

One thing that confused me for a bit: I forgot I still had AdGuard’s DNS turned on.

So while I was adding local DNS records in Pi-hole, some devices were still using AdGuard instead of Pi-hole for DNS resolution. That made the whole setup feel inconsistent and more broken than it actually was. One minute a hostname seemed fine, the next minute it didn’t resolve the way I expected.

Once I realized that, the fix was simple: make sure my devices were actually using Pi-hole as the DNS server instead of AdGuard.

After that, everything made a lot more sense, and the rest of the setup was pretty smooth.

That was a good reminder that when local DNS behaves strangely, the problem might not be the record itself. It might just be that the device is asking the wrong DNS server.

  1. Install Caddy

On Debian, I installed Caddy from the official repo.

First install the packages needed to add the repo:

sudo apt update
sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https curl

Add the repo key:

curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | \
  sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg

Add the repo:

curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | \
  sudo tee /etc/apt/sources.list.d/caddy-stable.list

Then install Caddy:

sudo apt update
sudo apt install -y caddy
  1. Create the Caddy config

Caddy’s main config file is:

/etc/caddy/Caddyfile

I edited it like this:

jellyfin.home.arpa {
    reverse_proxy 127.0.0.1:8096
    tls internal
}

audiobookshelf.home.arpa {
    reverse_proxy 127.0.0.1:13378
    tls internal
}

immich.home.arpa {
    reverse_proxy 127.0.0.1:2283
    tls internal
}

homepage.home.arpa {
    reverse_proxy 127.0.0.1:3000
    tls internal
}

A few notes: • reverse_proxy sends traffic to the real service • 127.0.0.1:PORT works if the service is listening on the same machine • tls internal tells Caddy to generate its own local certificates for HTTPS

That last part is what gives me HTTPS on the LAN without needing to expose anything to the public internet.

  1. Check and reload Caddy

Before reloading, I validated the config:

sudo caddy validate --config /etc/caddy/Caddyfile

If that looks good, reload it:

sudo systemctl reload caddy

And if I want to confirm it’s running:

sudo systemctl status caddy

  1. Allow HTTP and HTTPS through the firewall

If ufw is enabled, open the standard web ports:

sudo ufw allow 80/tcp
sudo ufw allow 443/tcp

Then check:

sudo ufw status

Now the browser can actually reach Caddy.

  1. Trust Caddy’s internal certificates

Because this is local HTTPS, the certs come from Caddy’s internal CA, not a public certificate authority.

That means browsers may still warn at first unless the Caddy root certificate is trusted on the devices you use.

On the server, Caddy can install its local CA like this:

sudo caddy trust

For other devices on the LAN, I still need to trust that root cert manually if I want the browser to fully accept the HTTPS connection without warnings.

So the setup is: • DNS name resolves locally with Pi-hole • Caddy serves HTTPS using internal certs • trusted devices get the clean browser experience

Even before doing that everywhere, I still liked having the structure in place.

  1. Test the finished setup

At that point I could open: • https://jellyfin.home.arpa • https://audiobookshelf.home.arpa • https://immich.home.arpa • https://homepage.home.arpa

No ports to remember. No IPs in bookmarks. Just readable service names.

That was the moment the whole setup started feeling a lot more organized.

  1. A nice side benefit: better setup for remote access later

Another reason this was worth doing is that it sets me up nicely if I ever want to access these services from outside the LAN later.

Right now, everything is still local-only, which is exactly how I want it. But using proper DNS names and a reverse proxy means the structure is already there if I decide to open up limited remote access in the future.

Instead of rebuilding everything later, I’d already have: • clean service hostnames • one reverse proxy handling traffic • a consistent pattern for adding new services • a much easier path to using a domain name later

So even though this change was mostly about making the LAN setup cleaner, it also feels like one of those upgrades that makes future improvements easier without forcing me to do them now.

That’s probably my favorite kind of infrastructure change: better today, and also a better foundation for later.

  1. Why this was worth doing

This was one of those upgrades that doesn’t add a brand new app, but still makes the server better every day.

Before: • IP addresses • random ports • browser warnings • harder to remember where everything lived

After: • clean service names • one front door for everything • HTTPS on the LAN • easier to add new services later

It made the server feel less like “a bunch of stuff running on one laptop” and more like a small platform I actually want to keep building on.

And that’s a big part of why I like self-hosting in the first place. Not just because the software works, but because I can keep refining the environment until it feels calm, clean, and mine.

What’s next

With Pi-hole handling local DNS and Caddy handling HTTPS + reverse proxying, adding new services gets a lot easier: 1. install the app 2. give it a DNS name in Pi-hole 3. add a Caddy block for it 4. open it at a clean URL

That pattern is simple enough that I’ll probably keep using it for the rest of this setup.