Workload · Running a reverse proxy

Nginx Proxy Manager: one front door with HTTPS for all your apps

Nginx Proxy Manager gives all the web apps on your server one shared front door: visitors arrive on port 80 or 443, NPM sends each domain name to the right app, and it fetches and renews free Let's Encrypt certificates for you, all from a point-and-click panel. You run it with Docker Compose. The one thing to get right is the panel itself on port 81: keep it off the internet and open it through an SSH tunnel.

The short version
  1. One HTTPS entrance for Nextcloud, Immich and the rest
  2. Certificates issued and renewed for you
  3. Admin panel on port 81, kept private
  4. No default password in current versions
  5. Free and open source under the MIT licence

What you get out of it

A reverse proxy is a doorman. Every visitor knocks on the same two ports, and the proxy reads which name they asked for, say photos.example.com, and walks them to the matching app. Nginx Proxy Manager, NPM for short, wraps the Nginx web server in a friendly panel so you never edit its config by hand.

  • Proxy hosts: a domain name pointed at an app, with HTTPS added in front even if the app has none.
  • Free certificates: Let's Encrypt, requested from the panel and renewed automatically.
  • Redirections and 404 hosts: send old names somewhere useful, or answer unknown names with a blank page.
  • Streams: plain TCP or UDP forwarding, for things like a game server or SSH that are not websites.
  • Access lists, users and an audit log: decide who may reach what, and see who changed what.

It comes only as a Docker image; the project's FAQ says that is simply how it is packaged. The docs give no hardware minimum, so pick the server for the apps you plan to put behind it.

Why a whole server makes this easier

A proxy only helps if it can sit on the two web ports, 80 and 443, and decide what happens there, which needs a machine you fully control. A dedicated server is that: one physical computer that runs your work and nobody else's, with root access.

  • The port is unmetered. Every photo, file and dashboard your apps send leaves through the proxy, and there is no transfer allowance to run over.
  • You can get back in after a mistake. An out-of-band console, the screen and power button reachable even when a firewall rule has locked SSH out, comes with rack servers and blades, and with two cartridge models, the m510 and m710x.
  • Start again whenever you like. Reinstall the operating system from the control panel, as many times as you need, with no ticket.
  • Addresses included. One IPv4 address comes with the server, and IPv6 is free.
  • No contract. It is month to month, and a ready server is handed over within 30 minutes of the payment being approved.

Be clear about the split. You manage the server; we keep the hardware and the network running. For a handful of apps a single-processor machine is plenty; if you plan to run a dozen services with their databases, look at the enterprise servers with two processors.

Up and running in an evening

  1. Point a DNS record for each app's name, for example cloud.example.com, at your server's address.
  2. Install Docker Engine 28 or later with its Compose plugin, following Docker's own instructions for your Linux release (older Engines let other machines on your local network segment get at ports published on 127.0.0.1).
  3. Switch on the firewall and create a private network that NPM and your apps will share (first block below).
  4. Create a folder such as /srv/proxy, save the Compose file below in it as docker-compose.yml, and bring it up from that folder: sudo docker compose up -d.
sudo sed -i '/^IPV6=/c\IPV6=yes' /etc/default/ufw
sudo ufw allow OpenSSH
sudo ufw allow 80,443/tcp
sudo ufw --force enable
sudo docker network create apps

Read this before you rely on the firewall: Docker adds its own rules for every port listed under ports:, and they take effect ahead of ufw, so ufw cannot close a port Docker has published. That is why the panel's port is tied to 127.0.0.1 below: Docker then listens on the server's loopback address and nowhere else. The IPv6 line comes before the allow rules because ufw only creates IPv6 versions of rules added after it is set.

services:
  npm:
    image: 'jc21/nginx-proxy-manager:2.16.0'
    restart: unless-stopped
    environment:
      TZ: "America/New_York"
    ports:
      - '127.0.0.1:81:81'   # admin panel, loopback only
      - '443:443'
      - '80:80'
    volumes:
      - ./letsencrypt:/etc/letsencrypt
      - ./data:/data

networks:
  default:
    name: apps
    external: true

That file keeps its settings in a small SQLite database inside ./data, which suits most people. If you would rather use MySQL or MariaDB, or Postgres, the setup guide shows the extra environment variables; you run that database yourself, and any password you put in the file should be your own, with the file readable by root only (chmod 600).

Your first login, done privately

Older guides tell you to sign in as admin@example.com using changeme as the password. That was true up to version 2.12. Since 2.13.0 there is no default account: the panel greets the first visitor with a welcome screen and asks them to create the admin. Whoever reaches port 81 first owns your proxy, which is the reason it stays private.

  1. On your own computer, run ssh -N -L 9081:127.0.0.1:81 you@your-server, signing in with your normal account on the server.
  2. Open http://localhost:9081 in your browser. On the very first start NPM generates its keys and sets up its database, which can take a couple of minutes.
  3. Create your admin account with a password you do not use anywhere else, and switch on two-factor authentication in your profile.
  4. Close the SSH window when you finish. Come back the same way next time.

Getting the padlock on every name

NPM offers two ways to prove to Let's Encrypt that a name is yours.

  • Over HTTP, the usual choice. Let's Encrypt fetches a file from your server on port 80, so the name must already point here and must stay reachable over plain HTTP for renewals to work. It cannot issue wildcard certificates.
  • Over DNS, for wildcards like *.example.com or names you do not want answering publicly. NPM installs the Certbot add-on that matches your DNS host and uses it to place a temporary record; you supply the provider's API credentials. The project warns that these plugins are maintained separately and that mixing two providers in one installation can cause conflicts.

Putting your apps behind it

Add each app to the same apps network in its own Compose file and delete its ports: lines. In NPM, create a proxy host with the app's service name as the forward hostname and the port it uses inside its container. The app is now reachable only through the proxy, with HTTPS on the front. The same steps work for an Immich photo library, a Nextcloud server or an Uptime Kuma status page.

For an internal tool with no login of its own, an access list can require a username and password or allow only certain IP addresses. Skip the password option on apps that already have a sign-in: the FAQ explains both would use the same browser header, and one of the two logins stops working.

Keeping a copy and staying current

Two folders hold everything: data, with the database and generated settings, and letsencrypt, with your certificates and their private keys. The script below pauses NPM for the few seconds it takes to pack them, so the database copy is clean. Make the folder first with sudo mkdir -m 700 -p /var/backups/proxy, because those keys are sensitive, and run the script from root's crontab at a quiet hour. Then copy the archives to another machine yourself.

# /srv/proxy/nightly-copy.sh
cd /srv/proxy || exit 1
docker compose stop npm
tar -czf /var/backups/proxy/proxy-$(date +%Y%m%d).tgz data letsencrypt
docker compose start npm

# sudo crontab -e, then add:
# 30 3 * * * bash /srv/proxy/nightly-copy.sh >> /var/backups/proxy/log-$(date +\%Y\%m).txt 2>&1

Updating takes two commands in /srv/proxy once you have changed the version number in the file: docker compose pull fetches the new image and docker compose up -d restarts NPM on it. NPM upgrades its own database when it starts. Glance at the release notes before each jump; some releases ask you to back up first, and recent ones changed Certbot in ways that can affect DNS plugins.

When something else fits better

If you already manage Nginx through config files or automation, a panel adds a layer you will fight. If your apps need single sign-on rather than a password prompt, pair NPM with an identity provider such as authentik. And if you do not want Docker on the machine, NPM is not an option, because Docker is the only way it ships.

Questions people ask

Is there a default username and password?
Not in current releases. From 2.13.0 onward, the first visit to the panel shows a welcome screen where you create the administrator yourself. Versions up to 2.12 used admin@example.com and changeme until you changed them. Either way, reach the panel only through an SSH tunnel.
Which ports does it need open to the internet?
Only 80 and 443. Port 81 is the admin panel and should be published on 127.0.0.1 only. Add extra ports only for streams you create, one line per port in the Compose file.
Do I need a separate database?
No. Out of the box it stores everything in SQLite inside the data folder. MySQL, MariaDB and Postgres are optional, and you would run that database yourself.
Will my certificates renew on their own?
Yes, NPM renews Let's Encrypt certificates for you. With the HTTP method, the name must keep pointing at your server and port 80 must stay reachable, or the renewal fails.
Does it cost anything?
The software is free and open source under the MIT licence, including for business use. You pay only for the server it runs on.

Give your apps one secure front door

A whole server with root access and an unmetered port, month to month.

Get a server for your proxy

Read next