Nginx Proxy Manager routing encrypted web traffic to isolated Docker services through a central reverse proxy
← Back
nginx-proxy-manager

How to Set Up Nginx Proxy Manager with Docker

enim · Jun 23, 2026 · 6 min read · Updated: Aug 23, 2026
TL;DR: Run Nginx Proxy Manager 2.15.1 with ports 80 and 443 public, bind its admin port 81 to localhost, attach applications to a shared Docker proxy network, and back up both /data and /etc/letsencrypt.

An Nginx Proxy Manager setup gives every self-hosted application a domain and an automatically renewed HTTPS certificate without making you maintain Nginx server blocks by hand. The dangerous part is not the reverse proxy itself; it is exposing the administrator dashboard or publishing application ports that should remain private.

I run Nginx Proxy Manager (NPM) in front of ByteGuard's public services. On 23 August 2026, the production container was running NPM 2.15.1 on Docker 29.7.2, using 116.5 MiB of RAM, with zero container restarts. This guide uses that same version-pinned pattern and keeps port 81 off the public internet.

What you need before installing Nginx Proxy Manager

You need a Docker host, a domain, and control of ports 80 and 443 before starting.

  • An Ubuntu or Debian server with Docker Engine and the Compose plugin.
  • A domain with an A record pointing to the server. Add an AAAA record only if IPv6 is configured and firewalled correctly.
  • TCP ports 80 and 443 open to the internet.
  • SSH or a VPN for reaching the private NPM dashboard.
  • A hardened base system. Start with the Linux VPS hardening guide and the SSH hardening guide if this is a new server.

If you are still choosing a server, see the ByteGuard VPS comparison.

Create a secure Nginx Proxy Manager Docker Compose file

The secure baseline exposes only the public web ports globally and binds the admin dashboard to localhost.

Create the working directories:

sudo install -d -m 0750 /opt/npm/data /opt/npm/letsencrypt
cd /opt/npm

Create /opt/npm/compose.yaml:

services:
  npm:
    image: jc21/nginx-proxy-manager:2.15.1
    container_name: nginx-proxy-manager
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
      - "127.0.0.1:81:81"
    environment:
      TZ: "Africa/Casablanca"
    volumes:
      - ./data:/data
      - ./letsencrypt:/etc/letsencrypt
    networks:
      - proxy

networks:
  proxy:
    name: proxy

Pinning 2.15.1 makes upgrades deliberate and reproducible. The two bind mounts persist NPM's SQLite database, generated configuration, certificates, and account state. Change the timezone to your own IANA timezone.

The official example publishes port 81 on every interface. Binding it as 127.0.0.1:81:81 means an internet scan cannot reach the dashboard even if the host firewall is accidentally loosened.

Start NPM and inspect its health:

docker compose up -d
docker compose ps
docker compose logs --tail=100 npm

The first boot creates keys and initializes the database, so it can take a couple of minutes. Confirm that only ports 80 and 443 are public:

sudo ss -lntp | grep -E ':(80|81|443)\b'

Port 81 should show 127.0.0.1:81, not 0.0.0.0:81 or [::]:81.

FREE PDF Server Hardening Checklist

The checklist I use for SSH, firewall rules, Docker, monitoring, and backups on a new VPS.

Unsubscribe anytime. No third-party tracking.

Open the NPM dashboard without exposing port 81

An SSH tunnel lets your browser reach the localhost-only dashboard without opening another public port.

Run this on your own computer:

ssh -L 8181:127.0.0.1:81 <YOUR_USER>@<YOUR_SERVER_IP>

Keep that terminal open and visit http://127.0.0.1:8181. Complete the initial setup immediately, choose a unique password, and enable two-factor authentication from the account settings.

For regular administration, a private VPN such as WireGuard is more convenient than reopening an SSH tunnel each time. The WireGuard server guide covers that route.

Do not proxy the NPM dashboard through NPM itself unless it is protected by a VPN or a strong identity-aware access layer. A hidden subdomain is not access control.

Put an application on the shared proxy network

A shared Docker network lets NPM reach an application by container name while the application's port remains unpublished.

The proxy network already exists because Compose created it. In another application's Compose file, declare that network as external:

services:
  app:
    image: ghcr.io/example/app:1.2.3
    restart: unless-stopped
    expose:
      - "8080"
    networks:
      - proxy
      - app_private

networks:
  proxy:
    external: true
  app_private:
    internal: true

expose documents the container port but does not publish it on the host. NPM can reach app:8080 over the shared network; internet clients cannot bypass the proxy with SERVER_IP:8080.

Do not put every container on one flat network. Attach NPM only to each application's frontend network, while databases stay on separate private networks. This limits lateral movement if one public application is compromised. The Docker security guide explains the broader container controls.

Create your first HTTPS proxy host

A proxy host maps a public hostname to the private container name and port.

In NPM, open Hosts → Proxy Hosts → Add Proxy Host and enter:

  • Domain Names: app.example.com
  • Scheme: http
  • Forward Hostname/IP: app
  • Forward Port: 8080
  • Block Common Exploits: enabled
  • Websockets Support: enable only when the application needs it

On the SSL tab, request a new Let's Encrypt certificate, enable Force SSL, and agree to the certificate terms. Save the host, then verify both the response and certificate:

curl -I https://app.example.com
openssl s_client -connect app.example.com:443 \
  -servername app.example.com </dev/null 2>/dev/null \
  | openssl x509 -noout -issuer -subject -dates

The request should return an expected success or redirect status, and the certificate dates should be current.

Use Cloudflare without exposing the origin

Cloudflare can hide the VPS origin only when the host firewall accepts web traffic exclusively from Cloudflare's published address ranges.

Set the DNS record to proxied, use Full (strict) TLS mode, and install an origin certificate or retain a valid public certificate at NPM. Then allow Cloudflare's current IPv4 and IPv6 ranges to ports 80 and 443 before rejecting other sources.

This is an advanced change: fetching stale ranges or applying the drop rule before the allows can lock out legitimate traffic. Test the origin directly with a non-Cloudflare address after deployment; it should time out or be rejected while the hostname remains available through Cloudflare.

Back up Nginx Proxy Manager before upgrades

A recoverable NPM backup includes both persistent directories and is tested before the old version is removed.

Stop the container briefly for a consistent filesystem snapshot:

cd /opt/npm
docker compose stop npm
sudo tar --xattrs --acls -czf \
  "/var/backups/npm-$(date +%F-%H%M).tar.gz" \
  data letsencrypt compose.yaml
docker compose start npm

Copy the archive off the server and record the image tag. Before upgrading, read the upstream release notes, pull the chosen tag, and recreate the container:

docker compose pull npm
docker compose up -d
docker compose logs --tail=100 npm

Never assume that a successful archive command proves restoration works. Periodically extract a backup into a temporary directory, verify its files, and document the rollback command.

Nginx Proxy Manager troubleshooting

Why does NPM show a 502 Bad Gateway?

A 502 usually means NPM cannot reach the upstream container. Confirm both containers share the same Docker network, use the container's internal port rather than its host-published port, and test name resolution from the NPM container:

docker exec nginx-proxy-manager getent hosts app
docker exec nginx-proxy-manager curl -I http://app:8080

Why does Let's Encrypt certificate issuance fail?

HTTP validation fails when DNS points elsewhere, port 80 is blocked, or a CDN/proxy rule interferes with the challenge. Verify the public DNS answer and test port 80 from outside your network. For internal-only services, use a DNS challenge instead of exposing the service.

Why can NPM not reach a service on localhost?

Inside the NPM container, 127.0.0.1 refers to NPM itself. Put both containers on a shared network and forward to the application's container name, such as vaultwarden:80.

Why does the application redirect to HTTP or loop forever?

The upstream application may not trust the reverse proxy headers. Configure its external URL as HTTPS, set its trusted-proxy count correctly, and ensure NPM sends the original host and protocol headers.

Why does the dashboard work locally but not at my server IP?

That is intentional in this configuration because port 81 is bound to localhost. Use the SSH tunnel or VPN described above instead of changing it back to 81:81.

Nginx Proxy Manager FAQ

What ports does Nginx Proxy Manager use?

Nginx Proxy Manager listens on ports 80 and 443 for public HTTP and HTTPS traffic and port 81 for administration. Only 80 and 443 normally need internet exposure; keep port 81 on localhost, a trusted LAN, or a VPN.

Is Nginx Proxy Manager free?

Yes. Nginx Proxy Manager is open-source software with no paid tier. You pay only for the server, domain, and any external services you choose.

Should I use SQLite or MariaDB with NPM?

SQLite is appropriate for most single-server self-hosting installations and makes backup straightforward. An external database adds operational complexity and is useful only when your deployment has a concrete reason for it.

Is Nginx Proxy Manager better than Caddy or Traefik?

NPM is usually the easiest choice when you want a graphical interface. Caddy favors concise configuration and automatic HTTPS, while Traefik is stronger for dynamic label-driven infrastructure. See Nginx Proxy Manager vs Traefik vs Caddy and Pangolin vs Nginx Proxy Manager for measured comparisons.

Conclusion

This Nginx Proxy Manager setup exposes only ports 80 and 443, keeps its administrator interface private, and connects applications over explicit Docker networks. It also pins the image version and backs up the database and certificate state before upgrades.

The next step is to proxy one low-risk service, verify HTTPS and renewal, and then migrate the rest of the stack one application at a time. If you want the entire server hardened and configured, ByteGuard's VPS setup service covers the Docker host, reverse proxy, TLS, firewall, and backups.

Sources

Affiliate disclosure

This post links to a VPS comparison that may contain affiliate links. Recommendations are based on services I use for production workloads, and using them costs you nothing extra.

enim

enim

Security researcher, CTF player, and compulsive self-hoster. Building byte-guard.net from a $10/mo Hetzner VPS. Everything I publish I have actually run in production.

Comments

Sign in with GitHub to comment. Threads live in the byteguard-comments repo.