← All Articles

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.

networkingreverse-proxyhttpsdockernginx

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:

  1. Go to Hosts > Proxy Hosts > Add Proxy Host
  2. Domain name: jellyfin.home.yourdomain.com
  3. Forward hostname: your server IP (or container name if on the same Docker network)
  4. Forward port: 8096
  5. Enable “Block Common Exploits”
  6. 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:

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.