diff --git a/docs/Reverse-Proxy.md b/docs/Reverse-Proxy.md new file mode 100644 index 0000000..935b0ba --- /dev/null +++ b/docs/Reverse-Proxy.md @@ -0,0 +1,45 @@ +# Reverse Proxy + +[Nginx Proxy Manager](https://nginxproxymanager.com/) sits in front of every service on the LAN, replacing `http://:` with friendly `.home` names and trusted local HTTPS. Runs as its own Compose stack under `nginx-proxy-manager/`. + +![Proxy hosts list](images/npm-proxy-hosts.png) + +## Why + +With a dozen-plus services running, remembering a different port per service didn't scale. NPM proxies each one behind a single hostname, and Pi-hole (already running for network-wide DNS filtering) resolves those hostnames to the server's LAN IP for every device on the network. + +## Port 80/443 conflict with Pi-hole + +Pi-hole runs in `network_mode: host`, which meant it was already bound directly to ports 80 and 443 for its own admin web UI — the same two ports NPM needs for HTTP/HTTPS proxying. Resolved by moving Pi-hole's web interface off those ports entirely via an FTL environment variable: + +```yaml +environment: + FTLCONF_webserver_port: '8082o,[::]:8082o' +``` + +The `o` suffix marks the bind as optional so Pi-hole won't crash if the port's briefly unavailable. Pi-hole's DNS service (port 53) is untouched — only its web UI moved. + +One follow-up gotcha: the first replacement port chosen (8081) turned out to already be in use by qBittorrent's web UI, so the two silently conflicted before landing on 8082. Worth checking `sudo ss -tulnp | grep LISTEN` for actual bound ports before picking a replacement — `docker ps` alone doesn't show ports for host-network containers. + +## Local trusted HTTPS with mkcert + +Since `.home` isn't a real public domain, Let's Encrypt can't issue certificates for it — there's no way to prove domain ownership over the public internet for a name that only resolves on the LAN. [mkcert](https://github.com/FiloSottile/mkcert) solves this by creating a local certificate authority, trusted on each device, that can then sign certificates for any local name. + +**First attempt: a wildcard cert (`*.home`) — this doesn't work.** Browsers reject wildcards on single-label TLDs like `.home` for security reasons (mkcert itself warns about this at generation time). Every proxied host showed `NET::ERR_CERT_COMMON_NAME_INVALID` despite the CA being correctly trusted. + +**Fix: generate one certificate listing every hostname explicitly** instead of a wildcard: + +```bash +mkcert filebrowser.home grafana.home n8n.home openhands.home pihole.home \ + plex.home portainer.home prometheus.home prowlarr.home qbittorrent.home \ + radarr.home sonarr.home whisparr.home sentinel.home +``` + +This produces one cert/key pair covering all listed names, uploaded into NPM as a single Custom SSL Certificate and attached to every proxy host. Adding a new service later just means either regenerating this list to include the new name, or issuing a small separate single-name cert for it — either works, since NPM assigns certificates per proxy host independently. + +**The CA itself only needs installing once per device** (`mkcert -install` on the server, then the resulting root CA file installed into each client's trusted root store — Windows via Certificate Manager, Android via Settings → Encryption & credentials, iOS via a config profile *plus* enabling full trust under Certificate Trust Settings, which is easy to miss). + +## Troubleshooting notes + +- A single host showing a stale/invalid cert after everything else worked turned out to be browser-side TLS session caching, not a real misconfiguration — confirmed by testing the same URL in an Incognito window, where it loaded cleanly. Restarting the NPM container forced a clean reload and resolved it, though clearing HSTS state for the domain (`chrome://net-internals/#hsts`) is a more targeted fix for the same symptom. +- Some LAN devices need their DNS pointed at Pi-hole explicitly (per-device static DNS, or configured once at the router's DHCP settings) for `.home` names to resolve at all — this is separate from the certificate/proxy setup above. diff --git a/docs/images/npm-proxy-hosts.png b/docs/images/npm-proxy-hosts.png new file mode 100644 index 0000000..3eb87a5 Binary files /dev/null and b/docs/images/npm-proxy-hosts.png differ diff --git a/nginx-proxy-manager/compose.yaml b/nginx-proxy-manager/compose.yaml new file mode 100644 index 0000000..b52c732 --- /dev/null +++ b/nginx-proxy-manager/compose.yaml @@ -0,0 +1,15 @@ +services: + nginx-proxy-manager: + image: 'jc21/nginx-proxy-manager:latest' + container_name: npm + restart: unless-stopped + ports: + - '80:80' + - '443:443' + - '81:81' + volumes: + - npm-data:/data + - npm-letsencrypt:/etc/letsencrypt +volumes: + npm-data: + npm-letsencrypt: