Why Your Homelab Needs a Reverse Proxy
A reverse proxy puts your services behind a single entry point with HTTPS and clean domain names. Here's why it matters and how to set one up with Nginx Proxy Manager or Caddy.
Without a reverse proxy, accessing your homelab services looks like this: http://192.168.1.100:8096 for Jellyfin, http://192.168.1.100:8888 for JupyterLab, http://192.168.1.100:9000 for Portainer. Port numbers to remember, no HTTPS, browser warnings on every app.
With a reverse proxy, it’s https://jellyfin.home.yourdomain.com, https://jupyter.home.yourdomain.com, and https://portainer.home.yourdomain.com. Valid certificates. No port numbers. And all traffic routed through a single entry point you control.
That’s the practical case for a reverse proxy. Here’s how to build it.
What a Reverse Proxy Does
A reverse proxy sits in front of your services and routes incoming requests based on the hostname. You expose the proxy on ports 80 and 443. The proxy checks the incoming hostname, looks up which backend service that maps to, and forwards the request — then sends the response back to the client.
The client sees a clean URL with a valid TLS certificate. The backend service doesn’t need to handle HTTPS itself. And you have one place to manage routing, certificates, and access control for everything.
Two Good Options
Nginx Proxy Manager (NPM)
NPM is the most common choice for homelab users. It wraps nginx in a web UI that lets you add proxy hosts, request Let’s Encrypt certificates, and manage redirect rules without touching a config file.
It’s not the most elegant tool — the UI is functional but dated — but it’s accessible to beginners and it handles the common cases well.
Docker Compose setup:
version: "3.8"
services:
npm:
image: jc21/nginx-proxy-manager:latest
container_name: nginx-proxy-manager
ports:
- "80:80"
- "443:443"
- "81:81" # Admin UI
volumes:
- ./data:/data
- ./letsencrypt:/etc/letsencrypt
restart: unless-stopped
Access the admin UI at http://your-server-ip:81. Default credentials: [email protected] / changeme. Change these immediately.
Caddy
Caddy is a modern web server and reverse proxy that handles HTTPS automatically. You write a Caddyfile with your routing rules, and Caddy requests and renews certificates without any additional configuration.
jellyfin.home.yourdomain.com {
reverse_proxy jellyfin:8096
}
portainer.home.yourdomain.com {
reverse_proxy portainer:9000
}
That’s the entire config. Caddy handles cert issuance, renewal, and HTTP-to-HTTPS redirects automatically.
Docker Compose:
version: "3.8"
services:
caddy:
image: caddy:latest
container_name: caddy
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile
- caddy_data:/data
- caddy_config:/config
restart: unless-stopped
volumes:
caddy_data:
caddy_config:
Caddy is cleaner and more modern than NPM. If you’re comfortable with config files, it’s the better long-term choice.
DNS Configuration
For https://jellyfin.home.yourdomain.com to work, DNS needs to point *.home.yourdomain.com (or each subdomain individually) at your server’s IP.
For local-only access: Add entries to your Pi-hole or local DNS server (AdGuard Home, Unbound, or router custom DNS). Point your subdomains at your server’s LAN IP. Certificates still work via DNS challenge (explained below).
For external access: Add DNS records at your registrar pointing to your public IP. Enable port forwarding on your router for ports 80 and 443 to your proxy server.
For the certificates: Let’s Encrypt validates that you control the domain. The HTTP challenge method requires your domain to be publicly reachable. The DNS challenge method proves ownership through your DNS provider’s API — it works even for local-only domains. Both NPM and Caddy support DNS challenge with most major DNS providers (Cloudflare, DigitalOcean, etc.).
If your domain is on Cloudflare, Cloudflare’s DNS API support for Let’s Encrypt DNS challenges is excellent and free.
Putting Your Services Behind NPM
Once NPM is running, adding a service takes 30 seconds:
- Go to Hosts > Proxy Hosts > Add Proxy Host
- Domain name:
jellyfin.home.yourdomain.com - Forward hostname: your server IP (or container name if on the same Docker network)
- Forward port:
8096 - Enable “Block Common Exploits”
- SSL tab: Request a Let’s Encrypt certificate, enable “Force SSL”
Done. NPM handles the cert request and sets up the routing.
Putting Everything on the Same Docker Network
For Caddy or NPM to route to containers by name (instead of IP), they need to be on the same Docker network.
Create a network and attach containers:
networks:
proxy:
external: true
Create it once:
docker network create proxy
Add networks: [proxy] to both your proxy container and each service container. Then in your routing config, use the container name as the hostname.
The Security Consideration
A reverse proxy is not a firewall. Don’t confuse “my services are behind a reverse proxy” with “my services are secure.” The proxy handles HTTPS and routing — it doesn’t protect against vulnerabilities in the services behind it, and it doesn’t stop someone with your URL from attempting to access your Portainer or Vaultwarden.
If you’re exposing services externally, also consider:
- Authelia or Authentik for authentication middleware (adds login before reaching any service)
- Cloudflare Access if your domain is on Cloudflare (zero-trust access control, free tier is generous)
- Keeping some services LAN-only and using Tailscale for remote access
A reverse proxy is infrastructure, not security. Use it for what it’s good at — HTTPS, clean URLs, centralized routing — and add authentication separately.
Start Here
If you’re new to this: install NPM first. The UI makes the initial setup faster, and once everything is working, you can migrate to Caddy later if you want more control.
If you’re comfortable with config files and want a cleaner setup from the start: use Caddy. It’s less clicking, more readable, and the automatic HTTPS handling is genuinely excellent.
Either way, once it’s running, you’ll stop thinking about port numbers — and that’s exactly the point.