Tutorial

Nginx Proxy Manager on VPS: Reverse Proxy with Auto SSL

Deployment9 min read8 steps

When several applications run on the same VPS, each one listens on a different port and SSL becomes a headache to manage manually. Nginx Proxy Manager (NPM) solves this problem with a web interface that automates Let's Encrypt certificates, routes domains to the right containers, and requires no nginx configuration file editing. This guide covers installation, proxy host configuration, and a comparison with Caddy and Traefik to help you choose the right tool for your situation.

Contents· Why a reverse proxy on a VPS1/10
  1. 01Why a reverse proxy on a VPS
  2. 02What Nginx Proxy Manager simplifies
  3. 03Prerequisites
  4. 04Installing NPM with Docker Compose
  5. 05Adding a first proxy host
  6. 06Configure a subdomain with forced HTTPS redirect
  7. 07Advanced case: proxy for a Docker app without exposed port
  8. 08Access Lists: protecting the backoffice with authentication
  9. 09Comparison: Nginx Proxy Manager vs Caddy vs Traefik
  10. 10NPM's limits and when to migrate to Traefik

Why a reverse proxy on a VPS

A VPS exposes a single public IP address. If you run three Docker applications — an API, a front-end and an admin tool — each occupies a different port: 3000, 8080, 9000. Without a reverse proxy, your visitors must type the port in the URL, SSL certificates must be managed application by application, and exposing all those ports publicly increases the attack surface.

A reverse proxy centralizes traffic entry: it receives all requests on ports 80 and 443, inspects the domain name or path, then forwards the request to the right container on the internal network. Applications no longer expose public ports. SSL terminates at the proxy level, which redistributes plain HTTP over the private Docker network.

This architecture offers three concrete benefits: a single certificate management point, network isolation of applications, and the ability to add or remove an app without touching the others.

What Nginx Proxy Manager simplifies

  • Web interface: add, modify and delete proxy hosts without command line or manual reload
  • One-click Let's Encrypt SSL: NPM requests, renews and deploys certificates automatically via HTTP-01 or DNS-01
  • Wildcard DNS: a single certificate for *.mydomain.com if your DNS provider supports the Certbot API
  • Access lists: HTTP basic authentication or IP whitelisting directly from the interface
  • Redirects and custom URLs: HTTP to HTTPS, 301/302 redirects, without modifying nginx.conf
  • Reload without interruption: NPM reloads the nginx configuration in the background, without downtime

Prerequisites

To follow this guide, you need a VPS running Ubuntu 22.04 or Debian 12 with at least 1 GB of RAM (2 GB recommended if multiple apps run simultaneously). Docker Engine and Docker Compose v2 must be installed.

Ports 80 and 443 must be accessible from outside. Check that your firewall (ufw or control panel rules) allows them. If you use a cloud firewall (security group, VPS firewall), open these two ports for inbound traffic.

Finally, you must own at least one domain name or subdomain pointing to your VPS IP. NPM can manage multiple domains simultaneously — the minimum requirement is that a DNS A record exists for each domain you want to proxy.

Installing NPM with Docker Compose

  1. Create the directory structure

    Create a dedicated folder and navigate into it:

    mkdir -p /opt/npm && cd /opt/npm

    This folder will contain the Compose file and NPM's persistent volumes (SQLite database, certificates, logs).

  2. Write docker-compose.yml

    Create the file /opt/npm/docker-compose.yml with the following content:

    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:
      default:
        name: proxy-net
        external: false

    Port 81 is the administration interface. The proxy-net network will be shared with your other containers so they can be reached without exposing public ports.

  3. Start NPM

    Launch the container in the background:

    docker compose up -d

    Wait 30 to 60 seconds for NPM to initialize its database. Verify that the three ports are listening:

    ss -tlnp | grep -E ':(80|81|443)'
  4. First login and password change

    Open http://YOUR_VPS_IP:81 in your browser. Default credentials are [email protected] / changeme.

    NPM forces you to change the email address and password on first login. Use a valid address: it will be used for Let's Encrypt certificate expiry notifications.

    Note: port 81 is publicly exposed. Immediately configure an access list (see "Access Lists" section) or filter this port at the firewall level to restrict it to your IP.

Adding a first proxy host

  1. Create a new proxy host

    In the NPM interface, click Proxy Hosts then Add Proxy Host. Fill in the Domain Names field with your domain, for example app.mydomain.com. Make sure the DNS A record for this subdomain already points to your VPS IP — Let's Encrypt will verify this resolution.

  2. Configure the destination

    In the Forward Hostname / IP and Forward Port fields, enter the host and port of your application. If the application runs in a Docker container on the same proxy-net network, use the Docker service name as the hostname (example: myapp and port 3000). Check Block Common Exploits to enable basic filtering rules.

  3. Enable Let's Encrypt

    Switch to the SSL tab in the same window. From the dropdown, select Request a new SSL Certificate. Check Force SSL to automatically redirect HTTP to HTTPS, and HTTP/2 Support to enable HTTP/2. Enter your email address, accept the Let's Encrypt terms, then click Save.

    NPM immediately launches the certificate request via the HTTP-01 challenge. In less than a minute, your domain is accessible via HTTPS with a valid certificate.

  4. Verify the result

    The proxy host list now shows your entry with a green SSL badge. Test from your browser or with curl:

    curl -I https://app.mydomain.com

    The response should contain HTTP/2 200 and a server: nginx header. Certificate renewal is automatic — NPM relaunches the request 30 days before expiry.

Configure a subdomain with forced HTTPS redirect

Forcing HTTPS is not just a best practice: it is the foundation of transport security. When creating or editing a proxy host, the SSL tab exposes three complementary options.

Force SSL: NPM automatically generates a return 301 https://$host$request_uri; block in the nginx vhost configuration. Any HTTP request is redirected server-side before even reaching your application.

HSTS (HTTP Strict Transport Security): by checking this option, NPM adds the Strict-Transport-Security: max-age=63072000; includeSubDomains; preload header to HTTPS responses. The browser remembers that this domain must always be contacted via HTTPS, even if the user types http://. Only enable HSTS if you are certain of maintaining SSL — disabling HSTS afterwards has no immediate effect on browsers that have already cached it.

HTTP/2 Support: enables the HTTP/2 protocol on the client side, without any modification on the application side. Multiplexing reduces perceived latency, especially on pages with many resources.

Advanced case: proxy for a Docker app without exposed port

One of the most underrated advantages of NPM is the ability to proxy containers that expose no public port. Communication takes place solely on the internal Docker network.

For a container to be reachable by NPM without public exposure, both services must share the same Docker network. Example with a Node.js app in /opt/myapp/docker-compose.yml:

services:
  web:
    image: my-image:latest
    restart: unless-stopped
    # No 'ports' section — the container is not accessible from the host
    networks:
      - proxy-net

networks:
  proxy-net:
    external: true

By declaring proxy-net as an external network and attaching the service to this network, the web container becomes reachable from NPM by its service name. In the NPM interface, Forward Hostname will simply be web and Forward Port the app's internal port (for example 3000).

This architecture means that even if an attacker compromises a container, they cannot reach other services directly from outside — everything goes through the proxy.

Access Lists: protecting the backoffice with authentication

NPM allows restricting access to certain proxy hosts via access lists. Go to Access Lists then Add Access List. Give the list a name, add entries under the Authorization tab (username + hashed password), and/or restrict by IP under Access.

Then edit the proxy host you want to protect and select this list in the Access List field. NPM automatically injects an auth_basic block into the nginx vhost configuration. This is particularly useful for exposing admin tools (Portainer, Grafana, internal API interfaces) without deploying a full authentication server.

For port 81 itself (the NPM interface), protection goes through the firewall — restrict access to this port to your fixed IP or a VPN.

Comparison: Nginx Proxy Manager vs Caddy vs Traefik

Scroll the table

CriterionNginx Proxy ManagerCaddyTraefik
ConfigurationGraphical web interface, no files to editDeclarative Caddyfile, concise syntaxYAML/TOML or Docker labels, steeper learning curve
Automatic SSLLet's Encrypt HTTP-01 and DNS-01, graphical interfaceBuilt-in natively, HTTP-01 and DNS-01 without pluginBuilt-in ACME, requires YAML configuration
Docker auto-discoveryNo, manual configuration per hostNot native, possible via labels with caddy-docker-proxyNative via Docker labels, detects services at startup
Ideal use caseDeveloper managing fewer than 20 apps, prefers UI over configSimple to medium stack, readable file-based configMicroservices, Kubernetes, dynamic environments
Advanced customizationLimited — custom nginx snippets possible but not recommendedHigh via modules and Caddyfile directivesVery high, chainable middlewares, rich plugins
Resources~50 MB RAM at rest~30 MB RAM at rest~40 MB RAM at rest, more depending on plugins

NPM's limits and when to migrate to Traefik

NPM covers the vast majority of use cases for developers managing a dozen applications on one or two VPS. It starts to show its limits in several scenarios.

Advanced nginx configuration: NPM generates its configuration files and regenerates them with each change from the interface. It is technically possible to add custom snippets, but they can be overwritten during an update. If you need fine-grained nginx configurations — per-route rate limiting, complex proxy cache, advanced rewrite logic — NPM becomes a friction layer rather than a help.

Dynamic environments: in a microservices architecture where containers appear and disappear frequently, manually configuring each host in NPM becomes a bottleneck. HAProxy or Traefik, which automatically detect services via Docker labels, are better suited to this context.

Kubernetes: NPM has no place in a Kubernetes cluster. Traefik has a native Ingress Controller; ingress-nginx is the other common option.

Practical rule: if your configuration fits in the NPM interface and does not require automation scripts to stay current, NPM is the right choice. As soon as you find yourself writing scripts to interact with the NPM API or managing configuration files outside the interface, that is the signal to evaluate Caddy or Traefik depending on your context.

A VPS ready for your Docker containers

ServOrbit.com offers cloud VPS starting at 99 DH/month, with high-availability networking, snapshots and technical support included. Deploy Nginx Proxy Manager in minutes.

Need help?

Browse our help center and FAQ, or reach our team — callback, WhatsApp or email. Support in French, English and Arabic.

Message us on WhatsAppopens in a new tab