Nginx Proxy Manager: Reverse Proxy + Free SSL Setup

If you're running more than two or three self-hosted apps at home or on a VPS, you've hit this problem: one public IP, one server, but five services all fighting over port 80 and 443. Nginx Proxy Manager (NPM) solves it — it's a Docker container that runs Nginx behind a web UI, lets you route domains to different internal services, and pulls free Let's Encrypt SSL certificates with a few clicks. No manually editing nginx.conf, no certbot cron jobs to babysit.

I've run NPM in front of Vaultwarden, a Plex-adjacent stack, a couple of internal dashboards, and a Gitea instance on the same box for a few years now. It's not perfect — the UI hides some Nginx behavior you'll eventually want to see directly — but for 90% of homelab and small-VPS use cases, it's the fastest path from "I have a service running on port 8080" to "https://app.mydomain.com works and renews itself."

What Is Nginx Proxy Manager, Exactly?

Nginx Proxy Manager is a Docker container (image: jc21/nginx-proxy-manager) that bundles Nginx with a Node.js management API and a web dashboard. Instead of writing server blocks by hand, you fill out a form: domain name, where to forward it (IP and port), and whether you want SSL. Click save, and NPM writes the Nginx config and requests a Let's Encrypt cert for you.

It ships with SQLite by default now, which is a real improvement — older tutorials will tell you to spin up a MariaDB container alongside it. Unless you're running more than about 50 proxy hosts, don't bother. SQLite is one less container to patch and one less thing to break.

Before You Start: What You Actually Need

  • A Linux host with Docker and Docker Compose installed (Ubuntu, Debian, or a VM/LXC on Proxmox all work fine)
  • A domain name you control, with the ability to edit DNS records
  • Port 80 and 443 free on that host — nothing else can already be bound to them
  • If you're at home: router access to forward ports 80 and 443 to the Docker host's internal IP

That last point trips people up constantly. Let's Encrypt's HTTP-01 challenge (the default method NPM uses) needs port 80 reachable from the public internet during certificate issuance. If your ISP blocks inbound port 80, or your router's port forward is pointed at the wrong internal IP, the cert request will fail and the error message won't always make that obvious.

Setting Up Nginx Proxy Manager with Docker Compose

Create a directory for it and drop in a compose file:

mkdir -p ~/npm && cd ~/npm
nano docker-compose.yml

Here's the minimal config — this is the one to use unless you specifically need MySQL/MariaDB for a large deployment:

services:
  app:
    image: 'jc21/nginx-proxy-manager:latest'
    container_name: npm
    restart: unless-stopped
    ports:
      - '80:80'      # HTTP - needed for sites + Let's Encrypt challenge
      - '443:443'    # HTTPS
      - '81:81'      # Admin UI
    volumes:
      - ./data:/data
      - ./letsencrypt:/etc/letsencrypt

Bring it up:

docker compose up -d

First start takes 20-30 seconds while it initializes the SQLite database and generates JWT signing keys. Check the logs if you're impatient:

docker compose logs -f

You're looking for a line indicating the app is ready to serve requests. Once it settles, hit http://your-server-ip:81 in a browser.

Note on the port 81 admin UI: it's plain HTTP, not HTTPS, on first setup. That's expected and fine on your LAN — just don't leave it exposed to the internet long-term. More on that in the security section below.

First Login and Default Credentials

The default login is:

  • Email: admin@example.com
  • Password: changeme

NPM forces you to change both the email and password immediately after first login — you can't skip this. Use a real email address you control and a password manager-generated password here. Don't reuse a password from anywhere else; this admin panel controls DNS-facing infrastructure.

How Do I Point a Domain at NPM?

Two things need to happen before you touch the NPM UI:

  1. Create an A record (or AAAA for IPv6) in your DNS provider pointing your subdomain — say app.yourdomain.com — at your server's public IP
  2. Wait for it to propagate. Usually minutes, sometimes longer depending on your registrar's TTL settings

Check propagation before wasting time troubleshooting NPM itself:

dig +short app.yourdomain.com

If that doesn't return your server's IP, stop — fix DNS first. Nothing downstream will work until this resolves correctly.

Adding a Proxy Host and Getting Free SSL

In the NPM dashboard, go to Hosts → Proxy Hosts → Add Proxy Host. Fill in:

  • Domain Names: app.yourdomain.com
  • Scheme: http (this is the internal connection to your app, not the public-facing scheme)
  • Forward Hostname/IP: the container name if it's on the same Docker network, or the internal IP if it's a separate host
  • Forward Port: whatever port your app actually listens on internally

Then switch to the SSL tab:

  • Select "Request a new SSL Certificate"
  • Toggle "Force SSL" (redirects HTTP to HTTPS — you want this on)
  • Toggle "HTTP/2 Support" if your app doesn't have a reason to avoid it
  • Agree to the Let's Encrypt terms, enter your email, save

If DNS and port 80 are both correct, this takes a few seconds. If it hangs or errors, it's almost always one of: DNS hasn't propagated, port 80 isn't actually reachable from the internet (check your router's port forward and any cloud firewall/security group), or you've hit Let's Encrypt's rate limits from too many retries on the same domain.

Forwarding to a Container on the Same Docker Host

If your app and NPM are both Docker containers on the same host, put them on a shared Docker network instead of relying on the host's IP:

docker network create proxy-net

Add networks: [proxy-net] to both the NPM compose file and your app's compose file, then in NPM's proxy host config, use the app's container name as the Forward Hostname, not an IP. This avoids a class of bugs where container IPs change on restart and your proxy host silently breaks.

Nginx Proxy Manager vs. Manual Nginx + Certbot

Factor Nginx Proxy Manager Manual Nginx + Certbot
Setup time per new site 1-2 minutes via UI 5-10 minutes editing config files
Cert renewal Automatic, handled internally Automatic via certbot cron/systemd timer, but you set it up
Config flexibility Limited to what the UI exposes, plus custom config snippets Full control over every directive
Learning curve Low — good for homelab beginners Higher — you need to understand Nginx directives
Best for Multiple small self-hosted apps, homelabs, small teams Complex routing, load balancing, high-traffic production

I still reach for manual Nginx config on production boxes where I need precise control over caching headers or complex location blocks. For a homelab with a dozen self-hosted apps, NPM wins on time saved, full stop.

Securing Nginx Proxy Manager

NPM has no built-in two-factor authentication as of this writing (verify this hasn't changed before you rely on it). That means port 81 — the admin UI — should never be reachable from the public internet. A few options, roughly in order of how much I'd trust them:

  • Best: bind port 81 to localhost only and reach it over a VPN (WireGuard, Tailscale) or SSH tunnel: ports: - '127.0.0.1:81:81', then ssh -L 8181:localhost:81 user@server from your laptop
  • Good: firewall port 81 to your home/office IP only, using ufw or your cloud provider's security group
  • Minimum: put an Access List (Hosts → Access Lists in the UI) with HTTP Basic Auth in front of it if you must expose it

Also keep the image patched — docker compose pull && docker compose up -d periodically. Security fixes do land in this project and there's no auto-update mechanism.

Common Problems and Fixes

Can't Reach the Admin UI on Port 81

Confirm the port is actually mapped:

docker compose ps

Check you're using http:// not https:// — the panel is plain HTTP unless you've put something else in front of it. Then check your host firewall isn't blocking it.

SSL Certificate Request Fails

This is nearly always a reachability problem, not an NPM bug. From an external machine (not on the same network), test:

curl -I http://app.yourdomain.com

If that doesn't connect, Let's Encrypt's servers can't reach you either, and the cert request will keep failing regardless of what you do inside the NPM UI.

502 Bad Gateway After Setup

The proxy host exists and SSL issued fine, but the upstream app isn't reachable. Check the Forward Hostname/IP and Forward Port match what your app is actually listening on, and if it's a container, confirm both containers share a Docker network.

FAQ

Is Nginx Proxy Manager free?

Yes. It's open source, and the Let's Encrypt certificates it issues are free. There's no paid tier or feature gating.

Do I need a static IP to use Nginx Proxy Manager?

No, but you do need a way for your domain to track your current IP if it changes — a dynamic DNS service works fine for home connections without a static IP.

Can I run Nginx Proxy Manager without Docker?

Officially, no — it's distributed and supported as a Docker image. There are unofficial bare-metal install methods floating around, but you lose the isolation and easy updates that make NPM worth using in the first place.

Does Nginx Proxy Manager work with wildcard SSL certificates?

Yes, but wildcard certs require DNS-01 validation instead of HTTP-01, which means configuring a DNS provider plugin (Cloudflare, Route53, etc.) in NPM's SSL settings rather than relying on port 80.

What's the difference between Nginx Proxy Manager and Traefik?

Traefik auto-discovers services via Docker labels and needs no UI to function; NPM is UI-first and you configure hosts manually through the dashboard. Traefik has a steeper learning curve but scales better for dynamic container environments; NPM is more approachable if you're not already comfortable with Docker labels and YAML-driven config.

Wrapping Up

Nginx Proxy Manager won't replace hand-written Nginx configs for complex production setups, but for getting SSL-secured domains in front of a handful of self-hosted services, it removes almost all the friction. Get the compose file running, lock down port 81, and the rest is filling out forms in a browser instead of fighting with certbot renewal hooks at 2am.