Nginx Proxy Manager: Finally, SSL Certificates That Don't Suck

Managing reverse proxies and SSL manually? Forget it. Nginx Proxy Manager gives you free HTTPS with a web UI. Here's how I set up my home lab.

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/