Skip to content
Go back

Nginx Proxy Manager for Normal Humans

By SumGuy 7 min read
Nginx Proxy Manager for Normal Humans

The Reverse Proxy You’ll Actually Use

Here’s the thing about nginx: it’s powerful, it’s battle-tested, and the config syntax will make you feel like you’re reading ancient runes at 11 PM on a Tuesday. location ~ ^/api/(?!internal)(.+)$ is a perfectly valid line that nobody wants to debug after a long day.

Nginx Proxy Manager (NPM) is what you install when you want a working reverse proxy in 20 minutes without reading a 40-page manual. It’s a GUI on top of nginx and certbot, ships as a Docker container, and handles 90% of home lab reverse proxy needs with point-and-click simplicity.

No condescension about the GUI approach — your services get HTTPS, you get your evening back. That’s a win.

What NPM Actually Is

Under the hood, NPM is three things duct-taped together nicely:

It runs as three Docker containers: the NPM app itself, a MariaDB database (stores your proxy configs), and an optional SQLite mode if you want fewer moving parts. The standard production setup uses MariaDB.

Docker Compose Setup

docker-compose.yml
services:
npm:
image: jc21/nginx-proxy-manager:latest
restart: unless-stopped
ports:
- "80:80"
- "443:443"
- "81:81" # Admin UI — lock this down later
environment:
DB_MYSQL_HOST: "db"
DB_MYSQL_PORT: 3306
DB_MYSQL_USER: "npm"
DB_MYSQL_PASSWORD: "changeme"
DB_MYSQL_NAME: "npm"
volumes:
- ./data:/data
- ./letsencrypt:/etc/letsencrypt
depends_on:
- db
db:
image: jc21/mariadb-aria:latest
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD: "changeme_root"
MYSQL_DATABASE: "npm"
MYSQL_USER: "npm"
MYSQL_PASSWORD: "changeme"
volumes:
- ./mysql:/var/lib/mysql

Spin it up:

Terminal window
docker compose up -d

Port 81 is the admin UI. Ports 80 and 443 are where your traffic actually flows. Keep port 81 off the public internet — firewall it to your local network or VPN.

First Login: Change the Defaults Immediately

Open http://your-server-ip:81. Default credentials:

The UI prompts you to change both on first login. Do it. Seriously — there are bots scanning for exposed NPM instances with default creds. This isn’t paranoia, it’s basic hygiene.

Adding Your First Proxy Host

Dashboard → Proxy HostsAdd Proxy Host.

The form is straightforward:

Hit the SSL tab. Select Request a new SSL Certificate, check Force SSL and HTTP/2 Support. Enter your email for Let’s Encrypt notifications.

Click Save. NPM talks to Let’s Encrypt, gets a cert, and configures nginx. Takes about 15 seconds. Your service is now live at https://app.yourdomain.com with a valid cert.

That’s it. That’s the core loop.

Wildcard Certs with DNS Challenge (Cloudflare)

Standard HTTP challenge certs require port 80 to be reachable from the internet. DNS challenge doesn’t — it proves domain ownership by creating a TXT record instead. This lets you get a wildcard cert (*.yourdomain.com) covering all subdomains, even for services on an internal network with no public port 80.

In NPM, when requesting a cert: check Use a DNS Challenge, select Cloudflare as the provider, and paste in your Cloudflare API token (needs Zone:DNS:Edit permissions on your zone).

NPM creates the validation TXT record, waits for propagation, gets the wildcard cert, then cleans up the record. One cert to rule all your subdomains.

DNS propagation gotcha: If cert issuance fails with a DNS timeout, it’s usually because Cloudflare’s API and their authoritative servers haven’t fully synced yet. Wait 60 seconds and retry. It’s not you, it’s eventual consistency being its usual self.

Access Lists: IP Whitelists and Basic Auth

Under Access Lists, you can create reusable auth layers and attach them to proxy hosts.

IP Whitelist: Add your home IP range (e.g., 192.168.1.0/24). Any request outside that range gets a 403. Great for internal tools you accidentally exposed publicly.

Basic Auth: Add username/password pairs. NPM handles the nginx auth_basic config. Attach the access list to a proxy host and that service now prompts for credentials before loading.

You can combine both — whitelist AND basic auth. Belt and suspenders. Your 2 AM self will appreciate it when you realize you left Portainer accessible to the internet.

Streams: Proxying TCP and UDP

Most people don’t know NPM does TCP/UDP proxying too, not just HTTP. This is called Streams in the UI.

Use cases:

Dashboard → StreamsAdd Stream. Set the incoming port, the forwarding host/IP, and the destination port. Toggle UDP if needed.

No SSL on streams — that’s a TCP/UDP layer, not HTTP. For encrypted connections you’d handle TLS at the application level.

Cert Renewal Gotchas

Let’s Encrypt certs expire every 90 days. NPM auto-renews them, but renewal can fail silently if:

  1. Port 80 is blocked. HTTP challenge renewal requires port 80 to be reachable. If your ISP blocks it or your firewall rule lapsed, renewals fail. Check NPM’s logs: docker compose logs npm | grep -i renew

  2. DNS propagation is slow. DNS challenge renewals have the same timing issues as initial issuance. If you’re on a flaky DNS provider, consider switching to Cloudflare.

  3. The container restarted mid-renewal. Unlikely but it happens. If a cert shows as expired in the UI, just click the cert → Edit → Force renew.

Set a calendar reminder to check NPM’s cert list every month or two. It takes 30 seconds and saves you from an expired cert ruining a weekend.

When You’ve Outgrown NPM

NPM is excellent until it isn’t. Here’s where it starts showing limits:

The upgrade path is clear: NPM works great for a single-node home lab. When you hit the limits above — usually around the time you’re managing 20+ services or need per-service middleware — Traefik or Caddy is the next step.

Both have steeper learning curves. Both reward you with more power. Neither will replace NPM’s “20 minutes to working HTTPS” onboarding experience.

The Bottom Line

Nginx Proxy Manager is the reverse proxy that gets out of your way. Install it, add your services, get SSL certs, and go do something else. The GUI isn’t a crutch — it’s a reasonable tool choice for a common problem.

When complexity demands more: Traefik for Docker-native label-based routing, Caddy for simple config files that don’t feel like regex puzzles. But for most home labs, NPM will handle everything you throw at it without complaint.

Start here. Migrate when you have a reason to, not because someone on Reddit said GUIs are for amateurs.


Share this post on:

Send a Webmention

Written about this post on your own site? Send a webmention and it may appear here.


Previous Post
Bulk File Renaming on Linux: rename, vidir, fd
Next Post
Context Window vs Token Limit: Not the Same Thing

Related Posts