Nginx Proxy Manager: Complete Homelab Setup Guide
Master reverse proxy management with a beautiful GUI. SSL certificates, access control, and multi-service routing made simple.
Metadata
- Difficulty Level: Intermediate
- Estimated Time: 45-60 minutes
- Target Audience: Homelab enthusiasts, self-hosting beginners, DevOps engineers
- Prerequisites: Docker/Docker Compose, basic networking knowledge
- Last Updated: December 2025
Introduction
Managing multiple self-hosted services in your homelab can quickly become overwhelming. Each application needs its own domain or subdomain, SSL certificates require manual renewal, and routing traffic to the correct service demands complex Nginx configurations. This is where Nginx Proxy Manager (NPM) shines as the perfect solution for homelabbers who want enterprise-grade reverse proxy capabilities without the complexity.
NPM transforms the powerful but notoriously complex Nginx into an accessible, GUI-based tool that anyone can master. Whether you’re exposing Home Assistant to the internet, setting up a media server stack, or organizing dozens of containerized applications, NPM provides a centralized dashboard to manage everything with just a few clicks.
What You’ll Learn
In this comprehensive guide, you’ll discover how to:
- Install and configure Nginx Proxy Manager in a production-ready setup
- Create proxy hosts to route traffic to your services
- Obtain and manage free SSL certificates from Let’s Encrypt
- Implement wildcard certificates using DNS challenges
- Secure your services with access lists and authentication
- Automate common tasks using the NPM API
- Troubleshoot the most common issues
- Integrate AI and automation for intelligent service management
By the end, you’ll have a fully functional reverse proxy managing all your homelab services with proper SSL encryption, access controls, and monitoring capabilities.
Understanding Nginx Proxy Manager
What is NPM?
Nginx Proxy Manager is a Docker-based application that provides a user-friendly web interface for managing Nginx reverse proxy configurations. Think of it as the control center for all incoming traffic to your homelab. When someone accesses movies.yourdomain.com, NPM routes that request to your Radarr container. When they visit home.yourdomain.com, it directs them to Home Assistant instead.
Key advantages:
- GUI-Based Management: No need to edit complex Nginx configuration files
- Built-in SSL/TLS: Automatic certificate provisioning and renewal from Let’s Encrypt
- Multi-User Support: Role-based access control and comprehensive audit logging
- Docker-Native: Designed specifically for containerized deployments
- Backup Simplicity: Only two directories to backup for complete disaster recovery
Why NPM is Perfect for Homelabs
NPM has become the de facto standard for homelab reverse proxies for several compelling reasons:
- Beginner-Friendly: The intuitive interface means you can get SSL-secured services running in minutes, not hours
- Centralized Management: One dashboard to control routing for all your services
- Free SSL: Let’s Encrypt integration provides free, automatically-renewing certificates
- Minimal Resources: Runs efficiently on modest hardware (documented to handle 50,000+ requests/day on 1GB RAM)
- Community Support: Large, active community with extensive documentation
NPM vs. Alternatives
| Feature | NPM | Traefik | Caddy |
|---|---|---|---|
| GUI Interface | Full-featured | Inspection-only | None |
| Learning Curve | Low | Medium-High | Very Low |
| Best For | 5-20 services | 20+ services | 1-10 services |
| Auto-Discovery | Manual | Automatic | Manual |
| Multi-User | Yes | No | Limited |
| Infrastructure-as-Code | Poor | Excellent | Good |
When to choose NPM:
- You’re starting with reverse proxies
- You prefer GUIs over configuration files
- You need multi-user management with audit trails
- Your homelab has 5-20 services
- You want proven, stable technology
When to consider alternatives:
- Traefik: Managing 20+ services, need GitOps workflows, want automatic service discovery
- Caddy: Want the absolute simplest setup, running fewer than 10 services
Installation and Setup
Prerequisites
Before installing NPM, ensure you have:
Technical Requirements:
- Docker 20.10+ and Docker Compose installed
- Linux system with 1GB+ RAM (512MB minimum)
- Access to ports 80, 81, and 443
- Basic command-line proficiency
Infrastructure Requirements:
- A domain name (required for Let’s Encrypt SSL certificates)
- Router access for port forwarding (if exposing services externally)
- At least one self-hosted service to proxy
System Preparation
First, verify that the required ports are available:
# Check port availability
sudo lsof -i :80
sudo lsof -i :443
sudo lsof -i :81
# Create organized directory structure
mkdir -p ~/npm/{data,letsencrypt,mysql}
cd ~/npm Production Installation
Create a production-ready docker-compose.yml file with MariaDB database:
version: '3.8'
services:
npm:
image: 'jc21/nginx-proxy-manager:latest'
restart: unless-stopped
ports:
- '80:80' # HTTP traffic
- '443:443' # HTTPS traffic
- '81:81' # NPM admin interface
environment:
DB_MYSQL_HOST: "db"
DB_MYSQL_PORT: 3306
DB_MYSQL_USER: "npm"
DB_MYSQL_PASSWORD: "secure_npm_password_here"
DB_MYSQL_NAME: "npm"
volumes:
- ./data:/data
- ./letsencrypt:/etc/letsencrypt
depends_on:
- db
networks:
- npm-network
db:
image: 'jc21/mariadb-aria:latest'
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD: 'secure_root_password_here'
MYSQL_DATABASE: 'npm'
MYSQL_USER: 'npm'
MYSQL_PASSWORD: 'secure_npm_password_here'
volumes:
- ./mysql:/var/lib/mysql
networks:
- npm-network
networks:
npm-network:
driver: bridge Important security notes:
- Replace
secure_npm_password_hereandsecure_root_password_herewith strong, unique passwords - Never commit passwords to version control; consider using environment variables or Docker secrets
Deploy the stack:
docker compose up -d
# Verify containers are running
docker compose ps
# Check logs for any errors
docker compose logs -f npm ARM Device Installation (Raspberry Pi)
For ARM-based devices like Raspberry Pi, use the Alpine MariaDB image instead:
db:
image: 'yobasystems/alpine-mariadb:latest'
# ... rest of configuration remains the same Initial Configuration
Access the admin interface:
- Navigate to:
http://your-server-ip:81 - Default email:
[email protected] - Default password:
changeme
- Navigate to:
CRITICAL: Immediately change the default password
- Log in with default credentials
- Click on your user profile (top-right)
- Navigate to “Details”
- Update email to your address
- Change password to a strong, unique password
- Save changes
Configure settings:
- Review Settings → Default Site (optional welcome page)
- Configure Settings → Advanced (if needed)
- Familiarize yourself with the interface
Security reminder: Never expose port 81 to the public internet. Only access it via:
- Local network
- VPN connection
- SSH tunnel
Creating Your First Proxy Host
Understanding Proxy Hosts
A proxy host is NPM’s configuration that routes a specific domain or subdomain to a backend service. For example:
- Domain:
movies.example.com→ Backend:radarr:7878 - Domain:
home.example.com→ Backend:homeassistant:8123
Step-by-Step Configuration
Let’s create a proxy host for a Home Assistant instance:
Step 1: Access Proxy Hosts
- Navigate to: Hosts → Proxy Hosts
- Click: “Add Proxy Host” button
Step 2: Configure Domain and Routing (Details Tab)
- Domain Names:
home.example.com - Scheme:
http(Home Assistant backend uses HTTP internally) - Forward Hostname/IP:
homeassistant(Docker container name)- If on same Docker network: use container name
- If external service: use IP address or hostname
- Never use
127.0.0.1; use host IP orhost.docker.internalinstead
- Forward Port:
8123(Home Assistant’s default port) - Cache Assets: Off (for dynamic applications)
- Block Common Exploits: ✓ Enabled (recommended for security)
- Websockets Support: ✓ Enabled (Home Assistant needs this for real-time updates)
Step 3: Configure SSL Certificate (SSL Tab)
- Click the SSL tab
- SSL Certificate: Select “Request a new SSL Certificate”
- Force SSL: ✓ Enabled (redirects HTTP to HTTPS)
- HTTP/2 Support: ✓ Enabled (improved performance)
- HSTS Enabled: ✓ Enabled (security header)
- HSTS Subdomains: Optional (enable if using subdomains)
- Email Address for Let’s Encrypt: Your email for renewal notifications
- I Agree to the Let’s Encrypt Terms of Service: ✓ Enabled
- Click Save
NPM will now:
- Validate domain ownership via Let’s Encrypt
- Generate and install the SSL certificate
- Generate Nginx configuration
- Reload Nginx service
- Make your service accessible at
https://home.example.com
Connecting Services on the Same Docker Network
For NPM to communicate with your backend services, they must be on the same Docker network. Here’s a complete example:
version: '3.8'
services:
npm:
image: jc21/nginx-proxy-manager:latest
restart: unless-stopped
ports:
- "80:80"
- "443:443"
- "81:81"
volumes:
- ./npm/data:/data
- ./npm/letsencrypt:/etc/letsencrypt
networks:
- homelab
homeassistant:
image: ghcr.io/home-assistant/home-assistant:latest
restart: unless-stopped
volumes:
- ./homeassistant:/config
networks:
- homelab
# Note: No need to expose port 8123 to host
# NPM will access via container network
radarr:
image: linuxserver/radarr:latest
restart: unless-stopped
environment:
PUID: 1000
PGID: 1000
volumes:
- ./radarr:/config
- /media/movies:/movies
networks:
- homelab
# Port 7878 not exposed to host
networks:
homelab:
driver: bridge In this setup:
- NPM uses
homeassistantas the forward hostname (Docker DNS resolves container names) - NPM uses
radarras the forward hostname for the media server - No ports need to be exposed to the host (more secure)
- All communication happens over the Docker network
Port Forwarding for External Access
If you want to access your services from outside your local network:
Router Configuration:
- Access your router’s admin interface
- Navigate to Port Forwarding (sometimes called “Virtual Servers”)
- Create two forwarding rules:
- External Port 80 → Internal IP:
npm-server-ip:80 - External Port 443 → Internal IP:
npm-server-ip:443
- External Port 80 → Internal IP:
- Save and apply changes
Important: Never port-forward port 81 (the admin interface) to the internet.
DNS Configuration:
- Log into your domain registrar or DNS provider
- Create A records pointing to your public IP:
home.example.com→ Your public IPmovies.example.com→ Your public IP- Or use a wildcard:
*.example.com→ Your public IP
SSL Certificate Management
Understanding Let’s Encrypt Challenges
Let’s Encrypt provides free SSL certificates but needs to verify you control the domain. NPM supports two validation methods:
HTTP Challenge (Standard):
- Requires port 80 accessible from the internet
- Simpler to set up
- Good for basic deployments
DNS Challenge (Advanced):
- Does NOT require public port 80 access
- Enables wildcard certificates (
*.example.com) - Works for completely private networks
- More secure for sensitive homelabs
HTTP Challenge Setup
This is the simplest method and works for most homelab scenarios:
Requirements:
- Port 80 forwarded to NPM from your router
- Domain DNS A record pointing to your public IP
- No firewall blocking port 80
Configuration:
- In proxy host configuration, go to SSL tab
- Select “Request a new SSL Certificate”
- Leave “Use a DNS Challenge” unchecked
- Enter your email address
- Accept Terms of Service
- Save
Let’s Encrypt will:
- Send an HTTP request to
http://yourdomain.com/.well-known/acme-challenge/ - NPM responds with validation token
- Certificate issued within seconds
- Certificate automatically renewed 30 days before expiration
Rate limits: 50 certificates per domain per week. Use wildcard certificates to avoid this limit.
DNS Challenge with Cloudflare (Wildcard Certificates)
The DNS challenge is the recommended approach for serious homelabs because:
- Single wildcard certificate covers all subdomains
- No need to expose port 80 publicly
- Works behind VPN or completely private networks
- More secure overall
Step 1: Create Cloudflare API Token
- Log into Cloudflare dashboard
- Navigate to: My Profile → API Tokens
- Click Create Token
- Use template: “Edit zone DNS”
- Permissions:
- Zone - DNS - Edit
- Zone - Zone - Read
- Zone Resources: Include → All zones (or specific zones)
- Click Continue to summary → Create Token
- Copy the token and save it securely (shown only once)
Step 2: Configure in NPM
- Navigate to SSL Certificates (not in a proxy host yet)
- Click Add SSL Certificate
- Select “Add Let’s Encrypt Certificate”
- Domain Names: Enter
*.example.comandexample.com(both recommended) - Email Address: Your contact email
- Use a DNS Challenge: ✓ Enabled
- DNS Provider: Select Cloudflare
- Credentials File Content:
dns_cloudflare_api_token = your_api_token_here - Propagation Seconds: 30 (Cloudflare is usually fast)
- I Agree to the Let’s Encrypt Terms of Service: ✓ Enabled
- Click Save
NPM will:
- Create a DNS TXT record at
_acme-challenge.example.com - Wait for DNS propagation
- Let’s Encrypt validates via DNS query
- Issue wildcard certificate
- Store in
/letsencryptvolume
Step 3: Apply to Proxy Hosts
Now when creating proxy hosts:
- SSL tab → SSL Certificate: Select your wildcard certificate from dropdown
- Force SSL: ✓ Enabled
- HTTP/2 Support: ✓ Enabled
- Save
One certificate now secures unlimited subdomains:
home.example.commovies.example.comtv.example.comanything.example.com
Supported DNS Providers
NPM supports 50+ DNS providers for DNS challenges:
- Cloudflare (most popular)
- Cloudns
- DigitalOcean
- DuckDNS (great for dynamic IPs)
- Google Cloud DNS
- Route53 (AWS)
- Namecheap
- And many more
Check the official documentation for provider-specific configuration.
Certificate Renewal
Automatic Renewal:
- NPM checks certificate expiration daily
- Triggers renewal 30 days before expiration
- Usually completes within hours
- Email notification if renewal fails
Monitoring Certificate Status:
# Check certificate expiration dates
docker exec npm ls -la /etc/letsencrypt/live/
# View specific certificate details
docker exec npm openssl x509 -in /etc/letsencrypt/live/npm-1/cert.pem -noout -dates Troubleshooting Failed Renewals:
- Check NPM logs:
docker logs npm - Verify DNS provider credentials still valid
- Ensure DNS API token has correct permissions
- Check Let’s Encrypt rate limits (use staging for testing)
- Manually trigger renewal in SSL Certificates section
Access Control and Security
Creating Access Lists
Access lists restrict who can access your services at the proxy layer, before requests even reach your applications.
Navigate to:
- Access Lists (in left sidebar)
- Click Add Access List
Example 1: Local Network Only
Name: Local Network Only
Satisfy Any: Off (all conditions must match)
Authorization Rules:
Pass: ✓ Enabled
Username: (leave empty for IP-only)
Password: (leave empty for IP-only)
Access Control Rules:
Action: allow
IP/CIDR: 192.168.0.0/16
Comment: Local network
Action: allow
IP/CIDR: 10.0.0.0/8
Comment: Private network range
Action: deny
IP/CIDR: 0.0.0.0/0
Comment: Block all others Example 2: VPN + Office Network
Name: VPN and Office
Satisfy Any: Off
Access Control Rules:
Action: allow
IP/CIDR: 10.20.0.0/16
Comment: VPN network
Action: allow
IP/CIDR: 203.0.113.0/24
Comment: Office public IP range
Action: allow
IP/CIDR: 192.168.1.0/24
Comment: Office LAN
Action: deny
IP/CIDR: 0.0.0.0/0
Comment: Block all others Example 3: HTTP Basic Authentication
Name: Password Protected
Satisfy Any: Off
Authorization Rules:
Username: admin
Password: (set strong password)
Action: allow
IP/CIDR: 0.0.0.0/0
Comment: Allow after authentication Applying Access Lists
- Edit a proxy host
- Navigate to Access tab
- Access List: Select from dropdown
- Save
Rules apply immediately. Test from:
- Allowed IP: Should work normally
- Blocked IP: Should see 403 Forbidden
- If using auth: Browser prompts for credentials
Security Best Practices
Layer 1: Network Security
- Never expose port 81 (admin interface) to internet
- Use VPN for remote access (WireGuard, OpenVPN)
- Implement network segmentation (VLANs)
Layer 2: Proxy Security
- Enable “Block Common Exploits” on all proxy hosts
- Use access lists for IP restrictions
- Implement HTTP Basic Auth for dashboards
Layer 3: Application Security
- Enable application-level authentication
- Use strong, unique passwords
- Enable 2FA where available
- Keep applications updated
Layer 4: Monitoring
- Review NPM audit logs weekly
- Monitor certificate expiration
- Set up alerts for configuration changes
- Track failed authentication attempts
Important: Don’t combine NPM’s HTTP Basic Auth with application-level auth that also uses Authorization headers (causes conflicts).
Advanced Security Headers
NPM’s “Block Common Exploits” automatically adds these headers:
# Automatically added by NPM
X-Frame-Options: SAMEORIGIN
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Strict-Transport-Security: max-age=31536000; includeSubDomains For custom headers, use the Advanced tab in proxy host configuration:
# Custom security headers
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;
add_header Content-Security-Policy "default-src 'self'" always; Real-World Homelab Examples
Small Homelab (3-5 Services)
Perfect for beginners starting their homelab journey.
Services:
- Home Assistant (home automation)
- Portainer (Docker management)
- Radarr (movie downloads)
docker-compose.yml:
version: '3.8'
services:
npm:
image: jc21/nginx-proxy-manager:latest
restart: unless-stopped
ports:
- "80:80"
- "443:443"
- "81:81"
volumes:
- ./npm/data:/data
- ./npm/letsencrypt:/etc/letsencrypt
networks:
- homelab
homeassistant:
image: ghcr.io/home-assistant/home-assistant:latest
restart: unless-stopped
volumes:
- ./homeassistant:/config
networks:
- homelab
portainer:
image: portainer/portainer-ce:latest
restart: unless-stopped
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- ./portainer:/data
networks:
- homelab
radarr:
image: linuxserver/radarr:latest
restart: unless-stopped
environment:
PUID: 1000
PGID: 1000
TZ: America/New_York
volumes:
- ./radarr:/config
- /media/movies:/movies
networks:
- homelab
networks:
homelab:
driver: bridge NPM Configuration:
- Single HTTP challenge certificate or wildcard via DNS challenge
- Three proxy hosts:
home.yourdomain.com→homeassistant:8123(websockets enabled)portainer.yourdomain.com→portainer:9000movies.yourdomain.com→radarr:7878
- Access list: Local network only
Medium Homelab (10-20 Services)
For users expanding their infrastructure with organized service categories.
Service Categories:
- Media: Radarr, Sonarr, Lidarr, Jellyfin, Transmission
- Home Automation: Home Assistant, Node-RED
- Monitoring: Prometheus, Grafana, Uptime Kuma
- Development: Gitea, VS Code Server
DNS Strategy:
- Wildcard certificate:
*.services.yourdomain.com - DNS challenge via Cloudflare
- Subdomains organized by function
Proxy Host Organization:
movies.services.yourdomain.com → radarr:7878
shows.services.yourdomain.com → sonarr:8989
music.services.yourdomain.com → lidarr:8686
media.services.yourdomain.com → jellyfin:8096
torrents.services.yourdomain.com → transmission:9091
home.services.yourdomain.com → homeassistant:8123
automation.services.yourdomain.com → nodered:1880
metrics.services.yourdomain.com → prometheus:9090
graphs.services.yourdomain.com → grafana:3000
uptime.services.yourdomain.com → uptime-kuma:3001
git.services.yourdomain.com → gitea:3000
code.services.yourdomain.com → code-server:8443 Access Lists:
local_only: 192.168.0.0/16vpn_users: 10.20.0.0/16family: Specific IPs + VPNpublic: Limited services (media server for friends)
Large Homelab (30+ Services)
Advanced setup with multiple domains and complex requirements.
Infrastructure:
- Multiple Docker Compose projects
- External MariaDB for high availability
- Comprehensive monitoring
- Automated backups
Multi-Domain Strategy:
*.home.yourdomain.com- Family services*.family.yourdomain.com- Shared with extended family*.dev.yourdomain.com- Development/test environment*.ops.yourdomain.com- Admin-only monitoring/management
Advanced docker-compose.yml:
version: '3.8'
services:
npm:
image: jc21/nginx-proxy-manager:latest
restart: always
ports:
- "80:80"
- "443:443"
- "81:81"
environment:
DB_MYSQL_HOST: "external-db.local"
DB_MYSQL_PORT: 3306
DB_MYSQL_USER: "npm"
DB_MYSQL_PASSWORD: "${NPM_DB_PASSWORD}"
DB_MYSQL_NAME: "npm"
volumes:
- ./npm/data:/data
- ./npm/letsencrypt:/etc/letsencrypt
- ./npm/custom:/data/nginx/custom
deploy:
resources:
limits:
cpus: '2'
memory: 2G
reservations:
cpus: '0.5'
memory: 512M
networks:
- external-proxy
- monitoring
npm-exporter:
image: custom/npm-prometheus-exporter:latest
restart: always
environment:
NPM_URL: "http://npm:81"
NPM_TOKEN: "${NPM_API_TOKEN}"
networks:
- external-proxy
- monitoring
networks:
external-proxy:
driver: bridge
monitoring:
external: true Role-Based Access Control:
- Admin role: Full configuration access, user management
- Power User role: Create/modify proxy hosts and certificates
- User role: View-only access for monitoring
- Comprehensive audit logging for compliance
Automation and AI Integration
NPM REST API
NPM provides an undocumented but functional REST API for programmatic management.
Authentication:
- Navigate to Settings → Users → Your Account
- Scroll to API Tokens
- Click Create Token
- Copy and save securely
Basic API Usage:
# List all proxy hosts
curl -X GET https://npm.yourdomain.com/api/nginx/proxy-hosts -H "Authorization: Bearer YOUR_API_TOKEN" | jq
# Get certificate list
curl -X GET https://npm.yourdomain.com/api/nginx/certificates -H "Authorization: Bearer YOUR_API_TOKEN" | jq Automated Service Provisioning
Automatically create proxy hosts when deploying new Docker services.
Python Automation Script:
#!/usr/bin/env python3
"""
NPM Service Auto-Provisioner
Discovers Docker containers and automatically creates proxy hosts
"""
import requests
import docker
import os
from typing import Dict, List
class NPMAutomation:
def __init__(self, npm_url: str, api_token: str, cert_id: str):
self.npm_url = npm_url.rstrip('/')
self.headers = {
"Authorization": f"Bearer {api_token}",
"Content-Type": "application/json"
}
self.cert_id = cert_id
self.docker = docker.from_env()
def discover_services(self) -> List[Dict]:
"""Find Docker containers with npm.enabled label"""
services = []
for container in self.docker.containers.list():
labels = container.labels
# Only process containers with npm.enabled=true
if labels.get('npm.enabled') != 'true':
continue
service = {
'name': container.name,
'domain': labels.get('npm.domain'),
'port': labels.get('npm.port'),
'scheme': labels.get('npm.scheme', 'http'),
'websockets': labels.get('npm.websockets', 'false') == 'true',
'access_list_id': labels.get('npm.access_list_id', '0')
}
# Validate required fields
if not service['domain'] or not service['port']:
print(f"Warning: Container {container.name} missing domain or port label")
continue
services.append(service)
return services
def proxy_host_exists(self, domain: str) -> bool:
"""Check if proxy host already exists for domain"""
response = requests.get(
f"{self.npm_url}/api/nginx/proxy-hosts",
headers=self.headers
)
if response.status_code != 200:
return False
hosts = response.json()
for host in hosts:
if domain in host.get('domain_names', []):
return True
return False
def create_proxy_host(self, service: Dict) -> bool:
"""Create a new proxy host via API"""
# Check if already exists
if self.proxy_host_exists(service['domain']):
print(f"Proxy host already exists for {service['domain']}, skipping")
return False
payload = {
'domain_names': [service['domain']],
'forward_scheme': service['scheme'],
'forward_host': service['name'],
'forward_port': int(service['port']),
'access_list_id': int(service['access_list_id']),
'certificate_id': self.cert_id,
'ssl_forced': True,
'caching_enabled': False,
'block_exploits': True,
'allow_websocket_upgrade': service['websockets'],
'http2_support': True,
'advanced_config': '',
'enabled': True
}
response = requests.post(
f"{self.npm_url}/api/nginx/proxy-hosts",
json=payload,
headers=self.headers
)
if response.status_code in [200, 201]:
print(f"✓ Created proxy host: {service['domain']} → {service['name']}:{service['port']}")
return True
else:
print(f"✗ Failed to create {service['domain']}: {response.text}")
return False
def auto_provision(self):
"""Discover and provision all labeled services"""
print("Discovering services...")
services = self.discover_services()
if not services:
print("No services found with npm.enabled=true label")
return
print(f"Found {len(services)} service(s) to provision")
for service in services:
self.create_proxy_host(service)
print("\nProvisioning complete!")
# Usage
if __name__ == "__main__":
npm = NPMAutomation(
npm_url="https://npm.yourdomain.com",
api_token=os.getenv("NPM_API_TOKEN"),
cert_id="le_wildcard" # Your wildcard certificate ID
)
npm.auto_provision() Docker Compose with Labels:
version: '3.8'
services:
jellyfin:
image: jellyfin/jellyfin:latest
restart: unless-stopped
labels:
npm.enabled: "true"
npm.domain: "jellyfin.yourdomain.com"
npm.port: "8096"
npm.scheme: "http"
npm.websockets: "true"
npm.access_list_id: "1" # Your access list ID
networks:
- homelab
nextcloud:
image: nextcloud:latest
restart: unless-stopped
labels:
npm.enabled: "true"
npm.domain: "cloud.yourdomain.com"
npm.port: "80"
npm.scheme: "http"
npm.access_list_id: "2"
networks:
- homelab
networks:
homelab:
external: true Run the provisioner:
# Set API token
export NPM_API_TOKEN="your_token_here"
# Install dependencies
pip install docker requests
# Run provisioner
python3 npm-autoprovision.py Automated Backup Solution
Backup Script (npm-backup.sh):
#!/bin/bash
#
# NPM Automated Backup Script
# Backs up NPM configuration and SSL certificates
#
set -e
# Configuration
BACKUP_DIR="/backup/npm"
CONTAINER_NAME="npm"
RETENTION_DAYS=30
DATE=$(date +%Y%m%d_%H%M%S)
# Create backup directory
mkdir -p "$BACKUP_DIR"
echo "Starting NPM backup at $(date)"
# Backup NPM data directory (configuration, database)
echo "Backing up NPM data..."
docker exec "$CONTAINER_NAME" tar czf - /data 2>/dev/null | gzip > "$BACKUP_DIR/npm_data_$DATE.tar.gz"
# Backup SSL certificates
echo "Backing up SSL certificates..."
docker exec "$CONTAINER_NAME" tar czf - /etc/letsencrypt 2>/dev/null | gzip > "$BACKUP_DIR/npm_certs_$DATE.tar.gz"
# Create combined backup
echo "Creating combined archive..."
cd "$BACKUP_DIR"
tar czf "npm_complete_$DATE.tar.gz" "npm_data_$DATE.tar.gz" "npm_certs_$DATE.tar.gz"
# Remove individual archives (keep combined only)
rm -f "npm_data_$DATE.tar.gz" "npm_certs_$DATE.tar.gz"
# Calculate backup size
BACKUP_SIZE=$(du -h "npm_complete_$DATE.tar.gz" | cut -f1)
echo "Backup completed: npm_complete_$DATE.tar.gz ($BACKUP_SIZE)"
# Clean up old backups (keep only last N days)
echo "Cleaning up old backups (keeping last $RETENTION_DAYS days)..."
find "$BACKUP_DIR" -name "npm_complete_*.tar.gz" -mtime +$RETENTION_DAYS -delete
# List current backups
echo "Current backups:"
ls -lh "$BACKUP_DIR"/npm_complete_*.tar.gz 2>/dev/null || echo "No backups found"
echo "Backup process completed at $(date)" Make executable and schedule:
# Make executable
chmod +x /usr/local/bin/npm-backup.sh
# Add to crontab (daily at 2 AM)
crontab -e
# Add this line:
0 2 * * * /usr/local/bin/npm-backup.sh >> /var/log/npm-backup.log 2>&1 Restore from backup:
#!/bin/bash
# npm-restore.sh
BACKUP_FILE="$1"
if [ -z "$BACKUP_FILE" ]; then
echo "Usage: $0 <backup_file>"
exit 1
fi
# Stop NPM
docker compose down
# Extract backup
tar xzf "$BACKUP_FILE"
# Extract data and certificates
tar xzf npm_data_*.tar.gz -C /
tar xzf npm_certs_*.tar.gz -C /
# Start NPM
docker compose up -d
echo "Restore completed!" AI-Powered Certificate Monitoring
Certificate Monitor Script:
#!/usr/bin/env python3
"""
AI-Powered NPM Certificate Monitor
Predicts renewal failures and alerts proactively
"""
import requests
import datetime
from typing import List, Dict
class CertificateMonitor:
def __init__(self, npm_url: str, api_token: str):
self.npm_url = npm_url.rstrip('/')
self.headers = {"Authorization": f"Bearer {api_token}"}
def get_certificates(self) -> List[Dict]:
"""Fetch all SSL certificates from NPM"""
response = requests.get(
f"{self.npm_url}/api/nginx/certificates",
headers=self.headers
)
return response.json() if response.status_code == 200 else []
def days_until_expiry(self, expires_on: str) -> int:
"""Calculate days until certificate expires"""
expiry_date = datetime.datetime.strptime(expires_on, "%Y-%m-%d %H:%M:%S")
delta = expiry_date - datetime.datetime.now()
return delta.days
def analyze_certificates(self):
"""Analyze certificates and generate alerts"""
certs = self.get_certificates()
alerts = []
for cert in certs:
days_left = self.days_until_expiry(cert['expires_on'])
domain = cert['domain_names'][0] if cert['domain_names'] else 'Unknown'
# Critical: Less than 7 days
if days_left < 7:
alerts.append({
'severity': 'CRITICAL',
'domain': domain,
'days_left': days_left,
'message': f'Certificate expires in {days_left} days!'
})
# Warning: Less than 30 days
elif days_left < 30:
alerts.append({
'severity': 'WARNING',
'domain': domain,
'days_left': days_left,
'message': f'Certificate expires in {days_left} days'
})
return alerts
def send_alerts(self, alerts: List[Dict]):
"""Send alerts via preferred notification method"""
for alert in alerts:
print(f"[{alert['severity']}] {alert['domain']}: {alert['message']}")
# Integrate with notification service (Discord, Slack, etc.)
# Usage
if __name__ == "__main__":
monitor = CertificateMonitor(
npm_url="https://npm.yourdomain.com",
api_token="your_api_token"
)
alerts = monitor.analyze_certificates()
if alerts:
monitor.send_alerts(alerts) GitOps Configuration Management
Manage NPM configuration as code for version control and disaster recovery.
infrastructure.yaml:
# NPM Configuration as Code
version: "1.0"
proxy_hosts:
- name: "home-assistant"
domains:
- "home.yourdomain.com"
forward:
scheme: "http"
host: "homeassistant"
port: 8123
ssl:
certificate: "wildcard"
force_ssl: true
http2: true
features:
websockets: true
block_exploits: true
caching: false
access_list: "local-only"
- name: "jellyfin"
domains:
- "media.yourdomain.com"
forward:
scheme: "http"
host: "jellyfin"
port: 8096
ssl:
certificate: "wildcard"
force_ssl: true
features:
websockets: true
block_exploits: true
access_list: "family"
- name: "grafana"
domains:
- "graphs.yourdomain.com"
forward:
scheme: "http"
host: "grafana"
port: 3000
ssl:
certificate: "wildcard"
force_ssl: true
access_list: "vpn-only"
access_lists:
- name: "local-only"
rules:
- action: "deny"
cidr: "0.0.0.0/0"
- action: "allow"
cidr: "192.168.0.0/16"
- name: "vpn-only"
rules:
- action: "deny"
cidr: "0.0.0.0/0"
- action: "allow"
cidr: "10.20.0.0/16"
- name: "family"
rules:
- action: "allow"
cidr: "192.168.0.0/16"
- action: "allow"
cidr: "10.20.0.0/16"
- action: "deny"
cidr: "0.0.0.0/0" Sync script to apply configuration:
#!/bin/bash
# npm-sync.sh - Apply GitOps configuration to NPM
python3 << 'EOF'
import yaml
import requests
import os
def sync_configuration():
with open('infrastructure.yaml', 'r') as f:
config = yaml.safe_load(f)
npm_url = os.getenv('NPM_URL')
api_token = os.getenv('NPM_API_TOKEN')
headers = {"Authorization": f"Bearer {api_token}"}
# Sync access lists
for access_list in config.get('access_lists', []):
# Implementation: Create or update access lists
print(f"Syncing access list: {access_list['name']}")
# Sync proxy hosts
for host in config.get('proxy_hosts', []):
# Implementation: Create or update proxy hosts
print(f"Syncing proxy host: {host['name']}")
sync_configuration()
EOF Troubleshooting Guide
502 Bad Gateway Error
Symptom: “502 Bad Gateway” when accessing service through NPM
Diagnostic Steps:
Verify backend service is running:
docker ps | grep <service-name> docker logs <service-name>Test connectivity from NPM to backend:
docker exec npm curl -v http://<forward-host>:<forward-port>Check forward hostname configuration:
- If Docker service on same network: Use container name (e.g.,
homeassistant) - If external service: Use IP address or resolvable hostname
- Never use
127.0.0.1orlocalhostfor Docker services
- If Docker service on same network: Use container name (e.g.,
Verify Docker network connectivity:
# Check if services are on same network docker network inspect <network-name> # Both NPM and backend should be listedEnable WebSocket support if needed:
- Services like Home Assistant, Node-RED require websockets
- Edit proxy host → Details tab → ✓ Websockets Support
Common Fixes:
- Change
localhostto container name or host IP - Verify port matches what service actually listens on
- Add services to same Docker network
- Check firewall rules on host
SSL Certificate Renewal Failures
Symptom: “Some challenges have failed” error; certificate stuck “issuing”
Diagnostic Steps:
Check Let’s Encrypt rate limits:
- Limit: 50 certificates per domain per week
- Solution: Use wildcard certificate instead
- Check status: https://crt.sh/?q=yourdomain.com
For HTTP Challenge failures:
# Test port 80 accessibility from external IP curl -v http://your-public-ip/.well-known/acme-challenge/test # Verify port forwarding # Verify DNS A record: nslookup yourdomain.comFor DNS Challenge failures:
# Verify DNS TXT record is created nslookup -type=TXT _acme-challenge.yourdomain.com # Should show acme validation string from Let's EncryptCheck DNS provider credentials:
- Verify API token is still valid
- Ensure token has “Edit DNS” permissions
- Regenerate token if expired
Check NPM logs:
docker logs npm --tail 100 -f # Look for specific error messages
Solutions:
- Use Let’s Encrypt staging environment for testing (Settings in SSL certificate)
- Wait 1 hour if rate limit hit, then retry
- For DNS challenge: increase propagation seconds to 60-120
- Delete failed certificate attempt and recreate
- Use single wildcard cert instead of multiple individual certs
Access Lists Not Working
Symptom: Access list rules not enforcing; still able to access when should be blocked
Diagnostic Steps:
Verify rule order:
- Rules evaluated top-to-bottom
- First match wins
- Most specific rules should be first
Test with curl from different IPs:
# From allowed IP curl -v https://service.yourdomain.com # Should return 200 OK # From blocked IP curl -v https://service.yourdomain.com # Should return 403 ForbiddenCheck client IP in NPM logs:
docker logs npm --tail 50 # Verify actual client IP (not proxy IP)Clear browser cache:
- Use private/incognito window
- Clear all cookies
- Disable any caching extensions
Solutions:
- Reorder rules (most specific first, default deny last)
- Ensure “Satisfy Any” is set correctly
- Don’t combine HTTP Basic Auth with app-level auth (Authorization header conflict)
- Check that access list is actually applied to proxy host
Port Forwarding Issues
Symptom: Services work internally but not accessible from external network
Diagnostic Steps:
Verify router port forwarding:
# From external network (use phone with mobile data): curl -v http://your-public-ip # Should connect to NPMRouter configuration:
- External port 80 → NPM internal IP:80
- External port 443 → NPM internal IP:443
Verify DNS resolution:
# From external network: nslookup yourdomain.com # Should return your public IPCheck ISP restrictions:
- Some ISPs block ports 80/443 for residential connections
- Test with alternative ports (8080, 8443)
- Consider VPN or Cloudflare Tunnel as alternative
Verify NPM is receiving requests:
docker logs npm -f # Should see access logs when making external requests
Solutions:
- Confirm port forwarding rules in router
- Update DNS A records to point to current public IP
- Use DDNS (DuckDNS, No-IP) if dynamic IP
- Check if ISP uses CGNAT (Carrier-Grade NAT) - requires VPN or tunnel solution
- Disable any host firewall temporarily to test
High Resource Usage
Symptom: NPM container using excessive CPU or memory
Diagnostic Steps:
Monitor resource usage:
docker stats npm # Watch CPU % and memory usageCheck for rate limiting issues:
docker logs npm | grep -i "rate limit" docker logs npm | grep -i "too many requests"Review proxy host configurations:
- Disable caching if enabled unnecessarily
- Check for redirect loops
- Review custom Nginx configurations
Solutions:
Set resource limits in docker-compose.yml:
services:
npm:
# ... other config ...
deploy:
resources:
limits:
cpus: '2'
memory: 2G
reservations:
cpus: '0.5'
memory: 512M Optimize Nginx configuration:
# Add to Custom Nginx Configuration
worker_processes auto;
worker_connections 1024; Best Practices and Recommendations
Security Best Practices
Critical Security Measures:
Never expose port 81 publicly
- Only access admin interface via local network or VPN
- Consider using SSH tunnel for remote access
- Block port 81 in firewall rules for external traffic
Use strong, unique passwords
- NPM admin password
- Database passwords
- HTTP Basic Auth passwords
- Consider using password manager
Implement multi-layered security
- Layer 1: Network (VPN, VLANs, firewall)
- Layer 2: Proxy (access lists, rate limiting)
- Layer 3: Application (app-level auth, 2FA)
Use DNS challenge for certificates
- More secure than HTTP challenge
- No need to expose port 80
- Enables wildcard certificates
- Works for completely private networks
Regular security audits
- Review access logs weekly
- Monitor certificate expiration
- Check audit logs for unauthorized changes
- Update NPM and Docker regularly
Certificate Management Strategy
Recommended Approach:
Use wildcard certificates
- Single cert covers all subdomains:
*.yourdomain.com - Reduces management overhead
- Avoids Let’s Encrypt rate limits
- Free via Let’s Encrypt
- Single cert covers all subdomains:
DNS challenge over HTTP challenge
- More secure (no port 80 exposure)
- Works behind VPN
- Required for wildcard certificates
- Cloudflare works great for homelabs
Certificate monitoring
- Set calendar reminders for renewal dates
- Monitor NPM logs for renewal failures
- Test backup/restore of certificates quarterly
Backup Strategy
Critical Data:
/datadirectory - NPM configuration and database/letsencryptdirectory - SSL certificates
Backup Schedule:
- Daily automated backups (retention: 30 days)
- Weekly offsite backups (retention: 90 days)
- Before major changes (manual backup)
Test restoration quarterly:
# Test restore process on separate instance
# Verify all proxy hosts work
# Confirm certificates are valid Network Architecture
Recommended Setup:
Internet
↓
Router (Port Forward 80, 443)
↓
Firewall/Firewall Rules
↓
NPM Container (DMZ/Isolated Network)
↓
Docker Network Bridge
↓
Backend Services (Separate Network) Advantages:
- Single point of entry
- Isolated NPM from backend services
- Easy to monitor and secure
- Clear traffic flow
Monitoring and Observability
Essential Monitoring:
Certificate Expiration
- Alert 30 days before expiry
- Alert on renewal failures
- Dashboard showing all cert status
Service Health
- Monitor 502/503 errors
- Track response times
- Alert on service downtime
Security Events
- Failed authentication attempts
- Access list violations
- Configuration changes (audit log)
Resource Usage
- CPU and memory trends
- Disk space for certificates
- Network bandwidth
Tools:
- Prometheus for metrics collection
- Grafana for visualization
- Uptime Kuma for availability monitoring
- Custom scripts for certificate checking
Upgrade Strategy
Safe Upgrade Process:
Backup before upgrading:
./npm-backup.shRead release notes:
- Check GitHub releases page
- Note any breaking changes
- Review migration steps
Test in staging (if possible):
- Deploy new version on test instance
- Verify proxy hosts work
- Test certificate renewal
Perform upgrade:
cd ~/npm docker compose pull docker compose up -dVerify functionality:
# Check logs docker logs npm --tail 100 # Test proxy hosts curl -I https://service.yourdomain.com # Verify admin interface # Check certificate statusRollback if needed:
# Restore from backup ./npm-restore.sh /backup/npm/npm_complete_YYYYMMDD_HHMMSS.tar.gz
Conclusion
Nginx Proxy Manager transforms the complex task of reverse proxy management into an accessible, GUI-driven experience perfect for homelabs. By following this guide, you now have:
- A production-ready NPM deployment with proper database configuration
- SSL/TLS certificates automatically managed via Let’s Encrypt
- Secure access controls protecting your services
- Automated backups ensuring disaster recovery capabilities
- API integration enabling infrastructure automation
- Troubleshooting knowledge to resolve common issues
Key Takeaways
- NPM excels for small to medium homelabs (5-20 services)
- Wildcard certificates via DNS challenge are the optimal SSL strategy
- Never expose the admin interface (port 81) to the internet
- Defense in depth - layer network, proxy, and application security
- Automate backups and test restoration regularly
- Monitor certificate expiration and renewal processes
When to Consider Alternatives
While NPM is excellent for most homelabs, consider alternatives if:
- Traefik: Managing 20+ services, need GitOps workflows, want automatic service discovery
- Caddy: Want simplest possible setup with fewer than 10 services
- Raw Nginx: Need absolute maximum performance and custom configurations
Next Steps
- Deploy NPM using the production docker-compose.yml
- Create proxy hosts for your existing services
- Set up wildcard SSL via DNS challenge
- Implement access lists for security
- Automate backups using provided scripts
- Monitor and optimize your deployment
Further Learning
- Official Documentation: https://nginxproxymanager.com
- GitHub Repository: https://github.com/NginxProxyManager/nginx-proxy-manager
- Community: r/homelab, r/selfhosted subreddits
- Related Guides: Docker networking, Let’s Encrypt deep dive, VPN setup
Resources
Official Links
Community Resources
Related Technologies
Guide Version: 1.0 Last Updated: December 2025 Word Count: ~6,000 words Estimated Completion Time: 45-60 minutes (hands-on)
This guide is part of the Homelabbing.io series on self-hosting and homelab infrastructure. For more guides on Docker, networking, security, and automation, visit homelabbing.io.