The three ports and what each one is for
NPM's setup documentation publishes three ports in its Compose file. Two carry your sites; one carries the tool that controls them, and it should never be reachable from outside.
| Port | Role in NPM | Binding on this server |
|---|---|---|
| 80/tcp | Public HTTP; also answers Let's Encrypt HTTP validation | All interfaces |
| 443/tcp | Public HTTPS for every proxy host | All interfaces |
| 81/tcp | Admin web UI (movable with NPM_ADMIN_PORT) | 127.0.0.1 only |
| Stream ports | Optional TCP/UDP forwarding, one Compose entry per port | Only if you need them |
The project's FAQ is blunt about packaging: Docker is required, because that is how NPM ships. Inside the container NPM runs OpenResty, the Nginx build it pins, and Certbot for certificates. Its docs publish no hardware minimum, so size the machine for the apps behind the proxy, not for the proxy.
Which machine to put it on
A reverse proxy is the front door for everything else on the box, so the useful question is what runs behind it. One app on a small machine is a cartridge job; a stack of self-hosted services with their own databases wants a single- or dual-socket server. Our dedicated server range lists each line with its processor, memory ceiling, port and cities; the single-socket class suits a web or application server with its database on the same machine, the dual-socket enterprise servers a dozen services side by side.
- Whole machine, root access. No hypervisor and no neighbour; Docker's networking and the host firewall are yours to set.
- Unmetered port. All traffic to every app behind the proxy crosses this one port, and nothing counts it.
- One IPv4 included, IPv6 free at checkout as individual addresses, which is all a proxy listening on 80 and 443 needs.
- Out-of-band console on every racked single-, dual- and quad-socket server (iLO, iDRAC or a Supermicro BMC), so a firewall mistake costs a console session, not a reinstall. Moonshot cartridges other than the m510 and m710x have power control only.
- Delivery: a racked machine is handed over in about 30 minutes once payment is approved.
Firewall first, and what ufw cannot see
Get Docker Engine 28 or later onto the machine first, with the Compose plugin, using the install guide Docker publishes (NPM's README points to it). Engines before 28 do not stop a neighbour on the same layer-2 segment from routing to a port published on 127.0.0.1, and the admin panel's privacy rests on that binding. Turn ufw on before Docker publishes anything. Set IPV6=yes first: ufw writes IPv6 copies only of rules added while that flag is on. Then open SSH plus the two web ports, and enable with --force so no prompt swallows the next pasted line.
sudo sed -i 's/^IPV6=.*/IPV6=yes/' /etc/default/ufw
sudo ufw default deny incoming
sudo ufw allow 22/tcp
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw --force enable
sudo docker network create proxy
sudo mkdir -p /opt/npm
Docker inserts its own iptables rules for every port under ports:, and those rules are evaluated before ufw's. A published port is reachable even when ufw has no allow rule for it. By default a mapping with no address is published on every host address, IPv4 (0.0.0.0) and IPv6 ([::]) alike, so do not count on ufw for any container port: bind private ports to 127.0.0.1 in the Compose file, where Docker listens only on that address.
The Compose file
This is the documented Compose file with two changes: the admin port is bound to loopback, and the stack joins the external proxy network created above. The image tag is pinned to the release the docs name, 2.16.0, so an update happens when you choose it. Save it as /opt/npm/docker-compose.yml, then start it from /opt/npm with sudo docker compose up -d.
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:
default:
external: true
name: proxy
With no database variables set, NPM keeps its configuration in SQLite at /data/database.sqlite. For MySQL or MariaDB, set the DB_MYSQL_* variables and run the database server yourself, usually as a second service in the same file; the docs also show Postgres through DB_POSTGRES_*. If both MySQL and SQLite variables are present, the MySQL ones win. The docs' sample database passwords are placeholders: replace them, or read them from files with the image's __FILE suffix, and keep the Compose file at mode 600.
First login through an SSH tunnel
Releases up to 2.12 created a default user, admin@example.com with password changeme, and forced a change at first login. Version 2.13.0 replaced it with a setup wizard: no default user exists, and the first person to open the admin UI creates the administrator. That is why port 81 must not be public even for a minute.
- From your workstation, open a tunnel with your own login:
ssh -L 8081:127.0.0.1:81 you@your-server. - Browse to
http://localhost:8081. The first start generates JWT keys and the database tables; the docs say this may take a couple of minutes. - The wizard asks for the admin account. Use a real address and a long password, then turn on two-factor authentication in the user's settings.
- Close the tunnel when you are done. Every later visit to the UI goes the same way.
The INITIAL_ADMIN_EMAIL and INITIAL_ADMIN_PASSWORD variables skip the wizard. They put the password in the Compose file and in docker inspect output, so prefer the wizard, reached through the tunnel, over these variables.
Certificates: HTTP or DNS validation
| HTTP challenge | DNS challenge | |
|---|---|---|
| Needs | A proxy host for the name, reachable on port 80 | A Certbot DNS plugin and your DNS provider's API credentials |
| Wildcards | Not supported | Supported |
| Renewal | Port 80 must keep answering for the name | Works with no proxy host and no HTTP access |
| Caveat | DNS must already point at the server | Plugins are third-party; two providers in one instance can conflict |
Use HTTP validation for ordinary names that resolve to this machine; NPM serves /.well-known/acme-challenge/ from its own webroot with access rules switched off for that path. Use DNS validation for *.example.com or for names that must not answer publicly. NPM's Certbot notes warn that DNS plugins lag behind Certbot at times; check the container log if a DNS request fails after an update.
Proxy hosts, upstream apps and access lists
The docs' best practice for apps on the same Docker host is a shared network: put each app's Compose file on proxy, remove its ports: section, and enter the app's service name and container port as the forward host in NPM. The app is then reachable only through the proxy. That is the pattern for a Nextcloud server, a Vaultwarden instance or Uptime Kuma on the same box; give each its own name and certificate.
Access lists combine allowed or denied client addresses with HTTP basic authentication, and 2.16.0 can apply them per path. Do not put a basic-auth list in front of an app that has its own login: the FAQ explains that both use the Authorization header, so one of the two logins breaks. Use an address list there instead.
Backups and updates
Everything NPM knows lives in two folders: data (database, generated Nginx config, JWT keys) and letsencrypt (certificates and account keys). Stop the stack for a consistent SQLite copy; the proxy is down for the seconds the archive takes, so schedule it at a quiet hour. Create the target with sudo install -d -m 700 /root/npm-backups and save this as /opt/npm/backup.sh:
cd /opt/npm || exit 1
docker compose stop
tar czf /root/npm-backups/npm-$(date +%F).tar.gz data letsencrypt
docker compose start
find /root/npm-backups -name 'npm-*.tar.gz' -mtime +14 -delete
# root crontab line (crontab -e):
# 15 4 * * * bash /opt/npm/backup.sh > /root/npm-backups/run-$(date +\%F).log 2>&1
The archives hold private keys, hence the 700 directory. Copy them off the machine yourself; a backup on the same disk does not survive the disk. With MariaDB, add its data folder to the tar line.
To update, change the pinned tag, pull the new image (docker compose pull) and recreate the container (docker compose up -d); NPM migrates its own database on start. Read the release notes first: 2.13.3 asked users to back up the database and /data before upgrading, and 2.15.0 and 2.16.0 updated Certbot with a warning that DNS plugins may need attention.
Limits worth knowing
- No package outside Docker. If you want Nginx managed by config files and your own tooling, NPM is the wrong layer.
- It terminates TLS for the apps; it does not authenticate users beyond basic auth and address lists. Put a real identity provider such as authentik in front of apps that need single sign-on.
- Custom Nginx snippets go in
/data/nginx/custom/, but heavy custom config is easier to own in plain Nginx. - The code is MIT-licensed, so running it commercially costs nothing and carries no licence key.
Questions
What is the default login for Nginx Proxy Manager?
Since 2.13.0 there is none: the first visit to the admin UI opens a setup wizard that creates the administrator. Releases up to 2.12 created admin@example.com with the password changeme. Because the first visitor becomes admin, open the UI only through an SSH tunnel to a port bound to 127.0.0.1.
Which port does the admin UI use, and can I change it?
Port 81 by default. The NPM_ADMIN_PORT variable moves it, and the Compose ports entry must match. Whatever the number, publish it on 127.0.0.1 only.
Should I use SQLite or MariaDB?
SQLite is the default and needs nothing else. MySQL, MariaDB and Postgres are supported through environment variables if you already run one or want the configuration in a database server you back up separately; you provide that server yourself.
Can it issue a wildcard certificate?
Yes, with the DNS challenge and a Certbot plugin for your DNS provider. The HTTP challenge cannot validate wildcards and needs port 80 reachable for the name, both at issue and at every renewal.
Can I publish the admin UI through NPM itself?
It is possible, but this page advises against it: it turns a management interface into a public login page. The SSH tunnel costs one command and keeps the UI off the internet entirely.
Is Nginx Proxy Manager free for business use?
Yes. NPM's licence is MIT, and MIT permits commercial use; you pay for the hardware, not the software.