Skip to content

How to Deploy Nginx Proxy Manager on a VPS

Updated Sep 2026

verified on Ubuntu 26.04 · Sep 2026
We earn commissions when you shop through the links below. Full disclosure →

Put Nginx Proxy Manager in front of your self-hosted apps — Let's Encrypt certificates from a web UI, apps on a private Docker network, and an admin panel that is not left open on port 81.

Before you start
  • A VPS with 1 GB RAM or more (Nginx Proxy Manager itself needs about 512 MB)
  • A fresh Ubuntu 24.04 or 26.04 server with root/sudo SSH access
  • A domain whose A records you can point at the server
  • Docker Engine + Compose installed (see the base guide below)
Need a box for this guide? Kamatera's free tier lets you spin one up now.Start free on Kamatera → (opens in new tab)

What Nginx Proxy Manager is

Nginx Proxy Manager (NPM) is nginx with a web interface on top. You add a "proxy host" in a form — a domain name, the address and port of the app behind it, a tick box for a Let's Encrypt certificate — and it writes the nginx config, requests the certificate and renews it. It also does redirects, raw TCP/UDP "streams", access lists with basic auth, and custom certificates. It is MIT-licensed, built on Node.js and nginx, and ships only as a Docker image.

Its stated goal is to make reverse proxying with TLS simple enough for anyone, and it delivers on that: nothing in this guide needs you to write an nginx config. The trade-off is that the configuration lives in a database behind a UI rather than in a file you can review and commit. If you want routes defined next to each app's compose file, look at Traefik; Nginx Proxy Manager vs Traefik and Caddy vs Nginx Proxy Manager compare the options.

Server sizing

On our test box (GCP e2-standard-2, Ubuntu 26.04, Docker 29.8.1, September 2026) an idle Nginx Proxy Manager container used about 80 MB of RAM and about 1.8 GB of disk for its image and data. Our catalog lists 512 MB as the working minimum.

As with any proxy, the apps behind it decide the server size, not NPM. A 1 GB VPS runs NPM plus a couple of light apps; size up for whatever you are actually hosting.

Prepare the server

This guide assumes Docker Engine and the Compose plugin are installed. If not, work through Docker & Compose on Ubuntu first.

sudo ufw allow OpenSSH
sudo ufw allow 80
sudo ufw allow 443
sudo ufw --force enable
sudo ufw status verbose

Note that port 81, the admin UI, is not opened here — and that on its own would not be enough. Ports published by Docker bypass ufw, because Docker manages its own iptables rules. So the compose file below binds port 81 to 127.0.0.1 only, which is what actually keeps it off the internet.

Where to host itaffiliate disclosure
Hetzner Cloudrun it on
2 vCPU · 4 GB RAM · 80 GB SSD · $23.59/mo
Get Hetzner Cloud (opens in new tab)
Kamaterafree trial
1 vCPU · 1 GB RAM · 20 GB SSD · $4.00/mo
Start free on Kamatera → (opens in new tab)
DigitalOceanalso works on
1 vCPU · 1 GB RAM · 25 GB SSD · $6.00/mo
Deploy on DigitalOcean → (opens in new tab)

Paid link — we earn a commission if you shop through it.

Install Nginx Proxy Manager (Docker Compose)

NPM's docs recommend a dedicated Docker network: NPM and every app it fronts join it, and the apps then need no published ports at all. Create it once:

mkdir -p ~/npm && cd ~/npm
docker network inspect proxy >/dev/null 2>&1 || docker network create proxy

NPM can create its first admin account from environment variables instead of a setup screen. Generate the password now, so the admin account never exists with a guessable one:

cd ~/npm
if [ ! -f .env ]; then
  echo "INITIAL_ADMIN_EMAIL=admin@example.com" > .env
  echo "INITIAL_ADMIN_PASSWORD=$(openssl rand -hex 16)" >> .env
  chmod 600 .env
fi

Use your real email in place of admin@example.com — it is the admin login name. Then write the compose file, based on the upstream setup page:

cd ~/npm
cat > docker-compose.yml <<'YAML'
services:
  app:
    image: jc21/nginx-proxy-manager:latest
    restart: unless-stopped
    ports:
      - "80:80"     # public HTTP
      - "443:443"   # public HTTPS
      - "127.0.0.1:81:81"   # admin UI, loopback only
    environment:
      TZ: "UTC"
      INITIAL_ADMIN_EMAIL: ${INITIAL_ADMIN_EMAIL}
      INITIAL_ADMIN_PASSWORD: ${INITIAL_ADMIN_PASSWORD}
    volumes:
      - ./data:/data
      - ./letsencrypt:/etc/letsencrypt
    healthcheck:
      test: ["CMD", "/usr/bin/check-health"]
      interval: 10s
      timeout: 3s

networks:
  default:
    external: true
    name: proxy
YAML
docker compose up -d

On first start NPM generates its keys, creates its SQLite database and the admin user; upstream notes this can take a minute or two. Wait for the healthcheck to go green:

cd ~/npm
for i in $(seq 1 30); do
  docker compose ps --format '{{.Status}}' | grep -q '(healthy)' && break
  sleep 5
done
docker compose ps

Two directories on the host now hold everything: ./data (the SQLite database, generated nginx configs, logs) and ./letsencrypt (certificates and their private keys).

Check it answers

From the server itself, confirm the admin API accepts the generated credentials, and that port 80 answers with NPM's default site:

cd ~/npm && . ./.env
curl -s -o /dev/null -w "admin UI: %{http_code}\n" http://127.0.0.1:81/
curl -s -X POST http://127.0.0.1:81/api/tokens -H "Content-Type: application/json" \
  -d "{\"identity\":\"$INITIAL_ADMIN_EMAIL\",\"secret\":\"$INITIAL_ADMIN_PASSWORD\"}" \
  | grep -q '"token"' && echo "admin login: OK"
curl -s -o /dev/null -w "public port 80: %{http_code}\n" http://127.0.0.1/

Reach the admin UI

Because port 81 only listens on the server's loopback, reach it through an SSH tunnel from your own computer:

ssh -L 8181:127.0.0.1:81 you@SERVER_IP

Then open http://localhost:8181 in your browser and sign in with the email and the password from ~/npm/.env. Change the password under your user menu once you are in, and delete the INITIAL_ADMIN_* lines from .env — they only matter on the very first start.

HTTPS + domain: your first proxy host

Point an A record for the app's hostname (say app.example.com) at the server's public IP and wait for it to resolve. Start the app on the proxy network without publishing its port, for example:

services:
  myapp:
    image: example/myapp:latest
    restart: unless-stopped

networks:
  default:
    external: true
    name: proxy

Then in the admin UI, Hosts → Proxy Hosts → Add Proxy Host:

  • Domain Names: app.example.com
  • Scheme / Forward Hostname / Forward Port: http, myapp (the compose service name), and the port the app listens on inside its container
  • Websockets Support: on for apps with live updates (chat, dashboards, code editors) — a page that loads and then never updates usually means this is off
  • SSL tab: "Request a new SSL Certificate", then enable Force SSL and HTTP/2 Support

Save, and NPM runs Certbot's HTTP challenge on port 80 and reloads nginx. The certificate renews automatically. For a wildcard certificate, choose the DNS challenge on the SSL tab: NPM installs the Certbot DNS plugin for your provider. Upstream warns those plugins vary in quality and that mixing several DNS providers in one instance can cause dependency conflicts, so stick to one.

Securing it

  • Keep port 81 on loopback. The admin UI controls every route and certificate on the box. The SSH tunnel is simpler and safer than exposing it, even behind its own proxy host.
  • Don't publish app ports. The proxy network is the point: an app with no ports: line can only be reached through NPM.
  • Access lists (Access Lists menu) add basic auth or IP allow/deny rules to a proxy host. One catch, from upstream's FAQ: an access list with a username/password breaks apps that use the Authorization header for their own login, because a request can only carry one. Use IP rules for those.
  • Change the admin password after first login, and add extra users with limited permissions rather than sharing the admin login.

Backups

The two directories are the whole application state. Stop the container for a consistent copy of the SQLite database:

cd ~/npm
docker compose stop
sudo tar czf ~/npm-backup-$(date +%F).tar.gz data letsencrypt docker-compose.yml .env
docker compose start
ls -lh ~/npm-backup-*.tar.gz

sudo is needed because files the container writes are owned by root. Copy the archive off the box. Restoring is unpacking it into ~/npm on a new server and running docker compose up -d; every proxy host and certificate comes back.

Upgrades

cd ~/npm
docker compose pull
docker compose up -d

Upstream's upgrade page says NPM migrates its own database on start. Back up first anyway, and skim the release notes for version-specific steps. If you prefer to stay on the 2.x line instead of latest, the image is also tagged jc21/nginx-proxy-manager:2.

Troubleshooting

502 Bad Gateway on a proxy host. NPM cannot reach the app. Confirm the app is on the proxy network (docker network inspect proxy), that the Forward Hostname is its compose service name, and that the port is the one the app listens on inside the container, not a host port.

The certificate request fails. Let's Encrypt must reach the domain on port 80: check the A record resolves to this server, that no cloud firewall blocks port 80, and read docker compose logs app for the Certbot error.

Locked out of a proxied app after adding an access list. That is the Authorization header clash above. Remove the username/password rule from the access list.

The admin UI does not load through the tunnel. Check the container is healthy (docker compose ps) and that you tunnelled to 127.0.0.1:81, not the public IP.

Verification + next steps

You're done when: the admin UI loads through the tunnel, a proxy host serves your app at https://app.example.com with a valid certificate, plain HTTP redirects to HTTPS, and ss -ltn on the server shows port 81 bound to 127.0.0.1 only.

From there, add a proxy host per app and keep every app on the proxy network. If you later want each route defined in code, next to the app it serves, the Traefik guide covers that approach. For hosting picks, see Best VPS for Docker.

Next steps

How to self-host Nginx Proxy Manager →More self-hosted reverse proxy tools →Automatic HTTPS with Caddy →Run Claude Code with Ollama on Your Own VPS →Deploy Coolify on a VPS →How to Deploy Actual Budget on a VPS →How to Deploy AnythingLLM on a VPS →How to Deploy Appwrite on a VPS →How to Deploy Audiobookshelf on a VPS →How to Deploy Authelia on a VPS →How to Deploy authentik on a VPS →How to Deploy Baserow on a VPS →How to Deploy Beszel on a VPS →How to Deploy Bitwarden on a VPS →How to Deploy BookStack on a VPS →How to Deploy CapRover on a VPS →How to Deploy Checkmate on a VPS →How to Deploy Directus on a VPS →How to Deploy docker-mailserver on a VPS →How to Deploy Docmost on a VPS →How to Deploy Dokku on a VPS →How to Deploy Dokploy on a VPS →How to Deploy Firefly III on a VPS →How to Deploy Forgejo on a VPS →How to Deploy Gatus on a VPS →How to Deploy Ghostfolio on a VPS →How to Deploy Gitea on a VPS →How to Deploy GitLab on a VPS →How to Deploy GlitchTip on a VPS →How to Deploy Grafana on a VPS →How to Deploy Graylog on a VPS →How to Deploy Headscale on a VPS →How to Deploy Healthchecks on a VPS →How to Deploy Home Assistant on a VPS →How to Deploy Immich on a VPS →How to Deploy Jan on a VPS →How to Deploy Jellyfin on a VPS →How to Deploy Karakeep on a VPS →How to Deploy Keycloak on a VPS →How to Deploy Leantime on a VPS →How to Deploy LibreChat on a VPS →How to Deploy Linkwarden on a VPS →How to Deploy LocalAI on a VPS →How to Deploy Mailcow on a VPS →How to Deploy Mailu on a VPS →How to Deploy Matomo on a VPS →How to Deploy Mattermost on a VPS →How to Deploy Meilisearch on a VPS →How to Deploy Memos on a VPS →How to Deploy n8n on a VPS →How to Deploy Navidrome on a VPS →How to Deploy NetBird on a VPS →How to Deploy Netdata on a VPS →How to Deploy Nextcloud on a VPS →How to Deploy Next.js to a VPS →How to Deploy NocoDB on a VPS →How to Deploy ntfy on a VPS →How to Deploy Ollama on a VPS →How to Deploy Open WebUI on a VPS →How to Deploy OpenHands on a VPS →How to Deploy OpenObserve on a VPS →How to Deploy OpenProject on a VPS →How to Deploy Outline on a VPS →How to Deploy Pangolin on a VPS →How to Deploy Paperless-ngx on a VPS →How to Deploy Passbolt on a VPS →How to Deploy Plane on a VPS →How to Deploy Plausible Analytics on a VPS →How to Deploy Pocket ID on a VPS →How to Deploy PocketBase on a VPS →How to Deploy Prometheus on a VPS →How to Deploy Psono on a VPS →How to Deploy Radarr on a VPS →How to Deploy Rocket.Chat on a VPS →How to Deploy SigNoz on a VPS →How to Deploy Sonarr on a VPS →How to Deploy Stalwart on a VPS →How to Deploy Stirling-PDF on a VPS →How to Deploy Supabase on a VPS →How to Deploy Synapse on a VPS →How to Deploy Taiga on a VPS →How to Deploy TeamPass on a VPS →How to Deploy Tinyauth on a VPS →How to Deploy Traefik on a VPS →How to Deploy Trilium on a VPS →How to Deploy Twenty CRM on a VPS →How to Deploy Umami on a VPS →How to Deploy Uptime Kuma on a VPS →How to Deploy Vaultwarden on a VPS →How to Deploy Vikunja on a VPS →How to Deploy wg-easy on a VPS →How to Deploy Wiki.js on a VPS →How to Deploy Zabbix on a VPS →How to Deploy Zitadel on a VPS →How to Deploy Zulip on a VPS →Docker & Compose on Ubuntu 26.04 →Building AI Workflows with n8n →Install Open WebUI with Ollama →Adding AI-Powered Insights to Plausible Analytics →Building AI-Powered Apps with Supabase and pgvector →

We use analytics cookies (Google Analytics, PostHog) to see which guides are useful. No ad networks, no cross-site tracking. See our privacy policy.