# 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.

Difficulty: Beginner\
Tested on: Ubuntu 24.04 LTS, Ubuntu 26.04 LTS, Debian 12, Debian 13

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.

> **Note**
>
> NPM needs ports 80 and 443 for itself. Run only one reverse proxy per server: NPM, [Caddy](/guides/caddy-reverse-proxy), [Nginx with Certbot](/guides/nginx-reverse-proxy-certbot) or [Traefik](/guides/traefik-reverse-proxy).

## 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](/guides/secure-a-new-linux-server) and [Set up SSH keys](/guides/ssh-keys).
- Docker Engine with the Compose plugin: [Install Docker on Ubuntu](/guides/install-docker-ubuntu) or [Install Docker on Debian](/guides/install-docker-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.

| Resource | Minimum (official) | Suggested starting point |
|---|---|---|
| CPU | Not published | 1 vCPU, shared with your apps |
| RAM | Not published | 512 MB free for NPM |
| Disk | Not published | 2 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.

> **Warning**
>
> Whoever opens the admin interface first can create the administrator account. That is why port 81 must never be reachable from the internet, not even briefly during installation. Finish the setup immediately after the first start.

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.

> **Note**
>
> Do not forward to `127.0.0.1` or `localhost`. Inside the NPM container these addresses point at NPM itself, so the result is a 502 error.

## 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.

> **Tip**
>
> Leave **HSTS** off until the site has worked over HTTPS for a while. Once browsers have seen the header, they refuse plain HTTP for that hostname until it expires, even if the certificate breaks.

## 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

- Learn the Compose features used here in [Docker Compose basics](/guides/docker-compose-basics).
- Prefer configuration as code? Compare with [Caddy](/guides/caddy-reverse-proxy) or [Traefik](/guides/traefik-reverse-proxy).
- Strengthen SSH access, which also protects your admin tunnel, with [Set up SSH keys](/guides/ssh-keys).
- Find a server for your apps on the [Docker hosting](/docker-hosting) page.
- Read the official [advanced configuration notes](https://nginxproxymanager.com/advanced-config/) for custom Nginx snippets and environment options.

## 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.

---

Source: <https://hyperdc.com/guides/tutorials/nginx-proxy-manager>\
Updated: 2026-10-09
