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.
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.
Step-by-step procedure
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.
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.
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.
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`.
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.
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.
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.
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.