🌐 Networking

One Gateway to Rule Them All: Nginx Proxy Manager on Proxmox

The Homelab IP Nightmare

Picture this: Your homelab is growing. You've got Jellyfin at 192.168.1.14:8096. Technitium at 192.168.1.11:5380. NPM's own UI at 192.168.1.12:81. Proxmox itself at 192.168.1.1:8006.

Now try telling a family member how to reach the media server. Or opening it on your phone. Or remembering which port that one service uses two months from now.

Translation: Raw IPs and ports don't scale past three services. You need a real front door.

That front door is a reverse proxy. It listens on ports 80 and 443, reads the domain name in each incoming request, and forwards it to the correct backend. Instead of 192.168.1.14:8096, you type jellyfin.home.lan. Same service. Zero mental overhead.

Bonus round: since every request now flows through one place, that same place can handle SSL, block common exploits, and gate certain services behind an access list. One tool. Multiple wins.

Picking the Right Tool

Four names come up in every "which reverse proxy?" thread: Nginx Proxy Manager, Traefik, Caddy, and HAProxy. Each has strong opinions and a devoted fanbase. Here's how they actually stack up for a homelab.

Tool Web UI Best For Downside
NPM Yes, clean Homelabs, learning Limited to what the UI exposes
Traefik Read-only Docker/Kubernetes stacks Config via labels, steep curve
Caddy None (CLI only) Simple auto-HTTPS setups No UI, config-file only
HAProxy None built-in Enterprise load balancing Overkill for a homelab

I went with NPM. The GUI means I can add a service in 30 seconds without touching a config file, and it runs happily in an LXC with 512 MB of RAM. For a homelab that's supposed to help me learn, not swallow my weekends, that's the sweet spot.

My Network Layout

Before diving in, some context on why the IPs look the way they do. My Proxmox host sits on an HP EliteDesk with a Ryzen 5 PRO 2400GE and 8 GB of RAM, and it bridges two networks:

Keeping services on an isolated internal bridge means devices on the LAN can only reach them through the Proxmox host and its firewall rules. It's one small step toward network segmentation, and it makes future traffic filtering trivial.

Step 1: Spin Up the Container

NPM doesn't need a full VM. An LXC container with Docker inside is faster, lighter, and boots in under a second. From the Proxmox shell:

# Grab a Debian 12 template pveam download local debian-12-standard_12.7-1_amd64.tar.zst # Create the container (nesting=1 is required for Docker) pct create 100 local:vztmpl/debian-12-standard_12.7-1_amd64.tar.zst \ --hostname delta \ --memory 512 \ --cores 1 \ --net0 name=eth0,bridge=vmbr1,ip=192.168.1.12/24,gw=192.168.1.1 \ --storage local-lvm \ --rootfs local-lvm:4 \ --unprivileged 1 \ --features nesting=1 \ --start 1

I named it delta because I'm running out of NATO alphabet slots and the Greek one felt less overused. 512 MB is plenty — NPM idles at around 150 MB in my setup.

Step 2: Docker + NPM

Inside the container, install Docker via the official convenience script and deploy NPM with a docker-compose file:

# Enter the container pct enter 100 # Install Docker apt update && apt upgrade -y apt install -y curl ca-certificates gnupg curl -fsSL https://get.docker.com | sh # Set up the project directory mkdir -p /opt/npm && cd /opt/npm

Then the compose file itself:

# /opt/npm/docker-compose.yml version: "3" services: npm: image: jc21/nginx-proxy-manager:latest container_name: npm restart: unless-stopped ports: - "80:80" # HTTP - "443:443" # HTTPS - "81:81" # Admin UI volumes: - ./data:/data - ./letsencrypt:/etc/letsencrypt environment: DB_SQLITE_FILE: "/data/database.sqlite"
# Launch it docker compose up -d

Point a browser at http://192.168.1.12:81 and log in with admin@example.com / changeme. NPM forces a password change immediately, which is the correct move.

Step 3: Bridging the Two Networks

My devices sit on 192.168.2.0/24, but NPM is on 192.168.1.12. Without a bridge between them, my laptop can't reach NPM at all. That's where iptables DNAT comes in — the Proxmox host catches traffic on ports 80/443 and rewrites the destination to NPM.

# On the Proxmox host — redirect HTTP/HTTPS to NPM iptables -t nat -A PREROUTING -i vmbr0 -p tcp --dport 80 \ -j DNAT --to-destination 192.168.1.12:80 iptables -t nat -A PREROUTING -i vmbr0 -p tcp --dport 443 \ -j DNAT --to-destination 192.168.1.12:443 # Persist across reboots apt install -y iptables-persistent netfilter-persistent save
⚠️

Don't forget the FORWARD chain

DNAT rewrites the destination, but the packet still has to traverse the host. If your FORWARD policy is DROP and you don't have accept rules for vmbr0→vmbr1, packets die silently. Always verify with iptables -L FORWARD -n -v before assuming the proxy is broken.

Step 4: DNS with Technitium

A reverse proxy needs a DNS server pointing every domain to it. I'm running Technitium in another LXC (192.168.1.11), with a wildcard record for *.home.lan:

*.home.lan  →  192.168.2.100  (Proxmox host, vmbr0 side)
                     ↓  iptables DNAT
              192.168.1.12    (NPM, vmbr1 side)
                     ↓  matches Host header
              192.168.1.14:8096  (Jellyfin backend)

The wildcard is the whole trick. Every subdomain of home.lan resolves to the same IP, and NPM picks the right backend from the Host header. Add a new service? Just create a proxy host in NPM — no DNS change needed.

⚠️

Avoid the .local TLD trap

I lost an hour to this. The .local TLD is reserved by mDNS (Bonjour) on Windows and macOS, which means those OSes never send .local queries to your DNS server — they try multicast and fail. Use .lan, .home, or a real domain you own.

Step 5: Wiring Up a Service

Every service follows the same routine: deploy the container, note the IP and port, add a proxy host in NPM. Here's my Jellyfin example, end to end.

1

Open NPM → Hosts → Proxy Hosts → Add Proxy Host

Nothing tricky. The "Add" button is in the top right.

2

Fill in the Details tab

Four fields do the heavy lifting:

  • Domain Names: zeta.home.lan
  • Scheme: http (NPM handles the TLS)
  • Forward Hostname/IP: 192.168.1.14
  • Forward Port: 8096

Turn on Block Common Exploits for free hardening, and Websockets Support because Jellyfin needs it for playback controls.

3

Save and test

Open https://zeta.home.lan. Jellyfin appears. That's the entire workflow for every future service.

HTTPS Without Let's Encrypt

Let's Encrypt won't issue certificates for private TLDs like home.lan — the CA has no way to validate a domain that only exists on your LAN. So for local-only services I use mkcert, which spins up a personal certificate authority and issues certs trusted by every device that installs its root CA.

# On my PC (Windows) winget install FiloSottile.mkcert # Install the root CA in the Windows trust store mkcert -install # Generate a wildcard cert for the whole zone mkcert "*.home.lan" home.lan

Two files pop out: _wildcard.home.lan+1.pem (the certificate) and _wildcard.home.lan+1-key.pem (the key). Upload both in NPM under SSL Certificates → Add SSL Certificate → Custom, then on each Proxy Host go to the SSL tab, pick the wildcard cert, and enable Force SSL.

For phones and other devices, copy the root CA file from mkcert -CAROOT and install it as a trusted certificate. After that, every service in the homelab shows a green padlock. No warnings, no clicking through scary red screens.

What I Actually Use It For

Here's what's currently sitting behind NPM in my setup. Each one is just a Proxy Host entry — five fields, thirty seconds.

🎬
zeta.home.lan

Jellyfin. Movies, series, and my music library, streamed to any device on the LAN.

🛡️
gamma.home.lan

Technitium DNS admin panel. DNS + ad-blocking config, all in one place.

🎯
delta.home.lan

NPM itself. Yes, the reverse proxy proxies its own admin UI. Turtles all the way down.

⚙️
alpha.home.lan

Proxmox web UI. HTTPS + Websockets on, so the console works flawlessly.

🔔
epsilon.home.lan

Uptime Kuma. Monitors every other service and pings me when something goes down.

Lessons Learned

🎯
Wildcard DNS Beats Per-Service Records

I started by adding an A record per service. Then I discovered specific records override the wildcard, and they were pointing to the wrong IP. One * record is enough — let NPM do the routing.

🕵️
DNS Bugs Hide Everywhere

Windows had four DNS servers listed and mine was third in line. It was only ever consulted when the first two timed out. Force the DNS manually on the interface until the setup is stable.

🧱
Segmentation Is Cheap Insurance

Keeping services on vmbr1 costs nothing but a few iptables rules, and it means an exposed service can't be reached without going through the host's firewall first.

🔐
Local HTTPS Is Worth the Effort

mkcert takes 5 minutes to set up and eliminates browser warnings forever. There's zero excuse to run internal services over plain HTTP once you know the trick.

What's Next

NPM is now the single entry point to my homelab, and the pattern for adding services is bulletproof. A few things on the roadmap:

Access Lists — restrict the admin UIs (NPM, Proxmox, Technitium) to my LAN only, so a rogue Proxy Host misconfig can't accidentally expose them.

Wireguard VPN — reach my homelab from outside the network without opening any ports on the Livebox. Combined with the existing DNS setup, my domains should Just Work over the tunnel.

Authelia or Authentik — add SSO in front of the services that don't have their own auth. Because typing passwords into a dozen dashboards gets old.

Building Your Own?

The whole setup took an afternoon, most of it spent chasing DNS quirks. If you've got a Proxmox box gathering dust and more than two services running, NPM is a very cheap upgrade to your quality of life.