Skip to main content
  1. Posts/

Clean URLs and HTTPS for Your Home Server (Without Touching a Port Number Again)

·1002 words·5 mins
Author
Rudy

Once Immich was running, I had a working photo server. But accessing it meant typing an IP address and a port number into my browser every time. Not the end of the world, but not great either, especially when you start adding more services.

There’s a better way, and setting it up opened a door I hadn’t expected.


The problem with IP addresses and ports
#

When a service runs on your home server, it listens on a port. Your photo app might be on port 2283, your backup tool on 51515, your DNS dashboard on 3000. To access them you type something like:

http://192.168.x.x:2283

A few problems with this:

  • No HTTPS, so browsers show warnings and some features don’t work
  • You have to remember which port belongs to which service
  • Bookmarks break if you change a port
  • It just looks and feels rough

The solution is a reverse proxy: one piece of software that sits in front of all your services and routes traffic based on a domain name instead of a port. Visit photos.yourdomain.com and the proxy sends you to the photo app. Visit backup.yourdomain.com and it sends you to the backup tool. Clean, consistent, no port numbers to remember.


Getting a domain
#

A domain is the foundation. I registered mine through Cloudflare, around $11 a year. That’s the only recurring cost in this part of the setup.

With a domain you can create subdomains for each service. You can also get a wildcard SSL certificate, a single certificate that covers every subdomain automatically, so everything gets HTTPS without any extra effort per service.

For the wildcard cert I used Let’s Encrypt with a DNS challenge via the Cloudflare API:

certbot certonly \
  --dns-cloudflare \
  --dns-cloudflare-credentials /path/to/cloudflare.ini \
  -d "*.yourdomain.com"

The certificate renews automatically every 90 days. A small deploy hook restarts the relevant services after renewal so nothing breaks silently.


Caddy: the reverse proxy
#

I use Caddy as the reverse proxy, installed natively on the server rather than in Docker. The config syntax is genuinely simple. Each service is about three lines:

photos.yourdomain.com {
  tls /path/to/cert.pem /path/to/key.pem
  reverse_proxy localhost:2283
}

backup.yourdomain.com {
  tls /path/to/cert.pem /path/to/key.pem
  reverse_proxy localhost:51515
}

Adding a new service takes thirty seconds. Caddy handles HTTPS termination and passes traffic through to wherever the service is actually listening.

The first time I typed photos.yourdomain.com into the browser and got a proper HTTPS page instead of an IP address and port number, it felt like a proper upgrade. Small thing, but it matters.

Caddy only runs on port 443. That port is not exposed to the internet, only accessible from inside the home network or through VPN.


The DNS piece (and the accidental bonus)
#

Here’s the part that wasn’t obvious to me at first.

Your domain is registered publicly on Cloudflare. But most of your services are internal. You don’t want backup.yourdomain.com resolving publicly on the internet. And even when you’re home, you want the domain to resolve to your local server IP directly, not go out to the internet and come back.

The solution is local DNS. I needed something running on the network that could intercept those lookups and return the local IP instead.

I ended up with AdGuard Home, not because I was specifically looking for it, but because it was the cleanest tool for the job. It acts as a DNS server for the whole network, and I configured my router to use it. Then I added a single wildcard rewrite:

*.yourdomain.com → your.server.local.ip

One entry, covers every subdomain automatically. Add a new service, point Caddy at it, done. No extra DNS step.

Now when any device on the network asks “where is photos.yourdomain.com?”, AdGuard returns the server’s local IP. Fast, private, never leaves the house.

The bonus I didn’t plan for: AdGuard also blocks ads and trackers across every device on the network, with no apps to install. Every phone, every laptop, just covered. It’s become one of my favourite parts of the whole setup, and I almost didn’t set it up at all.


What goes wrong: the mistakes I made
#

Forgetting to point the router at AdGuard. I set everything up, tested in the browser, got nothing. Spent a while confused before realising my phone was still using the ISP’s DNS. The DNS rewrites only work if devices are actually using AdGuard as their resolver. Router config first.

Trying to avoid buying a domain. I asked an AI and got told Tailscale subdomains could work as an alternative. Tried it, and the problem is you don’t control the subdomain. It’s on Tailscale’s domain, not yours. You can’t point photos.tailscale-domain.com somewhere and have it behave like your own. I spent longer than I’d like to admit figuring out that AI had confidently sent me down a dead end. $11 a year, not worth the rabbit hole.


One subdomain that’s public: sharing photos
#

Most subdomains have no public DNS records, they’re internal only. But one is different.

Immich has a photo sharing feature: generate a link, send it to someone, they open it in their browser without needing an account. For that to work, the link needs to be reachable from the internet.

I handle this with a Cloudflare Tunnel, an outbound connection from my server to Cloudflare’s edge with no inbound ports required. The tunnel is configured to only forward specific URL paths (the share link paths), and return 404 for everything else. So someone with a share link can view the photos, but someone trying to reach the login page gets nothing.

This keeps the service private while making sharing work.


What the whole thing looks like
#

When I open photos.yourdomain.com on my phone at home:

  1. Phone asks AdGuard: “where is photos.yourdomain.com?”
  2. AdGuard returns the server’s local IP
  3. Phone connects to Caddy on port 443
  4. Caddy sees the domain, proxies to the photo app
  5. Photo app responds, all over HTTPS, all on the local network

The request never leaves the house. No round-trip to the internet.