Reading time: ~14 minutes Audience: Homelabbers who want clean domains and HTTPS for all their Docker services
Why You Need a Reverse Proxy
You’ve got 10 Docker containers running — Nextcloud on port 8080, Pi-hole on port 8180, Portainer on port 9443 — and you’re typing http://192.168.1.50:32400 to access Plex. Every service has a different port, you have no HTTPS, and your browser keeps screaming “Not Secure.” There’s a better way.
A reverse proxy sits at the edge of your network, receives all incoming requests, and routes them to the correct backend service. Instead of memorizing ports, you access cloud.yourdomain.com, admin.yourdomain.com, and photos.yourdomain.com — all on standard port 443 with valid SSL certificates.
Nginx Proxy Manager (NPM) is the most beginner-friendly reverse proxy for homelab use. It wraps the powerful Nginx engine in a clean web GUI. No configuration files to edit. No CLI flags to memorize. Let’s Encrypt integration is built-in — one click and you have HTTPS.
By the end of this guide, you’ll have:
- A running Nginx Proxy Manager instance on Docker
- Custom subdomains for all your services (
*) - Auto-renewing free SSL certificates from Let’s Encrypt
- Portainer integration for easy stack management
What Is Nginx Proxy Manager?
Overview
Nginx Proxy Manager is an open-source web interface for managing Nginx reverse proxy hosts. It was created by Jamie Curnow (jc21) and has grown to over 33,000 GitHub stars. The goal: make Nginx accessible to people who don’t want to write config files.
Instead of hand-editing nginx.conf blocks, you fill in a web form: domain name, backend IP and port, select SSL options — done. The GUI updates Nginx configuration automatically and reloads the service.
Key Features
| Feature | Description |
|---|---|
| GUI Proxy Host Manager | Add, edit, and delete proxy hosts through a web dashboard |
| Automatic Let’s Encrypt SSL | HTTP and DNS challenge support; wildcard certificates |
| Access Lists | IP-based allow/deny rules for individual proxy hosts |
| 404 Redirects | Custom landing pages for unconfigured subdomains |
| Stream Proxies | Proxy raw TCP/UDP streams (ideal for game servers, SSH) |
| Custom Nginx Snippets | Inject raw Nginx config for advanced headers, CORS, or WebSocket |
| Let’s Encrypt Auto-Renewal | Certificates renew automatically every 60 days |
| Docker Healthcheck | Built-in health status endpoint |
NPM vs Traefik vs Caddy: Which Reverse Proxy?
NPM isn’t the only reverse proxy for homelab use. Here’s how the three main contenders compare:
| Feature | Nginx Proxy Manager | Traefik | Caddy |
|---|---|---|---|
| GUI | ✅ Full web UI | ❌ Config files only | ❌ Config files only |
| Learning curve | Low | High | Medium |
| Docker auto-discovery | ❌ Manual setup | ✅ Native Docker label discovery | ❌ Manual (or Caddy-Docker-Proxy plugin) |
| Let’s Encrypt | Built-in (+ DNS Challenge) | Built-in | Built-in (automatic) |
| Configuration syntax | Web form | YAML / labels | Caddyfile (simple DSL) |
| Stream proxy (TCP/UDP) | ✅ | ✅ | ✅ (Caddy L4) |
| Best for | Beginners, mixed teams, quick setup | Kubernetes, Docker Swarm, auto-scaling | Developers who like Caddyfile simplicity |
| GitHub Stars | 33k+ | 55k+ | 60k+ |
When to pick NPM: You want a GUI, you’re managing a handful of services, you don’t need automatic Docker container discovery, and you want to stop editing config files.
When to pick Traefik: You run dozens of containers, want auto-discovery via Docker labels, and are comfortable with YAML configuration.
When to pick Caddy: You like a simple config language (Caddyfile), want automatic HTTPS with zero configuration, and don’t need a web GUI.
For 90% of homelabbers, Nginx Proxy Manager is the right choice. It’s fast to set up, forgiving of mistakes, and the GUI means you’ll actually remember how it works when you come back to it six months later.
Prerequisites
Hardware Requirements
- Any x86_64 or ARM64 device running Docker (mini PC, Raspberry Pi 4/5, Proxmox LXC, or old laptop)
- 512MB RAM minimum (1GB recommended)
- 2GB storage for configuration and certificate data
Software Requirements
- Docker Engine 24.x+ with Docker Compose v2+
- Linux host (Debian 12, Ubuntu 22.04/24.04, or Proxmox LXC)
- Firewall rules allowing ports 80 and 443 (if using HTTP challenge or public access)
Networking Requirements
- A domain name (e.g., from Cloudflare, Namecheap, or Porkbun) — or local DNS via Pi-hole for internal-only use
- DHCP reservation or static IP for your Docker host
- Ports 80 and 443 forwarded on your router (if exposing services publicly) — or use Cloudflare Tunnel to skip port forwarding entirely
Step 1: Create the Docker Compose File
First, create a dedicated directory and the compose file:
mkdir -p ~/nginx-proxy-manager && cd ~/nginx-proxy-manager
Create docker-compose.yml:
version: '3.8'
services:
nginx-proxy-manager:
image: jc21/nginx-proxy-manager:latest
container_name: npm
restart: unless-stopped # Auto-restart on crash or host reboot
ports:
- "80:80" # HTTP — Let's Encrypt HTTP challenge
- "443:443" # HTTPS — secure traffic to your services
- "81:81" # Admin web UI (accessible at http://host:81)
volumes:
- ./data:/data # NPM configuration (proxy hosts, SSL certs, settings)
- ./letsencrypt:/etc/letsencrypt # Let's Encrypt certificate storage
environment:
- DISABLE_IPV6=true # Set to false if you use IPv6
networks:
- npm_network # Custom bridge for inter-container communication
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:81"]
interval: 30s
timeout: 10s
retries: 3
networks:
npm_network:
driver: bridge # Isolated network for NPM + your services
What each piece does:
jc21/nginx-proxy-manager:latest— Official upstream image from the project’s Docker Hub, maintained by the NPM creator.restart: unless-stopped— Ensures NPM comes back up after a power outage or host reboot.- Port 81 — The admin dashboard. Change the host port if 81 is already in use (e.g.,
"8081:81"). npm_network— A custom Docker bridge network. All containers you want NPM to reach must join this same network. Without it, NPM can’t resolve container names.healthcheck— Docker checks the admin UI every 30 seconds. The container status indocker psreflects actual health, not just “running.”
⚠️ Important: If you’re running Pi-hole or another service that binds port 80, stop it first. NPM must own ports 80 and 443. Pi-hole can use alternative ports — adjust its compose file to map
"8180:80"and"8143:443".
Step 2: Deploy and Log In
Start the stack:
docker compose up -d
Check that it’s running:
docker compose ps
You should see npm with status “healthy” after a few seconds.
Open your browser and navigate to http://[your-host-ip]:81. Log in with the default credentials:
- Email:
[email protected] - Password:
changeme
Change the password immediately. Click your email address in the top-right corner → Edit Details → enter your full name, a real email address, and a strong password.
🛡️ Security tip: Change the default admin email to a real one. Let’s Encrypt sends expiration warnings to this email address. If you leave it as
[email protected], you’ll never get renewal failure alerts.
Dashboard Overview
| Section | Purpose |
|---|---|
| Proxy Hosts | Main view — add and manage domain-to-backend mappings |
| Redirection Hosts | HTTP-to-HTTPS redirects or domain-to-other-domain redirects |
| Streams | Raw TCP/UDP forwarding (SSH, game servers, RTMP) |
| 404 Hosts | Custom “page not found” landings for unknown subdomains |
| SSL Certificates | Request, renew, and manage Let’s Encrypt certificates |
| Access Lists | IP-based allow/deny rules you can attach to proxy hosts |
| Users | Create additional admin or viewer accounts |
| Audit Log | See who changed what and when |
Step 3: Add Your First Proxy Host
Let’s expose a service at a clean subdomain. We’ll use a Nextcloud container as an example — assume it’s running on the same Docker host with container name nextcloud, listening on port 80 internally.
Make sure Nextcloud is on the same Docker network. Attach it to npm_network:
docker network connect npm_network nextcloud
Or, better, define the network in every service’s docker-compose.yml:
# In your Nextcloud's docker-compose.yml:
networks:
npm_network:
external: true # Use the pre-existing NPM network
Now, in the NPM admin dashboard:
- Click Proxy Hosts → Add Proxy Host
- Domain Names:
cloud.yourdomain.com - Scheme:
http - Forward Hostname / IP:
nextcloud(Docker container name resolves automatically on the shared bridge network) - Forward Port:
80(the port inside the container, not the host-mapped port) - Cache Assets: Leave off (Nextcloud handles its own caching)
- Block Common Exploits: ✅ Enable this — it’s a one-click SQL injection and path traversal guard
- Websockets Support: Enable if the app requires live updates (Portainer, Home Assistant, Immich, Uptime Kuma)
- Click Save
You can now access http://cloud.yourdomain.com and NPM routes traffic to Nextcloud. No port number needed.
Enabling WebSockets for Real-Time Apps
Some apps need WebSocket connections to function properly. If you see “Connection lost” or blank dashboards, enable Websockets Support in the Advanced tab of that proxy host entry.
| App | Needs WebSockets? |
|---|---|
| Portainer | ✅ Yes (live container logs) |
| Home Assistant | ✅ Yes (real-time state updates) |
| Uptime Kuma | ✅ Yes (monitor status pulses) |
| Immich | ✅ Yes (live photo uploads) |
| Grafana | ✅ Yes (dashboard refresh) |
| Nextcloud | ❌ No (standard HTTP is fine) |
| Pi-hole | ❌ No |
| Plex / Jellyfin | ❌ No |
Custom Nginx Location Snippets
For advanced setups, the Advanced tab lets you paste raw Nginx directives. This is useful for:
# Override upload size limit (Nextcloud needs this)
client_max_body_size 10G;
# Add security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
# Allow Home Assistant ingress
proxy_buffering off;
Step 4: Enable Free SSL with Let’s Encrypt
HTTP is fine for testing, but production services need HTTPS. Nginx Proxy Manager makes SSL certificates dead simple with two challenge options.
Option A: HTTP Challenge (Simplest, Requires Port 80 Forwarded)
- Go to your proxy host → SSL tab
- Select Request a new SSL Certificate
- Choose HTTP Challenge
- Enter your email address (for Let’s Encrypt expiry alerts)
- Agree to the Let’s Encrypt Terms of Service
- Click Save
NPM handles the ACME challenge, fetches the certificate, and auto-reloads Nginx. Your service now responds on https://cloud.yourdomain.com.
Option B: DNS Challenge (Better — Works Without Public Ports, Supports Wildcards)
DNS Challenge is the preferred method because:
- It works even if your ISP blocks ports 80/443
- You can request wildcard certificates (
*.yourdomain.com) and use one cert for all subdomains - No HTTP listener needed on the public internet
Step-by-step for Cloudflare:
- Go to Cloudflare Dashboard → your domain → API Tokens
- Create a token with permissions:
Zone:Read+DNS:Editfor your zone - Copy the token
Back in NPM:
- Navigate to SSL Certificates → Add SSL Certificate → Let’s Encrypt
- Enter
*.yourdomain.comandyourdomain.comin the Domain Names field (wildcard + apex) - Select Use a DNS Challenge
- Choose Cloudflare as the DNS provider
- Paste your API token into the credentials field
- Agree to the Terms of Service
- Click Save
NPM creates a TXT record in your Cloudflare DNS, validates it with Let’s Encrypt, and stores the wildcard certificate. This process takes about 15 seconds.
Now apply it: go back to any proxy host → SSL tab → select your new *.yourdomain.com certificate from the dropdown → Save.
Force SSL (HTTP → HTTPS Redirect)
In the SSL tab of each proxy host, enable Force SSL. This adds an automatic HTTP-to-HTTPS redirect so nobody accidentally uses the insecure URL.
Certificate Auto-Renewal
NPM automatically attempts renewal 30 days before expiry. Renewals use the same challenge method as the original request. If a renewal fails, NPM logs the error and retries daily. Let’s Encrypt also sends expiry warnings to the email you provided.
# Check renewal logs
docker logs npm 2>&1 | grep -i "renew"
Step 5: Integrate with Portainer (Optional)
If you run Portainer for container management, you can deploy NPM as a Portainer stack for a unified workflow.
In Portainer:
- Go to Stacks → Add Stack
- Name:
npm - Paste the
docker-compose.ymlfrom Step 1 - Under Network, ensure the stack is attached to an external network (
npm_network) - Click Deploy the stack
Portainer now shows NPM’s container status, logs, and resource usage in one dashboard. You can restart or update NPM without touching the CLI.
For services managed via Portainer stacks, add them to the NPM network by editing their stack’s compose file:
networks:
default:
name: npm_network
external: true
This ensures every service in the stack can be reached by NPM via its container name.
💡 Pro tip: Keep NPM in its own stack, separate from your application stacks. This way, you can restart or update services without affecting the reverse proxy.
Troubleshooting FAQ
Q: “502 Bad Gateway” when I try to access my service
This is the most common error and almost always means NPM can’t reach the backend container.
Checklist:
- Are both containers on the same Docker network? Run
docker network inspect npm_networkand confirm both containers are listed. - Is the Forward Port correct? Use the internal container port (e.g.,
80), not the host-mapped port (e.g.,8080). - Is the container actually running?
docker ps | grep your-container - Can NPM resolve the container name? Try
docker exec npm ping nextcloud— if it fails, they’re not on the same network. - Use the IP address instead of container name temporarily:
docker inspect -f '{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}' nextcloud
Q: My SSL certificate shows as invalid or expired
- Check the NPM logs:
docker logs npm 2>&1 | grep -i "error\|ssl\|cert" - For DNS challenges: verify the API token still has
Zone:ReadandDNS:Editpermissions - For HTTP challenges: verify port 80 is accessible from the internet (use canyouseeme.org)
- Force manual renewal: in SSL Certificates → click the three-dot menu → Renew Now
- Check the email address on your SSL certificate entry — Let’s Encrypt sends warnings to it
Q: WebSockets not working for Portainer / Home Assistant
In the proxy host’s Advanced tab, you need both the WebSockets Support toggle and sometimes additional headers:
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 86400;
Q: Can’t access NPM admin panel on port 81
- Check if another service is using port 81:
sudo lsof -i :81 - Verify your firewall:
sudo ufw status— if UFW is active, allow port 81:sudo ufw allow 81 - If the host firewall is iptables-based, check Docker’s rules:
sudo iptables -L DOCKER - Try mapping a different port in
docker-compose.yml: change"81:81"to"8181:81"
Q: Do I need NPM if I use Cloudflare Tunnel?
They solve different problems:
- Cloudflare Tunnel securely exposes your services to the public internet without opening router ports. It runs as a daemon on your server and proxies traffic through Cloudflare’s edge.
- Nginx Proxy Manager routes traffic within your LAN and provides local HTTPS with custom domains.
Real-world combo: Use Cloudflare Tunnel for public access (cloud.yourdomain.com from anywhere), and NPM for local access (cloud.home) — both pointing at the same backend containers. Best of both worlds.
Q: How do I back up my NPM configuration?
Back up two directories:
# Stop NPM first (optional but safer)
docker compose down
# Copy the data and certificate volumes
sudo cp -r ~/nginx-proxy-manager/data ~/backups/npm-data-$(date +%Y%m%d)
sudo cp -r ~/nginx-proxy-manager/letsencrypt ~/backups/npm-letsencrypt-$(date +%Y%m%d)
# Restart
docker compose up -d
Restoring is the reverse: copy the directories back and start the container. NPM picks up all proxy hosts and SSL certificates automatically.
Q: Can I use NPM for local-only services (no public domain)?
Yes. Use Pi-hole or AdGuard Home for local DNS:
- In Pi-hole: Local DNS → DNS Records
- Add:
cloud.home→192.168.1.50(your Docker host IP) - In NPM: add a proxy host for
cloud.homepointing at your container - Request an SSL certificate using DNS Challenge (no public port 80 needed)
This gives you valid HTTPS for internal-only services — even on .home or .local domains — using DNS Challenge with Cloudflare or DuckDNS.
Next Steps
- [internal_link] Secure your DNS with Pi-hole vs AdGuard Home
- [internal_link] Deploy a monitoring stack with Grafana + Prometheus
- [internal_link] Manage all your containers with Portainer Docker Management
- [internal_link] Run Proxmox as your hypervisor with the Proxmox Beginner Guide 2026
- [internal_link] Self-host Google Photos alternative with Immich Reverse Proxy Setup
References
- Nginx Proxy Manager GitHub
- Official Documentation
- Let’s Encrypt Documentation
- Cloudflare API Token Guide
Have questions or want to share your NPM setup? Join us in the r/selfhosted community. If this guide helped you, check out our other Docker Compose tutorials — every one is copy-paste ready.