Install Nginx Proxy Manager: Web UI Reverse Proxy on VPS
Running multiple services on a single VPS — Nextcloud, Gitea, Portainer, Grafana — means managing one Nginx config file per service, every single time you add something new. It's tedious and error-prone. Nginx Proxy Manager (NPM) solves this with a clean Web UI where you add proxy hosts, request SSL certificates, and manage access controls through point-and-click — no config files required.
NPM runs in Docker, supports automatic Let's Encrypt SSL (including wildcard certs via DNS challenge), and provides IP/password-based Access Lists. This guide walks through a complete setup from installation to production-ready configuration.
What Is Nginx Proxy Manager?
Nginx Proxy Manager wraps Nginx inside a Docker container and adds an Admin UI for managing:
- Proxy Hosts — Route domains/subdomains to backend services (http://localhost:PORT)
- SSL Certificates — Auto-issue Let's Encrypt certs, including wildcard certs via DNS challenge
- Redirections — HTTP → HTTPS or domain migration redirects
- Streams — TCP/UDP proxying (database ports, SSH, game servers)
- Access Lists — HTTP Basic Auth or IP whitelists per proxy host
Requirements
- VPS running Ubuntu 20.04/22.04/24.04 or Debian 11/12
- Docker and Docker Compose installed
- Ports 80, 443, and 81 open (port 81 is the Admin UI)
- A domain with DNS A record pointing to your VPS IP
- At least 512 MB RAM (1 GB recommended)
Install Docker and Docker Compose
If Docker isn't installed yet, run the official install script on Ubuntu/Debian:
# Official Docker install
curl -fsSL https://get.docker.com | sh
# Add current user to docker group (avoid sudo on every command)
sudo usermod -aG docker $USER
newgrp docker
# Verify
docker --version
docker compose version
Create Docker Compose File for NPM
Create a working directory and docker-compose.yml:
mkdir -p ~/nginx-proxy-manager && cd ~/nginx-proxy-manager
Create docker-compose.yml:
services:
app:
image: 'jc21/nginx-proxy-manager:latest'
restart: unless-stopped
ports:
- '80:80' # HTTP
- '443:443' # HTTPS
- '81:81' # Admin UI
volumes:
- ./data:/data
- ./letsencrypt:/etc/letsencrypt
Start NPM:
docker compose up -d
# Check status
docker compose ps
docker compose logs -f app
./data. For high-availability or multi-instance setups, switch to MySQL/MariaDB by adding the DB_MYSQL_* environment variables to the compose file.First Login to Admin UI
Open http://YOUR-VPS-IP:81 in your browser and log in with the default credentials:
- Email:
[email protected] - Password:
changeme
NPM immediately prompts you to change both the email and password. Use a strong password — the Admin UI controls all traffic routing on your server.
Add a Proxy Host (Forward Domain → Service)
Example: forward app.example.com to Portainer running on localhost:9000
- Go to Proxy Hosts → Add Proxy Host
- Domain Names:
app.example.com - Forward Hostname/IP:
172.17.0.1(Docker host bridge IP) orhost.docker.internal - Forward Port:
9000 - Enable Block Common Exploits
- Go to the SSL tab → Request a new SSL Certificate → enable Force SSL and HTTP/2 Support
- Enter your Let's Encrypt email → Save
portainer) as the Forward Hostname. If it runs on the host, use 172.17.0.1 (Linux bridge) or host.docker.internal.Wildcard SSL with DNS Challenge
For a wildcard cert (*.example.com), you must use DNS-01 challenge through a supported provider such as Cloudflare:
- Go to SSL Certificates → Add SSL Certificate → Let's Encrypt
- Domain Names:
*.example.comandexample.com - Enable Use a DNS Challenge → select Cloudflare
- Enter your Cloudflare API Token (needs Zone:DNS:Edit permission)
- Save — NPM creates a DNS TXT record to verify ownership and issues the cert automatically
Access Lists for Service Protection
To restrict access to sensitive services (Grafana, admin panels, internal tools):
- Go to Access Lists → Add Access List
- Name it (e.g.,
internal-only) - Authorization tab — add username/password pairs for HTTP Basic Auth
- Access tab — specify allowed IPs/CIDRs (e.g.,
192.168.1.0/24) or leave blank to use Basic Auth only - Edit any Proxy Host → select the Access List you created
TCP/UDP Stream Proxying
NPM also handles TCP/UDP streams. Go to Streams → Add Stream, specify the Incoming Port and Forward Host/Port. This is useful for exposing database ports or game server ports without SSH tunnels — just be careful not to expose sensitive services without access controls.
Verify Nginx Proxy Manager Is Working
# Check container status
docker compose ps
# Stream live logs
docker compose logs -f app
# Test HTTP response headers on a configured domain
curl -I https://app.example.com
# Check SSL certificate expiry
echo | openssl s_client -connect app.example.com:443 2>/dev/null | openssl x509 -noout -dates
# Inspect generated Nginx config
docker exec -it nginx-proxy-manager-app-1 cat /etc/nginx/conf.d/default.conf
# Confirm ports are listening
ss -tlnp | grep -E ':80|:81|:443'
Troubleshooting Common Issues
Error: "Port 80 is already in use"
Another Nginx or Apache instance is binding port 80. Stop those services before starting NPM.
# Find the process using port 80
sudo ss -tlnp | grep :80
sudo systemctl stop nginx apache2 2>/dev/null
sudo systemctl disable nginx apache2
# Start NPM
docker compose up -d
Error: "SSL Certificate could not be obtained"
Let's Encrypt can't verify domain ownership. Common causes: DNS not propagated, port 80 blocked by firewall, or Cloudflare proxy interfering.
# Verify domain resolves to correct IP
dig +short app.example.com
# Test port 80 is publicly reachable
curl -I http://app.example.com
# Check NPM error logs
docker compose logs app | grep -i "error\|cert\|acme"
# If using Cloudflare: set SSL mode to "Full" (not "Off")
# Or temporarily disable the Cloudflare proxy (orange cloud) during cert issuance
Error: "502 Bad Gateway" after adding proxy host
NPM can't reach the backend service. Verify the IP and port are correct and the service is running.
# Test connectivity from inside NPM container
docker exec -it nginx-proxy-manager-app-1 sh -c "curl http://172.17.0.1:9000"
# On Linux, host.docker.internal requires extra_hosts in compose:
# extra_hosts:
# - "host.docker.internal:host-gateway"
# Confirm target service is running
docker ps | grep portainer
ss -tlnp | grep 9000
Error: Can't reach Admin UI on port 81
The VPS firewall is blocking port 81, or the container isn't running yet.
# Allow port 81 temporarily for initial setup
sudo ufw allow 81/tcp
# Confirm container is running
docker compose ps
# After setup: close port 81 to the internet, proxy it through NPM instead
# Create proxy host: admin.example.com → localhost:81
sudo ufw delete allow 81/tcp
Container restarts in a loop / NPM crashes on startup
Typical causes: wrong permissions on data directory, port conflict, or full disk.
# Check exit reason in logs
docker compose logs --tail=50 app
# Check disk space (SQLite write failures cause crashes)
df -h
# Full reset (WARNING: deletes all config and certs)
docker compose down
sudo rm -rf ./data ./letsencrypt
docker compose up -d
Frequently Asked Questions
How does NPM compare to Traefik and Caddy?
NPM prioritizes ease of use through a Web UI — ideal for those who don't want to edit config files. Traefik uses Docker labels for automatic service discovery, making it better for dynamic environments and Kubernetes. Caddy uses a simple Caddyfile with built-in automatic HTTPS, no UI needed. NPM wins on beginner-friendliness; Traefik wins on production flexibility at scale.
Is NPM secure enough for production use?
Yes, for self-hosted and small-to-medium production deployments, provided you: (1) change default credentials immediately after first login, (2) close port 81 from the internet after setup and proxy the Admin UI through NPM itself, (3) keep the Docker image updated with docker compose pull && docker compose up -d, and (4) apply Access Lists to all sensitive services.
How do I back up and restore NPM?
All data lives in two directories: ./data (configuration, SQLite DB) and ./letsencrypt (SSL certificates). Backing up both directories is sufficient:
# Backup
tar -czf npm-backup-$(date +%Y%m%d).tar.gz ./data ./letsencrypt
# Restore on a new server
tar -xzf npm-backup-20260903.tar.gz
docker compose up -d
Let's Encrypt certs are valid for 90 days. NPM auto-renews them, but restoring from backup avoids waiting for a fresh issuance on a new server.