Install & Use Nginx Proxy Manager on VPS: Web UI for Reverse Proxy and SSL

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

NPM vs the Alternatives

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.

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

  1. Create a directory for NPM
    mkdir -p ~/npm && cd ~/npm
  2. 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
  3. 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.

  4. Verify that both containers are running
    cd ~/npm && docker compose ps

    Both the app and db containers should show a status of Up.

  5. Access the Web UI

    Open a browser and navigate to http://YOUR_VPS_IP:81 (replace YOUR_VPS_IP with 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:

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

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

  1. 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.

  2. Fill in the Details tab
    • Domain Names: Type your domain (e.g. app.example.com) and press Enter to confirm it
    • Scheme: Select http for services running plain HTTP
    • Forward Hostname / IP: Enter localhost for 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)
  3. 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
  4. 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

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: true

Backend 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: true

With 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

  1. 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.

  2. Add a new SSL Certificate in NPM

    Go to SSL Certificates → Add SSL Certificate → Let's Encrypt

    • Domain Names: *.example.com and example.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)
  3. 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.

  4. 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

  1. Go to Hosts → Redirection Hosts → Add Redirection Host
  2. 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: 301 for permanent redirect (recommended for SEO)
    • Preserve Path: Enable so that /about redirects to www.example.com/about
  3. 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:

# 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

# 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

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...

Limitations to Keep in Mind

Recommended Next Steps

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