
If you run multiple services on a single VPS — WordPress, Node.js apps, Grafana, Portainer — you know the pain of managing ports and SSL certificates manually. Every new service means editing Nginx config files, running Certbot, and debugging syntax errors at midnight. Nginx Proxy Manager (NPM) solves this completely. With its clean Web UI you can set up a reverse proxy, map a custom domain, and issue a free Let's Encrypt SSL certificate in under two minutes — without touching a single Nginx config file.
What is Nginx Proxy Manager?
Nginx Proxy Manager is a web application that acts as a graphical frontend for managing Nginx reverse proxies. Instead of manually writing /etc/nginx/sites-available/mysite.conf, you fill in a form — Domain Name, Forward Hostname, Port — click Save, and NPM generates the correct Nginx configuration and requests an SSL certificate automatically.
How NPM Runs
NPM is distributed as a Docker image alongside a MariaDB database container. This makes installation trivial, upgrades clean, and removal complete — no leftover files scattered across the filesystem.
Core Features
- Proxy Hosts: Map domains to backend services with SSL in a few clicks
- Free SSL: Automatic Let's Encrypt certificates including Wildcard certs via DNS Challenge
- Access Lists: Restrict access by IP or add HTTP Basic Auth to any proxy host
- Redirection Hosts: Redirect one domain to another (301/302)
- Streams: Forward TCP/UDP traffic for non-HTTP protocols
- Custom Nginx Config: Add any Nginx directive through the Advanced tab per host
NPM vs the Alternatives
- NPM vs Manual Nginx: NPM is dramatically faster for standard setups. Manual Nginx gives you more granular control for edge cases but requires knowing Nginx syntax.
- NPM vs Traefik: Traefik is better suited for Kubernetes and microservices environments where config changes constantly. NPM wins for simplicity on a standard VPS.
- NPM vs Caddy: Caddy also does automatic HTTPS but NPM offers a Web UI that non-developers can use without any terminal access.
Best for: A VPS running two or more services that each need their own domain and SSL certificate, teams who want to delegate proxy management without SSH access, and anyone who finds Nginx config syntax frustrating.
Prerequisites
Before installing Nginx Proxy Manager, make sure you have the following in place.
- VPS running Ubuntu 22.04 or 20.04 — both are supported
- Docker and Docker Compose installed — see the Docker on VPS guide if you haven't set these up yet
- A domain with DNS A Records pointing to your VPS IP — DNS must be propagated before NPM can issue SSL certificates
- Root or sudo access on the server
- At least 512 MB RAM (1 GB or more recommended — NPM runs MariaDB alongside the app)
Open Required Ports in UFW
NPM needs three ports: 80 (HTTP), 443 (HTTPS), and 81 (NPM Web UI). Open them with the following commands:
# Open Port 80 (HTTP) and 443 (HTTPS) — required for reverse proxy and SSL ufw allow 80/tcp ufw allow 443/tcp # Open Port 81 for NPM Web UI — temporary; close it after setup ufw allow 81/tcp # Verify firewall status ufw status numbered
Security Note: Port 81 should only be open during initial setup and maintenance. After setup is complete, block Port 81 from the public internet and access the NPM UI via SSH tunnel instead (covered in the Security Hardening section).
Install Nginx Proxy Manager with Docker Compose
Docker Compose is the recommended way to install and manage NPM. It keeps everything in one directory, makes upgrades a single command, and makes backups straightforward.
Step-by-Step Installation
-
Create a directory for NPM
mkdir -p ~/npm && cd ~/npm
-
Create the docker-compose.yml file
nano ~/npm/docker-compose.yml
Paste the following content, replacing the passwords with strong values of your own:
version: '3.8' services: app: image: 'jc21/nginx-proxy-manager:latest' restart: unless-stopped ports: - '80:80' - '443:443' - '81:81' volumes: - ./data:/data - ./letsencrypt:/etc/letsencrypt db: image: 'jc21/mariadb-aria:latest' restart: unless-stopped environment: MYSQL_ROOT_PASSWORD: 'npm_root_pw_changeme' MYSQL_DATABASE: 'npm' MYSQL_USER: 'npm' MYSQL_PASSWORD: 'npm_pw_changeme' volumes: - ./mysql:/var/lib/mysql -
Start the containers
cd ~/npm && docker compose up -d
Docker will pull the images and start both containers. The first run may take 1–2 minutes while the images download.
-
Verify that both containers are running
cd ~/npm && docker compose ps
Both the
appanddbcontainers should show a status ofUp. -
Access the Web UI
Open a browser and navigate to
http://YOUR_VPS_IP:81(replaceYOUR_VPS_IPwith your actual VPS IP address).
Port conflict: If Nginx or Apache is already running and listening on ports 80 or 443, Docker will fail to bind those ports. Stop and disable the conflicting service first: systemctl stop nginx && systemctl disable nginx
First Login and Changing the Default Password
When you open the Web UI for the first time, log in with the default credentials:
- Email:
[email protected] - Password:
changeme
NPM will immediately prompt you to update your email address and set a new password. Do not skip this step — the default credentials are public knowledge.
First Login Best Practices
- Use your real email address — NPM and Let's Encrypt will use it for certificate expiry notifications
- Set a strong password — at least 16 characters mixing uppercase letters, numbers, and symbols
- NPM will automatically log you out after the credential change; log back in with the new details
Adding Your First Proxy Host
A Proxy Host maps a domain name to a backend service running on a specific port (either on the host or inside a Docker container on the same network), and issues an SSL certificate for that domain automatically.
How to Add a Proxy Host
-
Navigate to Hosts → Proxy Hosts → Add Proxy Host
Click the Hosts menu at the top, select Proxy Hosts, then click the green Add Proxy Host button on the right.
-
Fill in the Details tab
- Domain Names: Type your domain (e.g.
app.example.com) and press Enter to confirm it - Scheme: Select
httpfor services running plain HTTP - Forward Hostname / IP: Enter
localhostfor host-level services, or the container name if the service is in the same Docker network - Forward Port: The port your service listens on (e.g.
8080) - Block Common Exploits: Enable this (recommended for all hosts)
- Websockets Support: Enable if the service uses WebSockets (e.g. Socket.io, Grafana Live)
- Domain Names: Type your domain (e.g.
-
Configure SSL in the SSL tab
- Click the SSL tab
- Select Request a new SSL Certificate from the dropdown
- Enable Force SSL — automatically redirects HTTP to HTTPS
- Enable HTTP/2 Support — improves performance for modern browsers
- Enter your Email Address for Let's Encrypt notifications
- Check I Agree to the Let's Encrypt Terms of Service
-
Click Save
NPM generates the Nginx configuration and requests an SSL certificate from Let's Encrypt. This takes roughly 15–30 seconds. Once complete, the Proxy Host row turns Online in green.
DNS must resolve first: Let's Encrypt verifies domain ownership by sending an HTTP request to your domain. If the DNS A Record doesn't point to your VPS IP yet, SSL issuance will fail. Verify propagation at dnschecker.org before adding the proxy host.
Real-World Example: Running 3 Services on One VPS
This is a classic scenario: WordPress, Grafana, and Portainer all running on the same VPS, each needing its own domain and SSL certificate. Here is how to wire everything up in NPM.
Target Domain Mapping
blog.example.com→localhost:8080(WordPress)monitor.example.com→localhost:3000(Grafana)docker.example.com→localhost:9000(Portainer)
WordPress (Port 8080)
# In NPM Web UI Domain Names: blog.example.com Scheme: http Forward Hostname / IP: localhost Forward Port: 8080 Block Common Exploits: ON Websockets Support: OFF SSL: Request new cert, Force SSL ON, HTTP/2 ON
Grafana (Port 3000)
# In NPM Web UI Domain Names: monitor.example.com Scheme: http Forward Hostname / IP: localhost Forward Port: 3000 Block Common Exploits: ON Websockets Support: ON ← Grafana uses WebSocket for live dashboards SSL: Request new cert, Force SSL ON, HTTP/2 ON
Portainer (Port 9000)
# In NPM Web UI Domain Names: docker.example.com Scheme: http Forward Hostname / IP: localhost Forward Port: 9000 Block Common Exploits: ON Websockets Support: ON ← Portainer uses WebSocket SSL: Request new cert, Force SSL ON, HTTP/2 ON
Important: When both NPM and your services run as Docker containers, using localhost as the Forward Hostname will not work — each container has its own isolated network namespace. The correct approach is to put all containers on a shared Docker network and use container names as hostnames. See the next section for the right way to do this.
Docker Networks — The Correct Approach for Container-to-Container Traffic
When NPM and your backend services all run as Docker containers, the most reliable way to connect them is through a shared Docker network. Container names on the same network resolve to each other's IP addresses, and those names remain stable across container restarts even as the underlying IPs change.
Create a Shared Docker Network
# Create a network called npm_network docker network create npm_network
NPM docker-compose.yml with Network Configuration
Edit ~/npm/docker-compose.yml to add the network block:
version: '3.8'
services:
app:
image: 'jc21/nginx-proxy-manager:latest'
restart: unless-stopped
ports:
- '80:80'
- '443:443'
- '81:81'
volumes:
- ./data:/data
- ./letsencrypt:/etc/letsencrypt
networks:
- npm_network
db:
image: 'jc21/mariadb-aria:latest'
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD: 'npm_root_pw_changeme'
MYSQL_DATABASE: 'npm'
MYSQL_USER: 'npm'
MYSQL_PASSWORD: 'npm_pw_changeme'
volumes:
- ./mysql:/var/lib/mysql
networks:
- npm_network
networks:
npm_network:
external: trueBackend Service docker-compose.yml (e.g. WordPress)
Any service that needs to be proxied must join the same network:
version: '3.8'
services:
wordpress:
image: wordpress:latest
restart: unless-stopped
container_name: wordpress # Use this name as the Forward Hostname in NPM
environment:
WORDPRESS_DB_HOST: wp_db
WORDPRESS_DB_NAME: wordpress
WORDPRESS_DB_USER: wp_user
WORDPRESS_DB_PASSWORD: wp_pass_changeme
volumes:
- ./wp_data:/var/www/html
networks:
- npm_network # Same network as NPM
wp_db:
image: mysql:8.0
restart: unless-stopped
container_name: wp_db
environment:
MYSQL_DATABASE: wordpress
MYSQL_USER: wp_user
MYSQL_PASSWORD: wp_pass_changeme
MYSQL_ROOT_PASSWORD: root_pass_changeme
volumes:
- ./wp_mysql:/var/lib/mysql
networks:
- npm_network
networks:
npm_network:
external: trueWith this setup, set the Forward Hostname in NPM to wordpress (the container name) and the Forward Port to 80 — not 8080, because port mapping (host:container) only applies when accessing from outside Docker; within the same network containers communicate on their internal ports directly.
Verify Network Membership
# See which containers are connected to npm_network
docker network inspect npm_network
# List all running containers with their names and ports
docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"SSL Certificates — Let's Encrypt and Wildcard
NPM manages the full SSL certificate lifecycle without needing a separate Certbot installation. All certificates are visible in the SSL Certificates tab, including their expiry dates. NPM handles automatic renewal; you do not need to configure anything extra.
Wildcard Certificates (*.example.com)
A wildcard certificate covers all subdomains of a domain with a single cert — *.example.com covers blog.example.com, monitor.example.com, docker.example.com, and any new subdomain you create. This eliminates the need to request a new certificate each time you add a service.
Wildcard certs require a DNS Challenge (rather than the standard HTTP Challenge) to prove you control the domain's DNS. NPM supports Cloudflare, Route 53, Namecheap, and several other DNS providers.
Wildcard Certificate via Cloudflare DNS Challenge
-
Create a Cloudflare API Token
Log in to Cloudflare → Profile → API Tokens → Create Token → use the "Edit zone DNS" template and restrict it to your specific zone.
-
Add a new SSL Certificate in NPM
Go to SSL Certificates → Add SSL Certificate → Let's Encrypt
- Domain Names:
*.example.comandexample.com(add both to cover the root domain) - Use a DNS Challenge: enable
- DNS Provider: Cloudflare
- Credentials File Content:
dns_cloudflare_api_token = YOUR_CF_API_TOKEN - Propagation Seconds:
120(allow 2 minutes for DNS propagation)
- Domain Names:
-
Click Save
NPM uses the API token to create a TXT record in Cloudflare to prove domain ownership, then issues the wildcard certificate from Let's Encrypt.
-
Apply the wildcard cert to Proxy Hosts
When creating or editing a Proxy Host, in the SSL tab, select the existing wildcard certificate from the dropdown instead of requesting a new one.
Automatic Renewal
Let's Encrypt certificates expire after 90 days. NPM automatically renews certificates when they have fewer than 30 days remaining. No cron jobs, no manual steps — it just works.
Access Lists — Restricting Who Can Reach a Service
Access Lists let you control who can access each Proxy Host. The two most common use cases are IP whitelisting and HTTP Basic Authentication.
Creating an Access List
Go to Access Lists → Add Access List.
Option 1: IP Whitelist
Restrict a service to specific IP addresses — ideal for admin panels and internal tools.
# Access List Configuration Name: "Home and Office" Allow: 203.0.113.10 # your home IP Allow: 198.51.100.0/24 # your office subnet # Leave "Satisfy any" OFF to make this a strict whitelist
Option 2: HTTP Basic Authentication
Add a username/password prompt to services that have no built-in login, such as a Netdata dashboard or a simple web app.
# In the Access List — Authorization tab # Add Username and Password pairs # NPM auto-generates the htpasswd file behind the scenes
Applying an Access List to a Proxy Host
Edit any Proxy Host and select your Access List from the Access List dropdown in the Details tab. NPM will enforce the restriction immediately on save.
Custom Nginx Config (Advanced)
Every Proxy Host has an Advanced tab where you can paste raw Nginx directives that the standard UI does not expose. This is a powerful escape hatch for uncommon requirements.
Common Custom Config Snippets
Increase Upload Size for WordPress
client_max_body_size 100m;
Extend Timeouts for Long-Running Requests
proxy_read_timeout 300; proxy_connect_timeout 300; proxy_send_timeout 300;
Pass Real Client IP to WordPress (behind a proxy)
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; proxy_set_header X-Forwarded-Host $host;
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; add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
Disable Buffering for Streaming / Server-Sent Events
proxy_buffering off; proxy_cache off;
Warning: Only valid Nginx directives are accepted in the Advanced tab. If NPM detects a syntax error it will refuse to save. Test your directives in a plain Nginx config first if you are unsure.
Redirection Hosts
Redirection Hosts handle simple domain redirects without needing a full Proxy Host — for example, redirecting example.com to www.example.com, or pointing an old domain to a new one.
How to Set Up a Redirect
- Go to Hosts → Redirection Hosts → Add Redirection Host
-
Fill in the details
- Domain Names: Source domain, e.g.
example.com(without www) - Scheme: https
- Forward Domain Name: Destination, e.g.
www.example.com - HTTP Code:
301for permanent redirect (recommended for SEO) - Preserve Path: Enable so that
/aboutredirects towww.example.com/about
- Domain Names: Source domain, e.g.
-
Add an SSL Certificate
Issue an SSL certificate for the source domain too, otherwise HTTP visitors will not get a clean redirect to HTTPS.
Streams — TCP/UDP Proxy
Streams forward non-HTTP traffic such as database ports, mail ports, or game server connections — things a standard reverse proxy cannot handle. NPM exposes this through the Streams section.
Example: Forwarding a MySQL Port
Scenario: You want to connect to a MySQL instance inside a Docker container from a database client on your local machine (e.g. DBeaver), without exposing port 3306 directly to the internet.
# In Streams — Add Stream Incoming Port: 33060 # port clients connect to (avoid using 3306 directly) Forward Host: mysql_container # container name or IP Forward Port: 3306 TCP Forwarding: ON UDP Forwarding: OFF
Security warning: Exposing a database port to the internet — even through a proxy — is a significant risk. Always combine stream forwarding with an Access List that restricts access to known IPs only.
Logs and Debugging
When a Proxy Host shows an error status or goes Offline, here are the most effective ways to diagnose the problem.
View NPM Container Logs in Real Time
# Follow NPM app container logs in real time cd ~/npm && docker compose logs -f app # Show only the last 100 lines docker compose logs --tail=100 app # Check the database container logs docker compose logs -f db
Common Errors and Fixes
502 Bad Gateway
NPM cannot reach the backend service. Common causes:
- Backend service is down or crashed — check with
docker psorsystemctl status servicename - Forward Hostname is wrong — if using a container name, verify spelling and that the container is on the same network
- Forward Port is wrong — verify what port the service actually listens on
# Check running containers docker ps # Test connectivity from inside the NPM container docker exec npm-app-1 curl -s http://CONTAINER_NAME:PORT
SSL Certificate Fails to Issue
- DNS has not propagated — verify the A Record resolves to your VPS IP
- Port 80 is blocked — Let's Encrypt HTTP Challenge requires port 80 to be reachable
- Rate limited — Let's Encrypt allows 5 certificate requests per domain per week; repeated failures can trigger a rate limit
# Check that port 80 is reachable for your domain curl -I http://YOUR_DOMAIN # Verify DNS resolution dig +short YOUR_DOMAIN A
Too Many Redirects
Occurs when Force SSL is enabled in NPM and the backend service also redirects HTTP to HTTPS, creating an infinite redirect loop.
# Fix option 1: Disable the redirect in the backend service's own config # Fix option 2: Set Scheme to "https" in the NPM Proxy Host if the backend # already serves HTTPS natively
Connection Refused
The port NPM is forwarding to has nothing listening, or the backend container is not on the same Docker network.
# Verify containers in npm_network docker network inspect npm_network | grep -A2 '"Name"' # Connect a running container to npm_network at runtime docker network connect npm_network CONTAINER_NAME
Security Hardening for NPM
The NPM Web UI on Port 81 must never be directly accessible from the public internet. Anyone who can reach it can modify all your proxy configurations. Apply one or more of the following hardening measures.
Method 1: Block Port 81 from Public Internet
# Block port 81 in UFW after initial setup is complete ufw deny 81 # Verify ufw status numbered
Method 2: Access NPM Web UI via SSH Tunnel
After blocking Port 81, you can still reach the NPM UI securely by tunneling through SSH. Run this command on your local machine, not on the server:
# Run on your local machine ssh -L 81:localhost:81 user@YOUR_VPS_IP # Then open in your browser http://localhost:81
This creates an encrypted tunnel that forwards Port 81 on your local machine to Port 81 on the server through the SSH connection. Port 81 on the server remains closed to the public.
Method 3: Restrict Port 81 to Your IP Only
# Allow only your IP (replace 203.0.113.10 with your actual IP) ufw allow from 203.0.113.10 to any port 81 # Block all other IPs from port 81 ufw deny 81
Method 4: Change the Default Admin Credentials
Confirm you have changed the admin email and password away from the defaults ([email protected] / changeme). If you haven't yet, go to Admin → Profile in the NPM UI immediately.
Backup and Updates
Everything NPM needs to restore to a working state lives in three subdirectories under ~/npm/. Back these up and you can recreate your entire NPM setup on a new server.
Creating a Backup
# Stop containers first for a clean consistent backup cd ~/npm && docker compose stop # Copy data to a timestamped backup directory mkdir -p /backup/npm-$(date +%Y%m%d) cp -r ~/npm/data ~/npm/letsencrypt ~/npm/mysql /backup/npm-$(date +%Y%m%d)/ # Start containers again docker compose up -d # Confirm backup was created ls -la /backup/
Updating NPM to the Latest Version
# Pull the latest image cd ~/npm && docker compose pull # Recreate containers with the new image (minimal downtime) docker compose up -d # Confirm the update docker compose exec app nginx -v
Always back up before updating: Even though NPM updates are usually seamless, a backup of the data directory takes only seconds and protects you against potential database migration issues when jumping between major versions.
What Each Directory Contains
~/npm/data/— all NPM configuration: Proxy Hosts, Access Lists, SSL settings~/npm/letsencrypt/— all SSL certificates and private keys~/npm/mysql/— the MariaDB database NPM uses internally
Summary and Next Steps
Nginx Proxy Manager is an excellent tool for any VPS running multiple services. It turns what used to require Nginx expertise into a task anyone can complete in a browser, without sacrificing the flexibility that power users need through the Advanced tab and custom configurations.
NPM is the Right Choice When You...
- Run two or more services on one VPS and want each to have its own domain and SSL certificate
- Want Let's Encrypt SSL without learning Certbot command-line syntax
- Need to delegate proxy management to team members who do not have SSH access
- Are running a homelab, self-hosted services, or a development environment
Limitations to Keep in Mind
- Requires Docker — not suitable for servers where Docker is not installed or wanted
- Needs at least 512 MB RAM because it runs MariaDB alongside the app (1 GB+ recommended)
- For Kubernetes or large microservices architectures, Traefik or an Ingress Controller is a better fit
- NPM is a community open-source project — no formal commercial support
Recommended Next Steps
- Deploy WordPress + NPM + SSL in a single docker-compose file to see everything working together
- Add Grafana + Prometheus monitoring and expose it through NPM with an Access List
- Install Portainer and proxy it through NPM for a fully GUI-managed Docker environment
- Explore the NPM REST API for automating Proxy Host creation in CI/CD pipelines
Ready to Run Nginx Proxy Manager on Your VPS?
AsiaGB VPS gives you full root access, 1 GB RAM or more, and Docker support right out of the box. Data centers in Thailand and Singapore. Starting from 500 THB/month.
View AsiaGB VPS Plans