Why I needed this
Have a home lab with multiple services - Plex, Jellyfin, Home Assistant, various dashboards, a couple of self-hosted apps. Was accessing them via IP:port like http://192.168.1.100:8090.
Wanted proper domain names and HTTPS. Looked into getting SSL certificates - meant dealing with certbot, DNS challenges, nginx config files, renewals that fail. Sounded like a nightmare.
Then someone mentioned Nginx Proxy Manager (NPM). Web UI for nginx, automatic Let's Encrypt certificates, one-click reverse proxy setup.
What NPM actually does
Web-based nginx management. Create proxy hosts, generate SSL certificates, set up access controls - all through a browser. No editing config files manually.
Best part: SSL certificates auto-renew. Never deal with expired certificates again.
So what is Nginx Proxy Manager
It's a Docker container that runs nginx and provides a web UI for managing it. Built on top of nginx but adds features like:
- Web UI: No more editing nginx.conf files
- Free SSL: Automatic Let's Encrypt certificates
- Multiple domains: Manage all your services from one place
- Access lists: Password protect services easily
- Load balancing: Distribute traffic across multiple servers
- Custom locations: Proxy specific paths to different services
- WebSocket support: Works out of the box
- Rate limiting: Protect against abuse
Why it's better than raw nginx:
- Don't need to learn nginx syntax
- SSL certificate handling is automatic
- See all your proxies in one dashboard
- Easier to debug - logs in the UI
- Can share access with team members
Underneath it's still nginx. You can drop into config files if you need something advanced. But 95% of use cases work through the UI.
Getting NPM running
Prerequisites
You'll need:
- Docker installed
- A domain name (or subdomains) pointed to your server
- Ports 80, 443, and 81 available
- Basic understanding of what reverse proxy does
Docker Compose setup
Create a docker-compose.yml:
version: '3'
services:
app:
image: 'jc21/nginx-proxy-manager:latest'
restart: unless-stopped
ports:
- '80:80' # HTTP
- '443:443' # HTTPS
- '81:81' # Management UI
volumes:
- ./data:/data
- ./letsencrypt:/etc/letsencrypt
environment:
- TZ=America/New_York
Then start it:
docker compose up -d
First login
Go to http://your-server-ip:81
Default credentials:
Email: admin@example.com
Password: changeme
Change them immediately. Then you're ready to add proxies.
Port 81 is for the management UI. Ports 80 and 443 are for the actual proxy traffic.
Setting up your first proxy
Example: Plex server
Let's say Plex runs on http://192.168.1.100:32400. Want to access it via https://plex.yourdomain.com
In NPM UI:
# 1. Click "Hosts" → "Add Proxy Host"
# 2. Fill in details:
Domain Names: plex.yourdomain.com
Scheme: http
Forward Hostname/IP: 192.168.1.100
Forward Port: 32400
# 3. Enable SSL
Select "Request a new SSL Certificate"
Email: your@email.com
Agree to Let's Encrypt TOS
Enable "Force SSL" (redirect HTTP to HTTPS)
# 4. Click "Save"
That's it. NPM handles the rest.
What happens behind the scenes
NPM automatically:
- Creates nginx config for your proxy
- Requests SSL certificate from Let's Encrypt
- Sets up HTTP-01 challenge on port 80
- Configures SSL certificate
- Enables HTTPS redirect
- Will auto-renew certificate before expiry
Accessing local services
For services on your local network, you'll need:
# Point DNS A record to your public IP
# Or use local DNS (Pi-hole, AdGuard Home)
# For local access without going through internet:
# Add entry to /etc/hosts or use local DNS
Managing SSL certificates
DNS Challenge (for local networks)
If you're using local IPs or don't want to open ports, use DNS challenge:
# In SSL Certificate settings:
Select "DNS Challenge"
# Choose your DNS provider (Cloudflare, Google, etc.)
Add API credentials
# Pros:
- Works for local IPs
- Can use wildcard certificates (*.yourdomain.com)
- Doesn't require port 80 to be open
# Cons:
- Need API access to DNS provider
- Slightly more complex setup
Wildcard certificates
Cover all subdomains with one certificate:
# Domain Names: *.yourdomain.com
# Select DNS Challenge
# One cert covers everything:
# - plex.yourdomain.com
# - home.yourdomain.com
# - anything.yourdomain.com
Custom certificates
Have your own SSL cert? Upload it:
# SSL Certificates → Add Custom Certificate
# Upload certificate and key
# Use for any proxy host
Certificate renewal
NPM auto-renews Let's Encrypt certs. Check status:
# Dashboard → SSL Certificates
# See expiry dates
# Manually renew if needed
# View renewal logs
Advanced configuration
Access lists
Password protect services:
# Proxy Host → Access List
# Create new list:
Name: Internal Only
Auth: Basic Authentication
Username: admin
Password: your-password
# Assign to proxy host
# Now requires username/password to access
Custom locations
Proxy specific paths to different services:
# Example: /api to backend, / to frontend
Domain: api.yourdomain.com
# Location: /
Forward: http://frontend:3000
# Location: /api
Forward: http://backend:8000
# Useful for SPA + API setups
WebSocket support
Enable WebSockets (needed for some apps):
# Proxy Host → Advanced → Custom Nginx Configuration
# Enable WebSockets support
# NPM adds the necessary nginx config
# Works for Home Assistant, WebSocket apps, etc.
Load balancing
Distribute traffic across multiple servers:
# Add multiple upstream hosts
Forward Host 1: 192.168.1.100:3000
Forward Host 2: 192.168.1.101:3000
Forward Host 3: 192.168.1.102:3000
# NPM load balances across them
# Automatically removes dead servers
Rate limiting
Protect against abuse:
# Proxy Host → Advanced → Rate Limiting
# Enable: 100 requests per 10 seconds
# Block client for 60 seconds when exceeded
# Good for public-facing services
Custom nginx config
Need something custom? Drop into raw nginx:
# Proxy Host → Advanced → Custom Nginx Configuration
# Add custom nginx directives
# Example: increase upload size
client_max_body_size 100M;
# Example: custom headers
add_header X-Frame-Options "SAMEORIGIN";
add_header X-Content-Type-Options "nosniff";
Real stuff I proxy with NPM
Home Assistant
# Domain: home.yourdomain.com
# Forward: http://192.168.1.50:8123
# WebSocket: Enabled
# SSL: Let's Encrypt
# Access: Basic auth + trusted networks
Media servers
# Plex: plex.yourdomain.com → 192.168.1.100:32400
# Jellyfin: jellyfin.yourdomain.com → 192.168.1.101:8096
# All with SSL, accessible from anywhere
Self-hosted apps
# Nextcloud: cloud.yourdomain.com
# Vaultwarden: vault.yourdomain.com
# Paperless-ngx: docs.yourdomain.com
# Grafana: stats.yourdomain.com
# All behind HTTPS, auto-renewing certs
Development
# Local dev: dev1.yourdomain.com → localhost:3000
# Testing: test.yourdomain.com → localhost:3001
# Wildcard cert covers all subdomains
Problems I hit
SSL certificate fails
Let's Encrypt keeps failing.
# Fixed by
1. Checking DNS actually points to server (dig command)
2. Making sure port 80 is open and reachable
3. Using DNS challenge instead for local IPs
4. Checking firewall isn't blocking connections
502 Bad Gateway
Proxy not reaching backend service.
# Fixed by
1. Verifying backend service is actually running
2. Checking IP and port are correct
3. Making sure backend allows connections from proxy IP
4. Checking firewall rules between proxy and backend
WebSocket connections drop
Real-time apps disconnecting.
# Fixed by
1. Enabling WebSocket support in NPM proxy host
2. Increasing timeout values
3. Checking for any nginx custom config breaking WebSockets
Too many redirects
Infinite redirect loop.
# Fixed by
1. Disabling "Force SSL" if app already handles HTTPS
2. Checking app isn't redirecting to http
3. Making sure scheme (http/https) matches backend
Certificate renewal fails
Let's Encrypt expires unexpectedly.
# Fixed by
1. Checking DNS provider API credentials still valid
2. Verifying port 80 is still accessible
3. Checking certificate logs for specific error
4. Manually renewing to see actual failure reason
Security best practices
Network isolation
Keep management UI secure:
# Don't expose port 81 to internet
# Use SSH tunnel to access:
ssh -L 8888:localhost:81 user@server
# Then access locally: http://localhost:8888
# Or use VPN/ZeroTrust to access
Regular updates
Keep NPM updated:
# Watch for new versions
docker pull jc21/nginx-proxy-manager:latest
docker compose up -d # recreates container with new image
# Test on non-production first
# NPM updates can change behavior
Backup configs
Save your NPM data:
# Backup the data directory
tar -czf npm-backup-$(date +%Y%m%d).tar.gz data/
# Store somewhere safe
# Can restore to new server if needed
Access control
Don't expose everything publicly:
# Use access lists for sensitive services
# Restrict to trusted networks (VPN, local IPs)
# Use strong passwords for basic auth
# Consider IP whitelists for critical services
NPM vs alternatives
| NPM | Raw Nginx | Traefik | Caddy | |
|---|---|---|---|---|
| Ease of use | ★★★★★ | ★★☆☆☆ | ★★★☆☆ | ★★★★☆ |
| Web UI | Yes | No | Yes | No |
| SSL auto-renew | Yes | Manual | Yes | Yes |
| Learning curve | Low | High | Medium | Low |
| Flexibility | Good | Excellent | Excellent | Good |
| Docker native | Yes | No | Yes | Yes |
| Best for | Home labs, simple setups | Complex needs, full control | Container orchestration | Automated setups |
NPM is the sweet spot for home labs and simple deployments. Easy enough for beginners, powerful enough for most use cases.
Would I recommend it?
Definitely. I've been running NPM for 2 years now. Managing 15+ services through it. SSL certificates auto-renew without me thinking about it.
The web UI saves so much time. Used to spend hours editing nginx configs, testing, reloading. Now click a few buttons, done.
DNS challenge with wildcard certificates is a game changer. One cert covers all my subdomains. Add new service, create new proxy, instant HTTPS.
It's not perfect - sometimes you need to drop into custom config for advanced stuff. But for 95% of use cases, the UI handles everything.
If you're self-hosting anything and want HTTPS without the headache, NPM is the answer.
Links: nginxproxymanager.com | GitHub: github.com/NginxProxyManager | Docs: nginxproxymanager.com/guide/