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

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

Related Resources