In short
- A homelab reverse proxy sits on ports 80 and 443, gets a certificate for each of your subdomains, and forwards each one to the right app, such as
jellyfin.example.comto port 8096. - The two easiest choices differ mainly in how you configure them. Caddy reads a short text file and turns on HTTPS by default.
- Nginx Proxy Manager (NPM) is a web UI on port 81 where you add hosts and request Let's Encrypt certificates by clicking.
- Both run from one Docker Compose file, and both publish arm64 images, so a Raspberry Pi works.
Contents7 sections
Caddy or Nginx Proxy Manager?
| Caddy | Nginx Proxy Manager | |
|---|---|---|
| How you configure it | A Caddyfile you edit and reload | A web admin UI on port 81 |
| HTTPS | Automatic for any site with a domain name. Redirects HTTP to HTTPS and renews certificates | Let's Encrypt, or your own certificates, requested per host |
| DNS challenge (no open ports) | Needs a DNS provider module, which means a custom build (the image has a :builder variant for this) | Built in. NPM installs the Certbot DNS plugin for the provider you pick |
| Extras | The full Caddy server: file serving, matchers, and more | Access lists and basic auth, redirects, streams, 404 hosts, users and an audit log |
| Docker image architectures | amd64, arm64, arm/v7, arm/v6 and more (tag 2.11.4) | amd64 and arm64. armv7 dropped in 2.14+ |
| Version checked 29 Sept 2026 | 2.11.4 (Docker Hub tag) | 2.16.0 (the tag the setup docs pin) |
The decision in one line: if you'd keep your config in git, pick Caddy. If you'd rather click, pick NPM. And if every app should stay on the LAN with no port forwarding, NPM's built-in DNS challenge is the easier route to real certificates.
Do you need a proxy, or a VPN? A proxy is how you publish services on the internet with HTTPS. Jellyfin's hardware guide warns that its server "is not designed to be exposed directly to the internet," which applies to plenty of homelab apps. For anything only you use, a VPN keeps it off the internet entirely. Many homelabs use both.
Prerequisites
- A domain with DNS records you can edit.
- An A/AAAA record per subdomain pointing at your public IP, or one wildcard record.
- Ports 80 and 443 forwarded from your router to the proxy machine. NPM's guide lists exactly this for home networks. Forward only those two. NPM's port 81 is the admin UI.
- Docker with the Compose plugin.
- Nothing else on ports 80 and 443. Pi-hole's Docker example publishes both by default. Its docs suggest remapping the web interface, for example
"8080:80/tcp". The Raspberry Pi 5 starter stack does exactly that.
Run one proxy, not both. They'd compete for the same two ports.
Option A: Caddy
Step 1: create the folders and a Caddyfile
The official image docs mount a conf folder (not the file itself) at /etc/caddy:
mkdir -p caddy/conf caddy/site && cd caddyCreate conf/Caddyfile. Each site gets a block:
jellyfin.example.com {
reverse_proxy 192.168.1.50:8096
}
uptime.example.com {
reverse_proxy 192.168.1.50:3001
}This is adapted from Jellyfin's Caddy page, which uses example.com with reverse_proxy 127.0.0.1:8096. Two changes:
- The upstream address. Jellyfin's example assumes Caddy runs on the host, where
127.0.0.1is the host. Inside a container,127.0.0.1is the container itself, so point at the host's LAN IP and the app's published port instead. - One braced block per hostname, so a single file serves several sites.
Step 2: write compose.yaml
This is the compose example from the official Caddy image docs, with the <version> placeholder filled in:
services:
caddy:
image: caddy:2.11.4
restart: unless-stopped
cap_add:
- NET_ADMIN
ports:
- "80:80"
- "443:443"
- "443:443/udp"
volumes:
- $PWD/conf:/etc/caddy
- $PWD/site:/srv
- caddy_data:/data
- caddy_config:/config
volumes:
caddy_data:
caddy_config:The image docs explain three of these lines:
caddy_datais where Caddy stores certificates and private keys. They say it's "very important to persist the data directory" and that it "must not be treated as a cache."- Don't mount the Caddyfile itself at
/etc/caddy/Caddyfile. Some editors replace the file, and then the container never sees your change. Mount the folder, as above. NET_ADMINis optional. It lets HTTP/3's UDP buffers grow. Keep443:443/udpif you want HTTP/3.
Step 3: start it and watch the certificates
docker compose up -d
docker compose logs -f caddyWhat you should see: Caddy obtaining a certificate for each hostname. Then https://jellyfin.example.com loads with a valid certificate. The Automatic HTTPS page lists the conditions: the domain's A/AAAA records point to your server, ports 80 and 443 are open externally, Caddy can bind to them, the data directory is writable and persistent, and the domain appears in the config. If all of those hold, "sites will be served over HTTPS automatically."
Step 4: reload after changes
After editing the Caddyfile, use the image docs' zero-downtime reload:
docker compose exec -w /etc/caddy caddy caddy reloadThe reverse_proxy docs say Caddy sets X-Forwarded-For, X-Forwarded-Proto and X-Forwarded-Host itself, and proxies WebSockets without extra configuration.
LAN-only names with Caddy
Hostnames such as .local, .internal, .home.arpa and bare IP addresses don't qualify for public certificates. Caddy still serves them over HTTPS, signing them with its own local certificate authority. Every device then has to trust Caddy's root certificate, or it shows security errors. The alternative is a real domain with the DNS challenge. For Caddy that needs a DNS provider module compiled in, using the image docs' builder pattern. The Jellyfin docs add a caution for that route: a DNS API token in the config can compromise your domain if it's misconfigured, so give it the least permissions you can.
Option B: Nginx Proxy Manager
Step 1: write compose.yaml
From NPM's quick-setup guide:
services:
app:
image: 'jc21/nginx-proxy-manager:2.16.0'
restart: unless-stopped
environment:
TZ: "Australia/Brisbane"
ports:
- '80:80'
- '81:81'
- '443:443'
volumes:
- ./data:/data
- ./letsencrypt:/etc/letsencryptChange TZ to your own timezone. The guide calls this "the bare minimum configuration". It uses SQLite in ./data. MySQL/MariaDB and Postgres are optional, and the setup page has examples for both.
Step 2: start it
docker compose up -dOn first run, the setup page says NPM generates JWT keys, initialises the database and creates the admin user, which "can take a couple of minutes depending on your machine."
Step 3: log in (there's no default password to look up)
Open http://<server-ip>:81. Current docs describe a first-user setup screen in the UI: you create the admin account there. To skip that screen, the Advanced Configuration page lets you set INITIAL_ADMIN_EMAIL and INITIAL_ADMIN_PASSWORD in environment:. If you do, keep the real password out of any file you commit.
What you should see: the NPM dashboard.
Step 4: add a proxy host with a certificate
In the UI, add a proxy host for jellyfin.example.com, forward it to 192.168.1.50 port 8096, and request a Let's Encrypt certificate for it. NPM's guide lists this as its core job: "easily create forwarding domains... without knowing anything about Nginx", with "free SSL using Let's Encrypt". The docs don't walk through each screen, so follow the UI's own forms.
For internal-only hosts, pick a DNS challenge when requesting the certificate. The Certbot page says NPM installs the matching Certbot DNS plugin for your provider. It also warns that the plugins are third-party, and that "using more than one DNS provider in the same Nginx Proxy Manager instance may introduce Python dependency conflicts."
Upgrading NPM
From the upgrading page:
docker compose pull
docker compose up -dNPM updates its own database. Check the release notes for version-specific steps. Pinning the tag, as above, means you choose when an upgrade happens: change 2.16.0 when you're ready.
Per-app notes that trip people up
- Nextcloud needs to know it's behind a proxy. Set
TRUSTED_PROXIESand related variables before the first start. See the Nextcloud Docker Compose guide. - Vaultwarden requires HTTPS for its web vault. That's often the reason to set up a proxy in the first place. See how to self-host Vaultwarden on a Raspberry Pi.
- Jellyfin on a subpath (
example.com/jellyfin) needs a Base URL set in Jellyfin first, plusredir /jellyfin /jellyfin/andreverse_proxy /jellyfin/* 127.0.0.1:8096in Caddy (per the Jellyfin docs). A subdomain is simpler.
Troubleshooting (documented issues only)
- No certificate, and "rate limit" errors. Caddy's docs warn that repeated failed attempts against Let's Encrypt can hit rate limits that "block your access to HTTPS for up to a week." While testing, point Caddy's ACME endpoint at Let's Encrypt staging (
https://acme-staging-v02.api.letsencrypt.org/directory). Caddy also switches to staging itself during retries. - The HTTP challenge fails. Port 80 must be reachable from the internet and forwarded to the proxy. The TLS-ALPN challenge needs the same for 443. Check the router's forwarding and that the DNS record points at your current public IP.
- Certificates vanish after recreating the Caddy container. The
/datavolume wasn't persisted. - Caddyfile edits are ignored. You mounted the file rather than the folder, or you didn't reload.
- You can't log in to an app after adding an NPM access list. The FAQ explains this: basic-auth access lists and apps with their own login both use the
Authorizationheader, and "one of the two logins will be broken." Remove one of them. - NPM won't run on an older Pi. armv7 isn't supported in 2.14+. The docs point 32-bit users to the
2.13.7tag. A 64-bit OS is the better fix. - The proxy won't start: port 80 or 443 in use. Another container, often Pi-hole's web UI, holds it. Remap that container's ports.
What to do next
- Put the proxy in front of your Pi services: Raspberry Pi 5 starter stack.
- Publish Jellyfin safely after turning on Intel Quick Sync transcoding.
- Would rather not run a proxy yourself? Dokploy and Coolify manage Traefik (or Caddy, on Coolify) for you. See Coolify vs Dokploy.
Sources (14)ShowHide
- Caddy docs: Reverse proxy quick-start · accessed 2026-09-29
- Caddy docs: Automatic HTTPS · accessed 2026-09-29
- Caddy docs: reverse_proxy directive (headers, WebSockets) · accessed 2026-09-29
- Official Caddy Docker image docs (docker-library/docs caddy/content.md) · accessed 2026-09-29
- Docker Hub: caddy tag 2.11.4 (architectures) · accessed 2026-09-29
- Nginx Proxy Manager: Guide (quick setup, home network basics) · accessed 2026-09-29
- Nginx Proxy Manager: Full Setup Instructions · accessed 2026-09-29
- Nginx Proxy Manager: Advanced Configuration (initial user creation) · accessed 2026-09-29
- Nginx Proxy Manager: FAQ · accessed 2026-09-29
- Nginx Proxy Manager: Certbot DNS plugins · accessed 2026-09-29
- Nginx Proxy Manager: Upgrading · accessed 2026-09-29
- Docker Hub: jc21/nginx-proxy-manager tags (2.16.0 pushed 2026-09-24) · accessed 2026-09-29
- Jellyfin: Reverse proxy with Caddy · accessed 2026-09-29
- Pi-hole docs: Docker tips and tricks (port conflicts) · accessed 2026-09-29