HomeTechOps

Backups & Storage

Home reverse proxy: NPM, Caddy, Traefik, or Pangolin

Once you run 4+ self-hosted services at home, manually managing TLS certs and remembering port numbers becomes the worst part of operating the stack. A reverse proxy fixes both: services live behind nice URLs like `jellyfin.example.com`, get automatic Let's Encrypt certs, and you stop port-forwarding individual services to the internet. The decision is which proxy — Nginx Proxy Manager (NPM, click-through GUI, beginner-friendly), Caddy (single binary, Caddyfile config-as-code, lightest), Traefik v3 (Docker-label-driven, container-auto-discovery), or Pangolin (newer, built-in identity layer, runs as Cloudflare Tunnel alternative). Most operators start with NPM and migrate when they outgrow GUI clicks.

Reverse proxy topology and the mixed-platform pattern

Reference images and diagrams. Click any image to view full resolution.

Three-tier home network diagram showing Trusted / IoT / Guest VLANs separated by router. Used here to illustrate where the reverse proxy sits in the topology: typically on the Trusted VLAN, with services on other LAN IPs reached internally by the proxy.
Original concept diagram (not vendor copyright). The reverse proxy sits on the Trusted VLAN (or its own internal network), with port 80/443 forwarded to it from the router. It reaches services on other LAN IPs (Synology, Unraid, Raspberry Pi) by their static internal IPs. Public traffic only enters via the proxy, never directly to the upstream services.

Who this is for

Operators running 4+ self-hosted services at home (Plex, Jellyfin, Home Assistant, Nextcloud, Immich, *arr stack, Vaultwarden, Frigate) who want them behind nice URLs with automatic TLS, without manually managing certs per service.

Outcome

A working reverse proxy serving all your home services under wildcard `*.example.com`, with the right tool for your operating style (NPM for GUI, Caddy for config-as-code, Traefik for Docker-label-driven, Pangolin for CGNAT). Public services have SSO in front; admin services stay on Tailscale.

Required inputs

  • A domain you control (Cloudflare-hosted DNS recommended for DNS-01 wildcard certs).
  • One always-on host on the LAN to run the reverse proxy (Unraid most common; dedicated Raspberry Pi 4/5 is the second-most-recommended).
  • Port 80 + 443 forwarded from the router to the proxy host (unless behind CGNAT — then Pangolin or Cloudflare Tunnel replaces port forwarding).
  • List of services to put behind the proxy, with their LAN IPs and ports.
GuideFollow in order

Step-by-step procedure

1

Decide which proxy fits your operating style

Do: GUI + visual debugging → NPM. Config-as-code + lightest → Caddy. Docker-label-driven + container auto-discovery → Traefik. CGNAT + multi-site → Pangolin. Most home operators start with NPM.

Expected result: Choice made; matches your comfort with text config vs UI clicking.

If not: If unsure, default to NPM — the migration path NPM → Caddy is straightforward; reverse migration is painful.

2

Set up DNS + wildcard Let's Encrypt cert (DNS-01)

Do: Move domain DNS to Cloudflare. Create API token at dash.cloudflare.com > My Profile > API Tokens with scope `Zone:Zone:Read + Zone:DNS:Edit` for your zone. Configure your proxy's DNS-01 provider with the token; request `*.example.com` cert.

Expected result: Cert issued for `*.example.com`. Renewal scheduled. The proxy can serve any new subdomain without requesting a new cert.

If not: If cert request fails with 'No valid A/AAAA records', API token lacks Zone scope. Recreate token with correct scope.

3

Forward router port 80 + 443 to the proxy host

Do: Router admin > Port Forwarding > add 80 → proxy_host:80 and 443 → proxy_host:443. Reserve the proxy host's IP via DHCP reservation so it doesn't change.

Expected result: From outside cellular, `https://test.example.com` reaches the proxy (404 or similar response — meaning the request landed).

If not: If port 80 from outside is unreachable, you may be CGNAT'd or your ISP blocks inbound 80/443 — see /fix/cgnat-home-access-no-public-ip.

4

Add services to the proxy one at a time, simplest first

Do: Start with a stateless service (Uptime Kuma, Homepage, Grafana). Add it as a proxy host: `service.example.com` → `192.168.1.X:port`. Test from cellular before moving to the next.

Expected result: Service reachable at `https://service.example.com` with the wildcard cert valid (no browser warnings).

If not: If browser shows cert warning, the wildcard cert isn't being served — recheck the proxy's TLS config; verify cert is for `*.example.com` not just `example.com`.

5

Add tricky services with their specific quirks

Do: Plex: enable WebSocket pass-through; set Plex's Custom Server Access URLs. Home Assistant: configure `http.trusted_proxies` in configuration.yaml; WebSocket read_timeout 0. Nextcloud: client_max_body_size 16G + timeouts 3600s. Jellyfin: WebSocket + disable buffering. Immich: max body 50GB.

Expected result: Each service works through the proxy without errors. WebSocket-dependent features (Plex sync, HA real-time, Jellyfin SyncPlay) function.

If not: Service-specific failures usually trace to WebSocket pass-through, body-size limits, or X-Forwarded headers. See the per-service section.

6

Set up split-horizon DNS for internal-LAN access

Do: On your self-hosted DNS server (Pi-hole, AdGuard Home, Technitium), add wildcard A record `*.example.com → proxy_LAN_IP`. Internal clients hitting `jellyfin.example.com` go directly to the proxy on the LAN, never traversing WAN.

Expected result: Phone on home Wi-Fi loads `https://jellyfin.example.com` and DNS resolves to the proxy LAN IP, not the WAN IP.

If not: If clients still resolve to WAN IP, the local DNS server isn't being used — check DHCP issued DNS, or set static DNS on each client.

7

Add SSO in front of sensitive services (Authentik or Authelia)

Do: Install Authentik (new builds) or Authelia (minimal). Create one provider + one application for a test service. Wire the proxy to use ForwardAuth pointing at the SSO endpoint.

Expected result: Hitting the protected subdomain redirects to the SSO login; after login, the proxied service is reachable.

If not: If the redirect loop happens, check the ForwardAuth header pass-through — common bug is the proxy not forwarding the auth cookie back.

8

Keep admin services on Tailscale only — don't proxy them

Do: Sonarr, Radarr, qBittorrent, Proxmox UI, Container Manager admin UIs: leave LAN-only and reach them via Tailscale, not the public proxy. Even with SSO in front, admin UIs are too high-value to be reachable from a public hostname.

Expected result: These services have no public hostname; you reach them by Tailscale IP (or LAN IP from inside).

If not: If you've already exposed admin UIs publicly, set up Tailscale access and remove the public hostname.

Commands and settings paths

Verify wildcard cert installed correctly

openssl s_client -connect service.example.com:443 -servername service.example.com 2>/dev/null | openssl x509 -noout -subject -dates

Where: From any internet-reachable workstation.

Expected: Subject shows `CN = *.example.com`; notAfter date is 60+ days in the future after a renewal.

Failure means: Cert is per-host (CN = service.example.com) instead of wildcard, OR renewal failed and cert is expiring.

Safe next step: Re-request wildcard via the proxy's DNS-01 config; check API token scope; check cert renewal job in proxy logs.

Check proxy can reach upstream service

From proxy host: `curl -k https://192.168.1.X:port/`

Where: SSH to the proxy host.

Expected: Service responds (200 / 302 / login page). Confirms the upstream is reachable from the proxy.

Failure means: Service is down, wrong IP/port, or firewall blocking proxy-to-service traffic.

Safe next step: Verify service is running; confirm IP + port; check any LAN firewall rules.

Evidence to record

  • Proxy config (Caddyfile, Traefik labels, NPM exported JSON) committed to Git.
  • Cloudflare API token + zone scope screenshotted (mask the token itself).
  • Test results: each service reachable via public URL with wildcard cert; SSO works on protected ones; split-horizon DNS resolves internally.
  • Cert renewal date noted (Let's Encrypt 90-day cycle; renew at 60 days).

Common mistakes

  • Exposing admin UIs (Sonarr, Radarr, Proxmox, NAS admin) through the public proxy — even with SSO in front, the attack surface is too high.
  • Per-service certs instead of wildcard — hits Let's Encrypt's 5-duplicate-per-week limit during dev iteration; wildcard avoids the limit entirely.
  • Forgetting the `trusted_proxies` config in Home Assistant — HA refuses external requests with HTTP 400 until you set `http.use_x_forwarded_for: true` + the proxy IP.
  • Routing Nextcloud large-file sync through Cloudflare Tunnel (100 MB cap) instead of through the home reverse proxy directly.
  • Running the proxy on the same box as Plex with `network_mode: host` on both — they fight over port 80/443.

Stop points

  • Stop before exposing any service publicly without an SSO/auth layer if the service has sensitive controls (NAS admin, smart-home control of locks/cameras, file deletion).
  • Stop before counting on Cloudflare Tunnel for personal video streaming (Plex/Jellyfin) — CDN terms restrict it; multiple operators report account warnings.
  • Stop before migrating production traffic from NPM → Caddy without keeping NPM running on an alternate port for a week as fallback.

Last reviewed

2026-05-18

Source-backed checks

HomeTechOps turns official docs and conservative safety rules into a shorter runbook. These links are the source trail for the page direction.