Nginx Proxy Manager: The Easy Reverse Proxy for Your Home Lab
Once you're running more than a couple of services in your home lab, you hit an annoying problem: everything is accessed by IP address and port number. Jellyfin is at 192.168.1.50:8096, Grafana is at 192.168.1.50:3000, Nextcloud is at 192.168.1.50:8081. Nobody wants to remember port numbers, and you definitely don't want to expose multiple ports to the internet.
A reverse proxy solves this by sitting in front of all your services and routing traffic based on hostname. You access jellyfin.home.example.com, and the reverse proxy forwards the request to the right service on the right port. One entry point, clean hostnames, and the ability to add SSL certificates so everything is encrypted.
Nginx Proxy Manager (NPM) puts a user-friendly web UI on top of Nginx, one of the most battle-tested web servers in existence. You get the power and reliability of Nginx without ever touching a configuration file. If you want a reverse proxy that just works and doesn't require you to learn a domain-specific configuration language, NPM is the answer.
Why a Reverse Proxy?
Before diving into setup, here's what a reverse proxy actually gives you:
Single port exposure. Instead of opening ports 3000, 8080, 8096, 8443, 9000, and a dozen others on your firewall, you expose port 80 and 443. The reverse proxy handles everything behind those two ports.
Hostname-based routing. Access services by name instead of IP:port. grafana.home.lab, jellyfin.home.lab, nextcloud.home.lab — each resolves to the same IP but routes to a different backend.
SSL everywhere. NPM integrates with Let's Encrypt for automatic, free SSL certificates. Every service gets HTTPS with zero effort after initial setup.
Access control. Block services from external access, require authentication for sensitive dashboards, or restrict access by IP range — all configurable through the web UI.
Centralized logging. Every request flows through the proxy, giving you a single place to monitor traffic and debug issues.
Installing Nginx Proxy Manager
NPM runs in Docker. Create a directory and a compose file:
mkdir -p ~/docker/nginx-proxy-manager
cd ~/docker/nginx-proxy-manager
# docker-compose.yml
services:
npm:
image: jc21/nginx-proxy-manager:latest
container_name: nginx-proxy-manager
restart: unless-stopped
ports:
- "80:80" # HTTP
- "443:443" # HTTPS
- "81:81" # Admin UI
volumes:
- ./data:/data
- ./letsencrypt:/etc/letsencrypt
environment:
TZ: "America/New_York"
Start it:
docker compose up -d
Wait about 30 seconds, then access the admin UI at http://YOUR_SERVER_IP:81.
Default Login
- Email:
admin@example.com - Password:
changeme
You'll be prompted to change these immediately on first login. Do it. Set a real email address and a strong password.
DNS Setup
Before NPM can route traffic by hostname, those hostnames need to resolve to your server's IP. You have several options:
Option 1: Local DNS (Pi-hole, AdGuard Home, or Router)
If you run Pi-hole or AdGuard Home, add local DNS records:
grafana.home.lab -> 192.168.1.50
jellyfin.home.lab -> 192.168.1.50
nextcloud.home.lab -> 192.168.1.50
All hostnames point to the same IP — the server running NPM. NPM differentiates based on the hostname in the HTTP request.
Option 2: Wildcard DNS
Some DNS servers let you create a wildcard record. In your router or DNS server:
*.home.lab -> 192.168.1.50
Now any subdomain of home.lab resolves to your server. This is the most convenient approach.
Option 3: /etc/hosts (Quick and Dirty)
On each client machine, edit /etc/hosts:
192.168.1.50 grafana.home.lab jellyfin.home.lab nextcloud.home.lab
This works but doesn't scale. You'll need to update every client when you add a service. Fine for testing, bad for production.
Option 4: Real Domain with Split DNS
If you own a domain (e.g., example.com), create A records or a wildcard in your public DNS pointing to your server's local IP:
*.home.example.com -> 192.168.1.50
This works for devices on your local network. For external access, point to your public IP and configure port forwarding on your router.
Adding Your First Proxy Host
From the NPM dashboard, click Hosts > Proxy Hosts > Add Proxy Host.
Fill in:
- Domain Names:
grafana.home.lab - Scheme:
http(the connection between NPM and your backend service) - Forward Hostname / IP:
192.168.1.50(or the Docker host IP) - Forward Port:
3000(Grafana's port) - Block Common Exploits: Enable
- Websockets Support: Enable (needed for many modern apps)
Click Save. That's it. Visit http://grafana.home.lab in your browser, and you'll see Grafana.
If Your Backend Is Also in Docker
When NPM and the target service are on the same Docker host, you can use the container name instead of the IP — but only if they're on the same Docker network.
# Create a shared network
docker network create proxy-network
Add the network to both NPM and the target service:
# In NPM's docker-compose.yml
services:
npm:
# ... existing config ...
networks:
- proxy-network
- default
networks:
proxy-network:
external: true
# In Grafana's docker-compose.yml
services:
grafana:
# ... existing config ...
networks:
- proxy-network
networks:
proxy-network:
external: true
Now in NPM, set the forward hostname to the container name (e.g., grafana) and the forward port to the container's internal port (e.g., 3000). No need for ports mapping on the backend service at all — NPM connects directly via the Docker network.
This is the preferred setup. Backend services don't expose any ports to the host, and all traffic flows through NPM.
Automatic SSL with Let's Encrypt
NPM makes SSL trivially easy. When adding or editing a proxy host, click the SSL tab:
- SSL Certificate: Select "Request a New SSL Certificate"
- Force SSL: Enable (redirects HTTP to HTTPS)
- HTTP/2 Support: Enable
- HSTS Enabled: Enable (tells browsers to always use HTTPS)
- Email Address: Your email for Let's Encrypt notifications
- Check "I Agree to the Let's Encrypt Terms"
Click Save, and NPM requests a certificate from Let's Encrypt automatically. Certificates are valid for 90 days and auto-renew.
Requirements for Let's Encrypt
Let's Encrypt validates domain ownership via HTTP-01 or DNS-01 challenges.
HTTP-01 (default): Requires that port 80 on your public IP reaches NPM. This means:
- Your domain's DNS record points to your public IP
- Port 80 is forwarded on your router to NPM's host
DNS-01: Validates via a DNS TXT record. NPM supports this for several DNS providers (Cloudflare, DigitalOcean, AWS Route 53, and others). This is the better option because:
- No need to open port 80 to the internet
- Supports wildcard certificates (
*.home.example.com)
To use DNS-01 with Cloudflare:
- In NPM, go to SSL Certificates > Add SSL Certificate > Let's Encrypt
- Enter
*.home.example.comas the domain - Check "Use a DNS Challenge"
- Select Cloudflare as the provider
- Enter your Cloudflare API token (create one with Zone:DNS:Edit permissions)
NPM creates the validation TXT record automatically and retrieves the wildcard certificate. Apply this certificate to any proxy host under home.example.com.
Self-Signed Certificates (Internal Only)
For purely internal services where you don't have a public domain, NPM can generate self-signed certificates. Your browser will warn about them, but the connection will still be encrypted.
Under the SSL tab, select "Request a New SSL Certificate" with the self-signed option. Or upload your own certificates from a private CA if you've set one up.
Access Lists
Access lists control who can reach a service. Useful for restricting admin panels, development services, or anything you don't want publicly accessible.
Go to Access Lists > Add Access List:
- Name: "Local Network Only"
- Under Access, add:
allow 192.168.1.0/24(your local subnet)deny all
Or create a password-protected access list:
- Under Authorization, add usernames and passwords
- This adds HTTP Basic Auth in front of the service
Apply access lists to proxy hosts from the host's settings. Multiple proxy hosts can share the same access list.
Practical Access List Examples
Admin services (local only):
allow 192.168.1.0/24
allow 10.0.0.0/8
deny all
VPN users + local:
allow 192.168.1.0/24
allow 100.64.0.0/10 # Tailscale range
deny all
Everyone, but with authentication: Create an authorization entry with username/password. Useful for services that don't have their own auth.
Custom Locations and Advanced Configuration
Sometimes you need more than simple proxying. NPM supports custom locations and advanced Nginx configuration.
Custom Locations
In a proxy host's settings, click the Custom Locations tab. Add a location:
- Location:
/api - Scheme:
http - Forward Hostname:
api-server - Forward Port:
8000
This routes /api/* requests to a different backend than the main path. Useful for microservice architectures or splitting traffic.
Advanced Configuration
The Advanced tab lets you inject raw Nginx directives. This is where you go for anything NPM's UI doesn't cover:
# Increase upload size limit (default is 1MB)
client_max_body_size 100M;
# Custom headers
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# WebSocket timeout adjustment
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
# Cache static assets
location ~* \.(jpg|jpeg|png|gif|ico|css|js)$ {
expires 7d;
add_header Cache-Control "public, no-transform";
}
Common reasons to use the advanced tab:
- File upload services (Nextcloud, Immich): Increase
client_max_body_size - WebSocket apps (Uptime Kuma, code-server): Adjust timeouts
- Specific security headers: Add Content-Security-Policy, X-Frame-Options, etc.
NPM vs. Traefik vs. Caddy
NPM isn't the only reverse proxy option for home labs. Here's how it compares to the two other popular choices.
Traefik
Traefik is a cloud-native reverse proxy that integrates deeply with Docker and Kubernetes. Instead of configuring routes in a UI, you add labels to your Docker containers and Traefik auto-discovers them.
Traefik is better if:
- You're running Kubernetes (k3s bundles it)
- You want fully automated service discovery
- You prefer infrastructure-as-code (everything in Docker labels or config files)
- You don't want a separate management UI
NPM is better if:
- You prefer a visual UI for managing routes
- You have non-Docker services to proxy (bare metal, VMs, LXC containers)
- You want quick changes without editing YAML
- You're less comfortable with Traefik's configuration model
Traefik's learning curve is steeper. Its configuration system (providers, routers, services, middlewares, entrypoints) is powerful but verbose. When it works, it's elegant. When something breaks, debugging is harder than NPM's straightforward approach.
Caddy
Caddy is a modern web server that handles HTTPS automatically. Its configuration file (Caddyfile) is remarkably simple:
grafana.home.lab {
reverse_proxy localhost:3000
}
jellyfin.home.lab {
reverse_proxy localhost:8096
}
That's the entire config for two services, including automatic HTTPS.
Caddy is better if:
- You want the absolute simplest configuration files
- You're comfortable editing text files
- Automatic HTTPS with zero configuration is important
- You want a lightweight, single-binary proxy
NPM is better if:
- You want a web UI
- You have team members who aren't comfortable with CLI
- You need access lists and authentication built in
- You want to manage SSL certificates visually
The Honest Recommendation
If you're comfortable with the command line and prefer clean config files, Caddy is the best home lab reverse proxy. Its Caddyfile syntax is a joy.
If you want a GUI and point-and-click simplicity, NPM is the right choice. It's especially good for home labs that mix Docker and non-Docker services.
If you're running Kubernetes or want deep Docker integration with zero manual route configuration, Traefik wins.
All three are excellent. You won't regret any of them. NPM is the one most people start with because the learning curve is the flattest.
Monitoring and Maintenance
Checking Logs
NPM stores access and error logs that you can view from the dashboard. For more detail:
# View NPM container logs
docker logs nginx-proxy-manager -f --tail 100
# Access Nginx logs inside the container
docker exec nginx-proxy-manager cat /data/logs/fallback_access.log
Updating NPM
cd ~/docker/nginx-proxy-manager
docker compose pull
docker compose up -d
NPM's data persists in the ./data and ./letsencrypt volumes, so updates are non-destructive.
Backing Up NPM
The entire NPM state lives in the ./data directory (SQLite database and Nginx configs) and ./letsencrypt (SSL certificates):
# Backup
tar czf npm-backup-$(date +%Y%m%d).tar.gz data/ letsencrypt/
# Restore
tar xzf npm-backup-20260209.tar.gz
docker compose up -d
Common Issues
Port 80 or 443 already in use. If another service is using these ports, NPM can't start. Check what's occupying them:
sudo ss -tlnp | grep -E ':80|:443'
Stop the conflicting service or change its ports.
SSL certificate not renewing. Certificates auto-renew 30 days before expiration. If renewal fails, check that:
- Port 80 is reachable (for HTTP-01 challenges)
- DNS records are correct (for DNS-01 challenges)
- The NPM container has internet access
You can manually trigger renewal from the SSL Certificates page in the admin UI.
502 Bad Gateway. NPM can reach the internet but not your backend service. Check:
- Is the backend service running?
- Is the hostname/IP correct in the proxy host config?
- Is the port correct?
- If using Docker networking, are both containers on the same network?
504 Gateway Timeout. The backend is reachable but too slow. Add to the advanced config:
proxy_connect_timeout 300s;
proxy_read_timeout 300s;
proxy_send_timeout 300s;
Putting It All Together
A typical NPM setup for a home lab looks like this:
- NPM runs in Docker on ports 80, 443, and 81
- Wildcard DNS (
*.home.lab) points to the Docker host - Backend services run in Docker on the same network, no ports exposed to the host
- SSL certificates via Let's Encrypt DNS-01 challenge (wildcard cert)
- Access lists restrict admin UIs to the local network
- External access via port forwarding 80/443 to NPM, with only specific services exposed
This gives you a clean, secure, maintainable setup where adding a new service takes about 60 seconds in the NPM UI. No config files to edit, no Nginx syntax to remember, no certificates to manage manually. It's the lowest-friction way to get professional-grade reverse proxying in a home lab.