Nginx Proxy Manager: SSL Certificates That Finally Auto-Renew
I spent way too much time manually renewing Let's Encrypt certs and editing Nginx config files. Nginx Proxy Manager (NPM) solved all of that with a clean web UI. Here's my production setup that's been handling 20+ services on my home lab for the past year.
Problem
Setting up multiple subdomains for testing. NPM kept failing with "Too many certificates already issued"
after creating just 5 certificates. I was stuck in development mode, couldn't test properly.
Error: Error creating certificate. Too many certificates already issued for: example.com
What I Tried
Attempt 1: Waited an hour - still hitting the limit.
Attempt 2: Deleted and recreated certs - count didn't reset.
Attempt 3: Tried using different domains - not practical for long-term.
Actual Fix
Use Let's Encrypt staging server for testing (unlimited rate limit), and DNS challenge instead of HTTP. DNS challenge bypasses many rate limits and works better behind Cloudflare proxy.
# In NPM UI: SSL Certificates → Add Certificate
# For testing, use Staging Let's Encrypt:
# - Domain: *.yourdomain.com
# - Email: your@email.com
# - Let's Encrypt: Staging (not Production)
# For production, use DNS Challenge with Cloudflare:
# 1. Get Cloudflare API Token:
# - Cloudflare Dashboard → My Profile → API Tokens
# - Create token with Zone:DNS:Edit permission
#
# 2. In NPM: Settings → SSL → DNS Challenge
# - Provider: Cloudflare
# - Credentials:
# - Cloudflare Email: your@email.com
# - Cloudflare API Key: your_api_token_here
# 3. Create certificate with DNS challenge
# This bypasses HTTP validation and many rate limits
Problem
NPM running in Docker, trying to proxy to other Docker containers on the same host.
Kept getting 502 Bad Gateway. The backend containers were running fine, accessible via curl on the host,
but NPM couldn't reach them.
curl http://localhost:3000 worked, but proxy didn't.
What I Tried
Attempt 1: Used localhost:3000 as upstream - didn't work (Docker networking issue).
Attempt 2: Used 127.0.0.1:3000 - same 502 error.
Attempt 3: Exposed all ports on the backend containers - security risk, still had issues.
Actual Fix
The issue is Docker networking - containers need to be on the same network to communicate. Created a shared Docker network, attached all containers (NPM + backends) to it. Then use container names as hostnames, not localhost.
# 1. Create shared network
docker network create proxy-network
# 2. Run NPM on the shared network
docker run -d \
--name nginx-proxy-manager \
--network proxy-network \
-p 80:80 \
-p 443:443 \
-p 81:81 \
-v /path/to/npm/data:/data \
'jc21/nginx-proxy-manager:latest'
# 3. Run your backend app on same network
docker run -d \
--name my-app \
--network proxy-network \
-v /path/to/app:/app \
my-nodejs-app
# 4. In NPM UI, use container name as upstream host:
# - Scheme: http
# - Forward Hostname/IP: my-app (not localhost!)
# - Forward Port: 3000
Problem
Running a Node.js app with Socket.io through NPM. WebSocket connections would connect initially but drop after 30-60 seconds with "Connection closed" errors. Direct connection to the backend worked fine.
What I Tried
Attempt 1: Increased timeout values in Nginx advanced settings - didn't help.
Attempt 2: Tried HTTP/1.1 instead of HTTP/2 - websockets still dropped.
Attempt 3: Disabled Cloudflare proxy (orange cloud → grey) - worked but defeats the purpose.
Actual Fix
Need to add specific Nginx configuration for WebSocket upgrade headers and timeouts. NPM has an "Advanced" tab where you can add custom Nginx location blocks.
# In NPM UI: Proxy Host → Advanced → Custom Nginx Configuration
# Add these location blocks for WebSocket support
location / {
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# WebSocket timeout settings
proxy_connect_timeout 7d;
proxy_send_timeout 7d;
proxy_read_timeout 7d;
# Pass all requests to backend
proxy_pass http://backend-container-name;
}
# For Socket.io specifically, also add:
location /socket.io/ {
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_http_version 1.1;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://backend-container-name;
}
What I Learned
- Lesson 1: Use DNS challenge for SSL certs when behind Cloudflare - HTTP challenge fails with proxy enabled.
- Lesson 2: All Docker containers must be on the same network - using localhost won't work.
- Lesson 3: WebSocket support requires specific Nginx config - don't assume defaults work for real-time apps.
- Overall: NPM's UI hides the complexity of Nginx, but understanding Nginx fundamentals (proxy headers, timeouts, networking) is still essential for troubleshooting.
Production Setup
Complete Docker Compose setup for NPM with multiple services on a home lab server.
# docker-compose.yml for NPM stack
version: "3"
services:
nginx-proxy-manager:
image: 'jc21/nginx-proxy-manager:latest'
container_name: nginx-proxy-manager
restart: unless-stopped
ports:
- '80:80' # HTTP
- '443:443' # HTTPS
- '81:81' # Management UI
volumes:
- ./npm/data:/data
- ./npm/letsencrypt:/etc/letsencrypt
networks:
- proxy-network
# Example backend service
my-app:
image: my-nodejs-app:latest
container_name: my-app
restart: unless-stopped
networks:
- proxy-network
# No port exposure needed - internal network only
# Another service
my-service:
image: my-service:latest
container_name: my-service
restart: unless-stopped
networks:
- proxy-network
networks:
proxy-network:
external: true
# Run this first to create the network:
# docker network create proxy-network
# Initial setup commands
mkdir -p npm/{data,letsencrypt}
docker network create proxy-network
# Start NPM
docker-compose up -d
# Access NPM UI
# Default credentials:
# Email: admin@example.com
# Password: changeme
# Change immediately after first login
# Configure DNS (Cloudflare example)
# A record: npm.yourdomain.com → your server IP
# A record: *.yourdomain.com → your server IP
# For Cloudflare SSL with NPM:
# SSL/TLS → Full (strict)
# Edge Certificates → Always Use HTTPS: ON
# Edge Certificates → Automatic HTTPS Rewrites: ON
Monitoring & Debugging
Essential commands and red flags for NPM maintenance:
Essential Commands
# View NPM logs
docker logs nginx-proxy-manager -f
# Check Nginx config (generated by NPM)
docker exec nginx-proxy-manager cat /etc/nginx/conf.d/default.conf
# Test Nginx configuration
docker exec nginx-proxy-manager nginx -t
# Reload Nginx without restart
docker exec nginx-proxy-manager nginx -s reload
# Renew all SSL certificates manually
# (NPM auto-renews, but this forces immediate renewal)
docker exec nginx-proxy-manager /app/certbot-helper.py renew-all
Red Flags to Watch For
- Certs expiring in < 7 days - check if auto-renew is working
- Container restarting more than once per day - memory or config issue
- 502 errors on all proxies - check backend container health and network connectivity
- High CPU usage from NPM - possible DDoS or misconfigured proxy