Skip to content

TutorialsWeb servers

How to install Nginx Proxy Manager with Docker and secure its admin panel

Run Nginx Proxy Manager with Docker Compose, keep the admin port off the internet, and add HTTPS proxy hosts with Let’s Encrypt, access lists and backups.

  • Beginner
  • 30 min read
  • Updated

Tested on: Ubuntu 24.04 LTS, Ubuntu 26.04 LTS, Debian 12, Debian 13

This guide is not available in your language yet, so it is shown in English.

On this page
  1. Prerequisites
  2. Step 1 — Create the shared proxy network
  3. Step 2 — Create the Compose project
  4. Step 3 — Start NPM and create the admin account
  5. Step 4 — Allow web traffic in the firewall
  6. Step 5 — Connect an app to the proxy network
  7. Step 6 — Add a proxy host with HTTPS
  8. Step 7 — Restrict access with access lists
  9. Step 8 — Publish the admin panel safely (optional)
  10. Back up and restore
  11. Update Nginx Proxy Manager
  12. Troubleshooting
  13. 502 Bad Gateway
  14. The certificate request fails with Internal Error
  15. I cannot open the admin interface
  16. Login to the app stops working after adding an access list
  17. Address family not supported by protocol
  18. Bind for 0.0.0.0:80 failed: port is already allocated
  19. Next steps

Nginx Proxy Manager (NPM) puts a web interface on top of Nginx and Let's Encrypt. Instead of writing server blocks, you click together proxy hosts: a domain name, the app it forwards to, and a certificate that NPM requests and renews for you. It suits administrators who prefer a GUI and teams that share one server for many apps. This guide installs NPM with Docker Compose from its official image, keeps the admin port off the internet, creates the admin account through an SSH tunnel, connects apps over a shared Docker network, adds HTTPS and access lists, publishes the admin panel itself safely, and covers backups, updates and troubleshooting.

Prerequisites

  • A server running Ubuntu 24.04 LTS, Ubuntu 26.04 LTS, Debian 12 or Debian 13 on amd64 or arm64. Since version 2.14, the image is no longer built for 32-bit ARM (armv7).
  • A non-root user with sudo rights and SSH key login: Secure a new Linux server and Set up SSH keys.
  • Docker Engine with the Compose plugin: Install Docker on Ubuntu or Install Docker on Debian.
  • A domain with A (and, with working IPv6, AAAA) records for each hostname, for example app.example.com, pointing at the server.
  • Ports 80 and 443 free on the host.

The project does not publish minimum hardware requirements. The values below are a conservative starting point for NPM alone with its default SQLite database; add what your apps need.

ResourceMinimum (official)Suggested starting point
CPUNot published1 vCPU, shared with your apps
RAMNot published512 MB free for NPM
DiskNot published2 GB free for the image, certificates and logs

Step 1 — Create the shared proxy network

NPM reaches your apps over a Docker network that both sides join. Create it once:

Bash
docker network create proxy

Step 2 — Create the Compose project

Create the project directory:

Bash
sudo mkdir -p /opt/npm
sudo chown $USER:$USER /opt/npm
cd /opt/npm

Create /opt/npm/compose.yaml. It follows the official example, with two changes: the admin port 81 is published only on 127.0.0.1, and the container joins the proxy network.

YAML
services:
  app:
    image: jc21/nginx-proxy-manager:2.16.0
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
      - "127.0.0.1:81:81"
    environment:
      TZ: "Etc/UTC"
    volumes:
      - ./data:/data
      - ./letsencrypt:/etc/letsencrypt
    networks:
      - proxy

networks:
  proxy:
    name: proxy
    external: true

What the parts do:

  • Image tag — the official setup page pins a version; 2.16.0 is the current release at the time of writing. Check the project's GitHub releases page for the newest tag and use it.
  • Ports — 80 and 443 serve your sites and must be public. Port 81 is the admin interface; on 127.0.0.1 only the server itself can open it.
  • ./data — the SQLite database (database.sqlite), generated Nginx configs, custom certificates and logs.
  • ./letsencrypt — Let's Encrypt certificates and account keys.
  • TZ — set your time zone, for example Europe/Istanbul, so logs show local time.

NPM uses SQLite by default. It can also use MySQL/MariaDB or PostgreSQL through DB_MYSQL_* or DB_POSTGRES_* environment variables, which the setup page documents; SQLite is enough for a single server. If the server has no IPv6, add DISABLE_IPV6: "true" under environment.

Step 3 — Start NPM and create the admin account

Start the container and wait until it has finished its first start:

Bash
docker compose up -d
docker compose logs -f app

On the first run, NPM generates its keys and creates the database tables, which can take a minute or two. Stop following the logs with Ctrl+C once they settle down, and check the status:

Bash
docker compose ps

Because port 81 listens only on 127.0.0.1, open it through an SSH tunnel from your own computer:

Bash
ssh -L 8181:127.0.0.1:81 your-user@203.0.113.10

Keep that session open and browse to http://localhost:8181. Since version 2.13.0 there is no default login: NPM shows a setup screen where you create the first administrator. Use your real email address and a long, unique password, ideally from a password manager.

After logging in, turn on two-factor authentication for the admin account in your user settings; NPM supports TOTP apps since version 2.13.6.

Step 4 — Allow web traffic in the firewall

Allow SSH, HTTP and HTTPS in ufw (install it first on Debian with sudo apt install ufw):

Bash
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable

Do not rely on ufw for port 81. Docker's documentation states that published container ports bypass ufw, because Docker routes the traffic before ufw's rules apply. Binding the port to 127.0.0.1 in compose.yaml is what keeps it private. Check from another machine that it is closed:

Bash
nc -vz 203.0.113.10 81

The connection must be refused or time out.

Step 5 — Connect an app to the proxy network

NPM forwards requests to apps by name over the proxy network. In the app's compose.yaml, remove the ports: entry and add the network, keeping default so the app still reaches its own database:

YAML
services:
  app:
    # keep image, environment and volumes from the app's guide
    networks:
      - default
      - proxy

networks:
  proxy:
    name: proxy
    external: true

Run docker compose up -d in the app's folder, then find the container's name:

Bash
docker network inspect proxy | grep '"Name"'

Use the container name (for example n8n-n8n-1) as the forward hostname in NPM. Service names also resolve, but many projects call their service app or server, and on a shared network two services with the same name collide. The container name is unique on the server.

Step 6 — Add a proxy host with HTTPS

In the admin interface, open Hosts, then Proxy Hosts, and add a new proxy host:

  1. Domain names: app.example.com. The DNS record must already point at the server.
  2. Scheme: http. The app speaks plain HTTP inside the Docker network; NPM handles HTTPS.
  3. Forward hostname / IP: the container name from Step 5. Forward port: the port the app listens on inside its container, for example 5678.
  4. Turn on WebSocket support if the app uses live updates, and Block common exploits.
  5. On the SSL tab, request a new Let's Encrypt certificate and turn on Force SSL, which redirects HTTP to HTTPS. HTTP/2 support can be on as well.

Labels can differ slightly between versions. Since version 2.13.0, NPM no longer asks for an email address or terms agreement when requesting a certificate. Save the host and test it:

Bash
curl -I http://app.example.com
curl -I https://app.example.com

The HTTP request returns a redirect to HTTPS, and the HTTPS request returns your app's response. NPM renews the certificates on its own.

Step 7 — Restrict access with access lists

An access list limits who can reach a proxy host. Open Access Lists and create one:

  • on the authorization tab, add usernames and passwords for HTTP basic authentication,
  • on the access tab, allow specific IP addresses or ranges (for example your office IP) and deny everything else,
  • choose whether a visitor must satisfy any one of the rules or all of them.

Then open the proxy host, select the access list and save. Use access lists for tools without their own login. For apps that use HTTP basic authentication themselves, use only the IP rules: the project's FAQ explains that the access list and the app would both use the Authorization header, which breaks one of the two logins.

Step 8 — Publish the admin panel safely (optional)

The SSH tunnel is the safest way to manage NPM. If you want the admin panel at a normal HTTPS address instead, let NPM proxy to itself:

  1. Create an access list that allows only your own IP addresses and denies all others.
  2. Add a proxy host for npm.example.com with scheme http, forward hostname 127.0.0.1 and forward port 81. Inside the container, 127.0.0.1 is NPM itself, which is exactly the target here.
  3. Request a certificate, turn on Force SSL, and select the access list.

Keep port 81 bound to 127.0.0.1 in compose.yaml so that the tunnel still works if you lock yourself out. With two-factor authentication and the IP access list, the admin panel is protected by three layers.

Back up and restore

Everything NPM knows lives in /opt/npm: compose.yaml, data/ (database, configs, logs) and letsencrypt/ (certificates and keys). Stop the container briefly so the SQLite database is copied in a consistent state. The folders belong to root, so use sudo:

Bash
sudo mkdir -p /opt/backups
cd /opt/npm
docker compose stop
sudo tar czf /opt/backups/npm-$(date +%F).tar.gz -C /opt npm
docker compose start

To restore on a new server with Docker installed, recreate the network, unpack the archive and start NPM:

Bash
docker network create proxy
sudo tar xzf /opt/backups/npm-2026-10-09.tar.gz -C /opt
cd /opt/npm
docker compose up -d

Reconnect your app projects to the proxy network as in Step 5. Keep the archive private, because it contains private keys, and copy it off the server. If you switched to MySQL/MariaDB or PostgreSQL, also dump that database with its own tool.

Update Nginx Proxy Manager

Check the GitHub releases page for the newest version and read its notes; some releases list extra upgrade steps. Take a backup, change the image tag in compose.yaml, for example from 2.16.0 to the new version, then pull and recreate:

Bash
cd /opt/npm
docker compose pull
docker compose up -d
docker compose logs --tail 50 app

NPM updates its database and other requirements automatically on start. Open the admin interface afterwards and check that your proxy hosts and certificates are listed.

Troubleshooting

502 Bad Gateway

NPM cannot reach the app. Check that the forward hostname is the app's container name (not 127.0.0.1), that the app is attached to the proxy network (docker network inspect proxy), and that the forward port is the port inside the container, not a host port. Then look at docker compose logs app in the app's folder.

The certificate request fails with Internal Error

Let's Encrypt could not validate the domain. Check that the A and AAAA records point at this server (dig +short A app.example.com), that port 80 is reachable from the internet, and that no AAAA record points at an address the server does not answer on. Read docker compose logs app in /opt/npm for the detailed error. Repeated failures count against Let's Encrypt's limit of 5 failed validations per hostname per hour.

I cannot open the admin interface

Make sure the SSH tunnel is running and you browse to http://localhost:8181 on your own computer, not the server's address. Check that the container is up with docker compose ps, and give it a minute after the first start.

Login to the app stops working after adding an access list

The access list and the app both use the Authorization header. Remove the username and password rules from the access list and keep only IP rules, or rely on the app's own login.

Address family not supported by protocol

The server has no IPv6, but NPM tries to listen on it. Add DISABLE_IPV6: "true" under environment in compose.yaml and run docker compose up -d.

Bind for 0.0.0.0:80 failed: port is already allocated

Another web server, such as Nginx, Apache or Caddy, already uses port 80 or 443. Find it with sudo ss -tlpn 'sport = :80', stop and disable it, then run docker compose up -d again.

Next steps

Frequently asked questions

What are the default login details for Nginx Proxy Manager?

Current versions have none. Since version 2.13.0, the first visit to the admin interface opens a setup screen where you create the administrator account. Older guides mention a default email and password, which no longer apply.

Can I just block port 81 with ufw?

No. Docker publishes container ports in a way that bypasses ufw, so a rule for port 81 has no effect. Publish the admin port on 127.0.0.1 in the Compose file instead and reach it through an SSH tunnel or a protected proxy host.

Why does forwarding to 127.0.0.1 give a 502 error?

Inside the Nginx Proxy Manager container, 127.0.0.1 is the container itself, not the server. Put your apps on the same Docker network and forward to their container name and container port.

Can I write my own Nginx directives?

Yes. Each proxy host has an advanced section for custom directives, and the project supports optional include files such as http.conf or server_proxy.conf under /data/nginx/custom for settings that apply to all hosts.

Does Nginx Proxy Manager support WebSockets?

Yes. Turn on WebSocket support in the proxy host settings for apps that need it, such as chat, dashboards or workflow editors.

Створити пароль

Please confirm