TL;DR:
- Run Nginx Proxy Manager 2.16.0 from the pinned
jc21/nginx-proxy-manager:2.16.0image in Docker Compose, with ports 80 and 443 public and the admin panel on127.0.0.1:81. - Version 2.13.0 removed the default login. Whoever completes the setup form on port 81 first becomes the administrator, so open it through an SSH tunnel.
- Docker-published ports bypass UFW, so the
127.0.0.1address in the port mapping is what keeps port 81 off the internet. - Let's Encrypt validates each hostname over port 80. NPM checks every hour and renews certificates that expire within 30 days.
- Apps in other Compose projects join a shared Docker network named
proxy, and NPM forwards to them by service name. - Back up the
dataandletsencryptfolders before every update, because releases can migrate the database.
Applies to: Ubuntu 24.04 LTS · Nginx Proxy Manager 2.16.0 · Docker Engine 29.8.2 · Docker Compose 5.6.0 · checked October 2026
Nginx Proxy Manager (NPM) is an open-source Docker application, released under the MIT license, that manages Nginx reverse proxy hosts, Let's Encrypt certificates and access lists from a web interface. This tutorial installs Nginx Proxy Manager 2.16.0 on Ubuntu 24.04 with Docker Compose, keeps its admin panel on the loopback address, and covers a proxy host with HTTPS, an access list, a shared Docker network for other apps, backups and updates. On a VPS, each self-hosted app gets an HTTPS address on your own domain.
How Nginx Proxy Manager works on a server
NPM runs as one container. Inside it, OpenResty 1.31.1.1 (an Nginx build) serves your sites, Certbot 5.8.0 requests the TLS certificates, often called SSL certificates, and a Node.js backend serves the admin panel and its API:
| Port | Serves | Reachable from |
|---|---|---|
| 80 | HTTP, redirects to HTTPS, Let's Encrypt validation | Internet |
| 443 | HTTPS for every proxy host | Internet |
| 81 | Admin panel and API | 127.0.0.1 only, through an SSH tunnel |
NPM keeps its state in two folders next to the Compose file: data holds the SQLite database, generated Nginx configuration and logs, and letsencrypt holds the certificates and private keys. For Nginx configured by hand with Certbot, see the Nginx, PostgreSQL, and Redis setup guide.
Prerequisites
- A VPS running Ubuntu 24.04 LTS or 26.04 LTS. NPM's documentation sets no minimum memory, and the 2.16.0 image is a 475 MB download on amd64, so size the server for the apps behind it. Arct Cloud Linux images include Ubuntu 26.04 LTS and accept an SSH public key at deploy.
- A non-root user with sudo privileges and SSH key login. See How to Generate an SSH Key on Windows, macOS, and Linux.
- Docker Engine and the Docker Compose plugin from Docker's apt repository, with your user in the
dockergroup. Follow How to Install Docker and Docker Compose on Ubuntu 24.04 and 26.04. Its Step 6 makes127.0.0.1the default publish address, so the Compose file below names0.0.0.0for ports 80 and 443. - A domain where you can create DNS records.
- Nothing else on ports 80 or 443. Stop and disable Caddy or Nginx if another tutorial installed it on the host.
Commands run as the sudo user on the server unless a step says "from your own computer". Replace values written in capitals, such as YOUR_SERVER_IP, with your own.
Step 1: Point DNS A records at the server
Let's Encrypt connects to each hostname over port 80 before it issues a certificate, so DNS must point at the server first. Create an A record for every hostname NPM will serve, such as app.example.com, with the server's IPv4 address, for example 203.0.113.10. Check it:
getent hosts app.example.com
203.0.113.10 app.example.com
Let's Encrypt connects over IPv6 first when a hostname has an AAAA record. If your server has a public IPv6 address, add AAAA records only after the IPv6 port mappings in Step 3, or validation can fail.
Step 2: Open ports 80 and 443 in UFW
Allow SSH before you enable the Uncomplicated Firewall (UFW), or the current session drops. Then allow HTTP and HTTPS:
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
sudo ufw status
Type y if UFW asks to confirm. The output looks similar to this:
Status: active
To Action From
-- ------ ----
OpenSSH ALLOW Anywhere
80/tcp ALLOW Anywhere
443/tcp ALLOW Anywhere
OpenSSH (v6) ALLOW Anywhere (v6)
80/tcp (v6) ALLOW Anywhere (v6)
443/tcp (v6) ALLOW Anywhere (v6)
Do not add a rule for port 81. Docker publishes container ports through its own iptables rules, which UFW does not filter, so the address in each port mapping decides who can connect. If your provider runs a network firewall in front of the server, open 80 and 443 there too.
Step 3: Write the Nginx Proxy Manager Compose file
Create the shared Docker network that NPM and your apps join:
docker network create proxy
Create the project directory and open a new Compose file:
mkdir ~/npm
cd ~/npm
nano compose.yaml
Paste this file, then save with Ctrl+O and exit with Ctrl+X:
services:
npm:
image: jc21/nginx-proxy-manager:2.16.0
restart: unless-stopped
ports:
- "0.0.0.0:80:80"
- "0.0.0.0:443:443"
- "127.0.0.1:81:81"
environment:
TZ: "Etc/UTC"
volumes:
- ./data:/data
- ./letsencrypt:/etc/letsencrypt
healthcheck:
test: ["CMD", "/usr/bin/check-health"]
interval: 10s
timeout: 3s
networks:
- proxy
networks:
proxy:
external: true
What the main settings do:
- The
2.16.0tag pins the release; thelatestand2tags move to each new version. 0.0.0.0publishes ports 80 and 443 on every IPv4 address, and127.0.0.1:81:81keeps the admin panel on loopback.- If your server has a public IPv6 address, add
"[::]:80:80"and"[::]:443:443"underports. An IPv4 mapping creates no IPv6 listener. Docker's userland proxy carries the IPv6 traffic to the container, so NPM logs and access-list IP rules see the Docker gateway address for IPv6 visitors. - The health check runs a script that ships in the image but is off by default.
- Set
TZto your time zone, such asEurope/Berlin, for log timestamps.
Step 4: Start Nginx Proxy Manager
Pull the image and start the container in the background:
docker compose up -d
The first start generates NPM's JWT keys and database, which NPM's setup guide says can take a couple of minutes depending on the machine. Check the status and the port mappings:
docker compose ps
docker port npm-npm-1
The STATUS column shows (healthy) once NPM is ready. The port list looks similar to this:
80/tcp -> 0.0.0.0:80
81/tcp -> 127.0.0.1:81
443/tcp -> 0.0.0.0:443
Ask the API whether an admin account exists:
curl -s http://127.0.0.1:81/api/
The response starts with {"status":"OK","setup":false. setup changes to true after Step 5.
Step 5: Open the admin panel and create the admin account
Port 81 answers only on the server's loopback address, so reach it through an SSH tunnel. From your own computer, run:
ssh -N -L 8081:127.0.0.1:81 deploy@YOUR_SERVER_IP
Replace deploy with your sudo user. -L forwards local port 8081 to port 81 on the server, -N opens no remote shell, and Ctrl+C closes the tunnel. The SSH tunnel tutorial covers Windows clients and a saved LocalForward entry.
Open http://localhost:8081 in your browser. NPM shows Welcome! and a form to create the admin account. Enter your Full Name, Email address and a New Password of at least 8 characters, then click Save. NPM creates the account and signs you in.
Until this form is saved, anyone who reaches port 81 can create the administrator, which is why the port stays on loopback. Turn on two-factor authentication under Two-Factor Auth in the user menu and store the backup codes.
Step 6: Connect an app on the shared Docker network
NPM forwards requests to containers by name over the proxy network, so the app publishes no host port. Inside the NPM container, 127.0.0.1 is the container's own loopback address. Start a test nginx web server in its own Compose project:
mkdir ~/hello
cd ~/hello
nano compose.yaml
Paste this file, then save and exit:
services:
hello:
image: nginx:1.30.5-alpine
restart: unless-stopped
networks:
- proxy
networks:
proxy:
external: true
Start it, then test the connection from inside the NPM container:
docker compose up -d
cd ~/npm
docker compose exec npm curl -sI http://hello | head -n 1
HTTP/1.1 200 OK
To put an existing app behind NPM, such as n8n from the n8n Docker Compose tutorial, add the network to the web service only and keep the project's default network for its database:
services:
n8n:
# existing settings stay as they are
networks:
- default
- proxy
networks:
proxy:
external: true
Run docker compose up -d in that project. In NPM, forward to the service name (n8n) and the container port (5678). Service names must be unique on the network; if two projects both have a service named app, use the container name from docker compose ps.
Step 7: Add a proxy host with a Let's Encrypt certificate
A proxy host maps one or more hostnames to an app. In the admin panel, open Hosts > Proxy Hosts and click Add Proxy Host. On the Details tab:
- Domain Names:
app.example.com - Scheme:
http - Forward Hostname / IP:
hello - Forward Port:
80 - Turn on Block Common Exploits. Turn on Websockets Support for apps that use WebSockets, such as n8n.
- Access List: leave Publicly Accessible for now.
On the SSL tab, set SSL Certificate to Request a new Certificate, then turn on Force SSL and HTTP/2 Support. Leave HSTS Enabled off until HTTPS works, because NPM's HSTS header tells browsers to refuse plain HTTP for two years. Click Save.
Certbot proves control of the hostname over port 80 and stores the certificate under letsencrypt/live. NPM checks its certificates every hour and renews any that expire within 30 days. A wildcard certificate such as *.example.com needs Use DNS Challenge and an API token from your DNS provider, which NPM keeps in its database.
Step 8: Restrict a host with an access list (optional)
An access list puts a password or an IP allowlist in front of a proxy host, for apps without their own login. Open Access Lists and click Add Access List:
- Details: a name, such as
admins. With Satisfy Any on, an IP rule or a password is enough; off, a request needs both. - Authorizations: a username and password for HTTP basic authentication.
- Rules: Allow your own address, such as
198.51.100.20or a range like198.51.100.0/24. When at least one rule exists, NPM adds a deny-all rule after it. With the Step 3 IPv6 mappings, IP rules match IPv4 visitors only.
Save the list, edit the proxy host, and choose it under Access List on the Details tab. A username and password list breaks apps that log in through the Authorization header, NPM's own panel included. Let's Encrypt validation paths stay open behind an access list, so renewals keep working.
Step 9: Verify HTTPS and the closed admin port
From your own computer, request the site over HTTP and HTTPS, then try the admin port. On Windows, type curl.exe instead of curl:
curl -sI http://app.example.com
curl -sI https://app.example.com
curl --max-time 5 http://YOUR_SERVER_IP:81
The first two responses start with these status lines:
HTTP/1.1 301 Moved Permanently
HTTP/2 200
The 301 is Force SSL redirecting to HTTPS, and HTTP/2 401 means an access list asks for a password. The third request fails or times out after 5 seconds; a page means port 81 is public. The Certificates page lists the new certificate and its expiry date.
Update Nginx Proxy Manager
Read the notes for every release between your version and the new one on the Nginx Proxy Manager releases page. NPM migrates its database when the new version starts, and the 2.13.3 notes warn that going back to an earlier version after such a migration is hard. Back up first (next section), then change the tag, recreate the container and remove the old image:
cd ~/npm
sed -i 's|jc21/nginx-proxy-manager:.*|jc21/nginx-proxy-manager:YOUR_NEW_VERSION|' compose.yaml
docker compose pull
docker compose up -d
docker image rm jc21/nginx-proxy-manager:YOUR_OLD_VERSION
Replace YOUR_NEW_VERSION with the release number from the releases page and YOUR_OLD_VERSION with the tag you are replacing, such as 2.16.0.
Back up Nginx Proxy Manager
The data and letsencrypt folders are owned by root, because NPM runs as root inside the container. Stop NPM so the SQLite database is not written during the copy, archive the folders with the Compose file, and start it again:
cd ~/npm
docker compose stop
sudo tar -czf - data letsencrypt compose.yaml > ~/npm-backup-$(date +%F).tar.gz
docker compose start
chmod 600 ~/npm-backup-*.tar.gz
Proxy hosts stop answering while NPM is stopped. The archive contains private keys, so chmod 600 limits it to your user. Copy it off the server with rsync or scp. To restore on the same server:
cd ~/npm
docker compose down
sudo rm -rf data letsencrypt
sudo tar -xzf ~/npm-backup-YOUR_DATE.tar.gz
docker compose up -d
Replace YOUR_DATE with the date in the file name. On a new server, run docker network create proxy, extract the archive in a new ~/npm folder, and run docker compose up -d. For server-level copies, see VPS backups.
Troubleshoot common Nginx Proxy Manager errors
| Symptom | Cause | Fix |
|---|---|---|
failed to bind host port 0.0.0.0:80/tcp: address already in use, or Bind for 0.0.0.0:80 failed: port is already allocated | Caddy, Nginx or another container holds port 80 or 443 | sudo ss -tlnp to find it, then stop it, for example sudo systemctl disable --now caddy |
network proxy declared as external, but could not be found | The shared network does not exist yet | docker network create proxy, then docker compose up -d |
502 Bad Gateway on a proxy host | The forward host is 127.0.0.1, or the app is stopped or off the proxy network | Use the service name, then test with docker compose exec npm curl -sI http://SERVICE_NAME |
Internal Error when requesting a certificate | The A record points elsewhere, port 80 is blocked upstream, or an AAAA record has no IPv6 listener | getent hosts YOUR_DOMAIN, then sudo tail -n 50 ~/npm/data/logs/letsencrypt.log. Let's Encrypt allows 5 failed attempts per hostname per hour |
Address family not supported by protocol in docker compose logs npm | IPv6 is turned off on the host | Add DISABLE_IPV6: "true" under environment, then docker compose up -d |
Use Caddy instead for a single app
Caddy is a web server that obtains and renews TLS certificates on its own and reads its configuration from a text file, the Caddyfile. On a single-app server, it replaces NPM, its admin panel and its database. Install Caddy from Caddy's apt repository with the commands in the n8n tutorial's Caddy option, publish the app on 127.0.0.1, and put this in /etc/caddy/Caddyfile:
app.example.com {
reverse_proxy 127.0.0.1:8080
}
Replace 8080 with the app's port and run sudo systemctl reload caddy. Caddy redirects HTTP to HTTPS and needs the same DNS records. Run Caddy or NPM, not both, because each needs ports 80 and 443.
FAQ
What is the Nginx Proxy Manager default login?
Since version 2.13.0 there is none: the first visit to port 81 shows a form that creates the admin account. Older versions used [email protected] with the password changeme. INITIAL_ADMIN_EMAIL and INITIAL_ADMIN_PASSWORD skip the form, but NPM prints that password in its log.
What port does Nginx Proxy Manager use?
Nginx Proxy Manager serves its admin panel on port 81 and your proxy hosts on ports 80 and 443. The NPM_ADMIN_PORT variable moves the admin panel; keep it on 127.0.0.1 and use an SSH tunnel.
Does Nginx Proxy Manager need port 443?
Yes, for HTTPS. Browsers reach your proxy hosts on port 443, and Let's Encrypt checks each hostname over port 80, so open both. With only port 80 open, NPM still gets a certificate, but visitors cannot load the HTTPS site.
How do I install Nginx Proxy Manager on Ubuntu?
Install Docker Engine and the Compose plugin, write a compose.yaml that runs jc21/nginx-proxy-manager with a pinned tag and ports 80, 443 and 127.0.0.1:81, and start it with docker compose up -d. NPM ships only as a Docker image.
How do I reset my Nginx Proxy Manager password?
Another administrator can set a new password on the Users page with Set Password. NPM documents no reset command for a single locked-out admin, so keep the password in a password manager.
How do I add an SSL certificate in Nginx Proxy Manager?
Open the proxy host, go to the SSL tab, choose Request a new Certificate and save. The A record must point to the server and port 80 must be reachable. Add a certificate you already have under Certificates as a custom certificate.
Nginx Proxy Manager vs Caddy: which should I use?
Use Nginx Proxy Manager when several apps need hostnames, certificates and access lists managed from a web interface. Use Caddy for a single app configured in a text file, with no admin panel to protect. Both request and renew Let's Encrypt certificates on their own.
Next steps
- How to Self-Host LiteLLM Proxy with Docker Compose on Ubuntu 24.04, which publishes LiteLLM through NPM on the
proxynetwork - Advanced configuration and Upgrading in the Nginx Proxy Manager docs
Arct Cloud Linux images deploy in under 30 seconds with root SSH on every plan. Compare plans.
This work is licensed under CC BY-NC-SA 4.0.