Why a reverse proxy on your VPS
A VPS exposes a single public IP address. Behind that IP, you can run five, ten or twenty services on different internal ports. Without a reverse proxy, exposing each service on a public port means opening those ports in the firewall, manually managing TLS certificates for each one, and communicating each port to every user. A reverse proxy solves all three problems at once: it receives all traffic on ports 80 and 443, identifies the requested domain, and forwards the request to the correct internal container. Nginx Proxy Manager adds a graphical interface layer to this mechanism: no conf file to write, no manual reload, and Let's Encrypt certificates are managed in a few clicks. This is particularly suited to agencies hosting several client applications on the same server, or to independent developers running multiple projects in parallel.
NPM versus manual Nginx configuration: 7 concrete advantages
- No configuration file: each proxy host is created from the web interface, without any Nginx syntax to memorize.
- Automatic Let's Encrypt certificates: NPM requests, installs and renews certificates without intervention. Wildcard possible via DNS challenge.
- Centralized management: all your domains, subdomains and redirects in a single screen, instead of a dozen files in
/etc/nginx/sites-enabled/. - Built-in Access Lists: protect a service by IP or by password without touching each app's configuration.
- Per-host logs viewable in the interface: identify a 502 error or abuse without SSH-ing into the container.
- HTTP to HTTPS redirects and custom 404 pages configurable visually.
- Discovery by Docker container name: target
portainer:9000rather than an unstable internal IP that changes on restart.
Measured prerequisites before installing NPM
NPM only routes traffic, so it is lightweight. A VPS with 1 GB of RAM and 1 vCPU is sufficient if NPM is the only notable service. If you also host the applications behind it, size according to the sum of their needs. Plan for 15 GB of SSD disk (certificates, SQLite database, logs). Required software: Docker 24+ and the Compose v2 plugin (docker compose, not the old docker-compose command). Network: ports 80 and 443 must be free on the VPS — NPM is the sole occupant. Port 81 is reserved for the administration interface; it must never be exposed directly on the Internet. DNS: point your A records to the VPS IP before requesting certificates. For a wildcard *.your-domain.com, you need an API key from your DNS provider (Cloudflare, OVH, etc.).
Installation and initial configuration of Nginx Proxy Manager
Install Docker on the VPS
On a Debian/Ubuntu server: curl -fsSL https://get.docker.com | sh. Then verify: docker --version and docker compose version. If the Compose plugin is not present, install the docker-compose-plugin package via apt.
Create the working directory and Compose file
Create /opt/npm on the VPS. In that directory, create a docker-compose.yml file. Declare the NPM service with the image jc21/nginx-proxy-manager:latest, map the ports 80:80, 443:443 and 81:81, and define two volumes: ./data:/data and ./letsencrypt:/etc/letsencrypt.
Start the container
From /opt/npm, run docker compose up -d. Check that the container is running: docker compose ps. The administration interface is accessible at http://VPS_IP:81 within thirty seconds.
First login and admin account security
Log in with the default credentials: email [email protected], password changeme. NPM immediately prompts you to change both values. Do so before any other action: this account gives full access to your routing infrastructure.
Create a shared Docker network
For NPM to reach your application containers by name, they must share the same Docker network. Create an external network: docker network create proxy. In your NPM docker-compose.yml, declare this network as external. Do the same in the Compose file of each application to be proxied.
Add a first Proxy Host
In the interface: Proxy Hosts then Add Proxy Host. Enter the domain (e.g. app.your-domain.com), the Forward Hostname (container name or internal IP) and the application port. Check Block Common Exploits. If the application uses WebSockets, enable Websockets Support.
Enable Let's Encrypt SSL
In the SSL tab of the Proxy Host, choose Request a new SSL Certificate, accept the Let's Encrypt terms and check Force SSL. NPM contacts the ACME servers, validates the domain via HTTP-01 and installs the certificate. Your service is on HTTPS within seconds. Renewal is automatic.
Secure the administration interface (port 81)
Close port 81 in your firewall (ufw deny 81). Then create a dedicated Proxy Host — e.g. npm.your-domain.com pointing to localhost:81 — with SSL and an Access List restricted to your management IP. You can then access the interface via HTTPS without exposing port 81 to the rest of the world.
Wildcard certificate via DNS challenge
To cover all your subdomains with a single certificate, use the DNS challenge. In the SSL tab, select Use a DNS Challenge, choose your provider (Cloudflare, OVH, etc.) and enter the API key. NPM creates a TXT record in your DNS zone, validates domain ownership and obtains the *.your-domain.com certificate.
Test the complete chain
From a terminal: curl -I https://app.your-domain.com. Expect a 200 code or your application response code. Also check the HTTP redirect: curl -I http://app.your-domain.com should return a 301 to the HTTPS version.
Advanced configuration
NPM exposes in its interface several features that go beyond simple reverse proxying. Access Lists allow restricting access to a host by IP range, by HTTP Basic Auth credentials, or a combination of both — useful for protecting a backoffice without adding authentication in the application itself. Streams handle TCP/UDP proxying for non-HTTP protocols (remote MySQL, game servers, MQTT, etc.). The Redirections section creates permanent (301) or temporary (302) rules from a domain or path to another URL, without any service needed behind it. The Advanced tabs of each proxy host allow injecting raw Nginx configuration when the interface does not cover a particular case (custom security headers, extended proxy_read_timeout for long upload requests, etc.).
Hardening: rate limiting in the Advanced tab
NPM does not expose a rate limiting control in its base interface, but you can inject the Nginx directive in the Advanced tab of the proxy host concerned. Declare a shared zone in the custom configuration: limit_req_zone $binary_remote_addr zone=api:10m rate=30r/m; then apply it: limit_req zone=api burst=10 nodelay;. This limits each IP to 30 requests per minute with a burst of 10, which is enough to cut off scraping or brute-force attempts on a login form without blocking legitimate users.
Troubleshooting common errors
Here are the five most frequent problems encountered after an NPM installation, with their direct diagnosis.
Common errors and their causes
- 502 Bad Gateway: NPM cannot reach your container. First check that the application container is running (
docker ps) and that it is on the same Docker network as NPM (docker network inspect proxy). Then verify that the Forward Hostname exactly matches the service name declared in the application Compose file. - SSL certificate pending or ACME error: Let's Encrypt must be able to reach your server on port 80 via the declared domain. Check that the DNS A record points to the correct IP, that port 80 is not blocked by the VPS firewall or an upstream network filter, and that the domain resolves from outside (
dig app.your-domain.com). - Container unreachable via Docker DNS name: if you target
myapp:3000and get a resolution error, the myapp container is not on the same network as NPM. Add the proxy network in the application Compose file and recreate the containers (docker compose up -d --force-recreate). - Infinite HTTP to HTTPS redirect loop: this happens when the application itself performs its own HTTPS redirect on top of NPM. Add in the Advanced tab:
proxy_set_header X-Forwarded-Proto $scheme;so the application knows the actual protocol. - Administration interface inaccessible after restart: make sure the NPM service has the
restart: unless-stoppeddirective in your Compose file. Without it, Docker will not restart the container after a VPS reboot.
Nginx Proxy Manager versus Traefik
| Criterion | Nginx Proxy Manager | Traefik |
|---|---|---|
| Configuration | Visual web interface, no file to edit | YAML files and Docker labels |
| Learning curve | Low, accessible without Nginx experience | Steeper, DevOps-oriented |
| Let's Encrypt SSL | Automatic via the interface, wildcard possible | Automatic via configuration |
| Service discovery | Manual (container name or IP:port) | Automatic via Docker labels |
| Ideal for | A few manually managed services, clear setup | Dynamic environments with many containers |
| RAM consumption | Very low | Low |
| Logs per host | Viewable in the interface | Via external log stack |
| Access management | Built-in Access Lists (IP, password) | Middlewares to configure |
When to consider Traefik or Caddy
NPM is perfectly suited for a stable pool of services whose number remains manageable. As soon as you manage dynamic deployments — containers that appear and disappear automatically, as in Kubernetes environments or with CI/CD pipelines that create previews on the fly — Traefik becomes more appropriate: its service discovery via Docker labels avoids returning to the interface for each new container. Caddy is a good alternative if you want file-based configuration simplicity with automatic certificates, without a web interface. In all cases, NPM can coexist with Traefik or Caddy on the same VPS, each managing a portion of the traffic — provided no two proxies occupy the same ports 80 and 443. If your infrastructure grows to the point where you manage several dozen services across different teams, a service mesh (Consul Connect, Linkerd) advantageously replaces a stack of proxies.