How to Set Up Nginx Proxy Manager: The Easiest Reverse Proxy for Self-Hosters
Set up Nginx Proxy Manager in Docker and manage SSL certificates, domains, and reverse proxies without touching a config file.
If you’re running multiple services on your homelab, you’ve hit the same problem I have: how do you route app1.example.com to one container, app2.example.com to another, and handle SSL for all of them without manually editing nginx config files every time?
Nginx Proxy Manager (NPM) is the answer. It’s a reverse proxy with a web UI that handles all the tedious stuff — SSL certificates, domain routing, access control — without requiring you to touch a single configuration file.
I switched from manually managing nginx configs to NPM about a year ago, and honestly, I’ve never looked back. If you find nginx configs intimidating (or just tedious), NPM is a game-changer.
What’s a Reverse Proxy Anyway?
Quick primer: A reverse proxy sits in front of your services and directs incoming traffic to the right place. Here’s why you need one:
SSL/HTTPS for everything: Your Vaultwarden instance, Jellyfin, Nextcloud — they all need HTTPS. A reverse proxy terminates SSL at one place, so you don’t configure it on every app.
Multiple domains, one server: Instead of running apps on random ports like localhost:8080, localhost:9000, you can use clean URLs: vault.example.com, media.example.com.
Access control: Block traffic, set IP whitelists, or require authentication before reaching your apps.
Performance: Caching, compression, load balancing across containers.
Easy updates: Update an app without losing your SSL config or routing rules.
NPM vs. Traefik vs. Caddy
I get asked this constantly. Here’s my honest take:
Traefik: Incredibly powerful, but configuration lives in YAML or Docker labels. If you like that, great. I find it overkill for a homelab with 5-10 services.
Caddy: Fantastic, minimal Caddyfile syntax, auto-SSL. We use Caddy for Vaultwarden because it’s lightweight. Downside: no web UI for non-technical users.
Nginx Proxy Manager: Dead simple web UI, point-and-click setup, auto-SSL via Let’s Encrypt. Perfect for homelab runners who want to avoid config files entirely. The trade-off? Slightly less flexible than pure nginx, but you gain 100 times better UX.
For a self-hosted setup with 3+ services? I pick NPM every time.
What You’ll Need
- Docker & Docker Compose: You know the drill. Guide here if needed.
- A domain name: With DNS pointing to your server. NPM will handle the Let’s Encrypt magic.
- A server with port 80/443 accessible: Needed for SSL certificate validation.
That’s it. Seriously. No config files, no nginx knowledge required.
Step 1: Install Nginx Proxy Manager
Create a directory:
mkdir -p ~/docker/npm
cd ~/docker/npm
Create docker-compose.yml:
version: '3.8'
services:
app:
image: 'jc21/nginx-proxy-manager:latest'
restart: unless-stopped
container_name: npm
ports:
- '80:80'
- '443:443'
- '81:81'
environment:
DB_MYSQL_HOST: 'db'
DB_MYSQL_PORT: 3306
DB_MYSQL_USER: 'npm'
DB_MYSQL_PASSWORD: 'npm-password'
DB_MYSQL_NAME: 'npm'
volumes:
- ./data:/data
- ./letsencrypt:/etc/letsencrypt
depends_on:
- db
db:
image: 'jc21/mariadb-aria:latest'
restart: unless-stopped
container_name: npm-db
environment:
MYSQL_ROOT_PASSWORD: 'root-password'
MYSQL_DATABASE: 'npm'
MYSQL_USER: 'npm'
MYSQL_PASSWORD: 'npm-password'
volumes:
- ./data/mysql:/var/lib/mysql
Security note: Change those placeholder passwords (npm-password, root-password). Use something strong.
Start it up:
docker compose up -d
Verify it’s running:
docker compose ps
You should see both npm and npm-db containers running.
Step 2: First Login
Open your browser to http://your-server:81
Default credentials:
- Email:
[email protected] - Password:
changeme
Change these immediately — go to Settings and update your admin email and password.
Step 3: Point Your Domain
Make sure your domain’s A record points to your server:
dig yourdomain.com +short
# Should return your server's IP
NPM will use this to validate Let’s Encrypt certificates. Don’t skip this step or SSL won’t work.
Step 4: Add Your First Proxy Host
This is where NPM shines. Let’s route app.yourdomain.com to a service running on port 3000 locally.
- In the NPM admin panel, go to Proxy Hosts
- Click Add Proxy Host
- Fill in:
- Domain Names:
app.yourdomain.com - Scheme:
http(orhttpsif your app already uses SSL) - Forward Hostname/IP:
localhostor your service container name - Forward Port:
3000(whatever your app is running on)
- Domain Names:
- Go to the SSL tab
- Select Request a new SSL Certificate
- Check Force SSL to always redirect HTTP → HTTPS
- Click Save
NPM will request a Let’s Encrypt certificate automatically. This usually takes 30 seconds.
That’s it. You now have https://app.yourdomain.com pointing to your service with a valid SSL certificate. No manual nginx editing. No certificate renewal scripts. It just works.
Step 5: Add More Services
Repeat Step 4 for every service:
vault.yourdomain.com→ your Vaultwarden instance on port 8080media.yourdomain.com→ Jellyfin on port 8096cloud.yourdomain.com→ Nextcloud on port 80
Each one gets its own proxy host, its own Let’s Encrypt certificate, and its own routing rule. All from the web UI.
Access Control: Block Traffic You Don’t Want
NPM includes Access Lists for IP whitelisting and authentication.
IP Whitelisting
If you only want your home IP accessing something:
- Go to Access Lists → Add Access List
- Name it:
Home IP - Add your IP address
- Go to your proxy host → Access tab
- Select your access list
Now only your IP can reach that service. Attempts from elsewhere get a 403.
HTTP Basic Auth
For on-the-go access without hardcoding IPs:
- Access Lists → Add Access List
- Enable HTTP Basic Auth
- Add username/password
- Assign to proxy host
When accessing, you’ll be prompted for credentials. Simple but effective.
Custom Locations & Advanced Routing
Sometimes you need fancier routing. NPM supports Custom Locations:
- Go to your proxy host → Locations tab
- Click Add Location
- Set path:
/api/special - Forward to a different hostname/port
- Add custom headers or rules
Example: Route /api/* requests to one container, everything else to another.
Common Gotchas
SSL certificate won’t issue
The most common culprit: your domain’s DNS doesn’t actually point to your server. Verify:
dig yourdomain.com
If you see your IP, DNS is fine. If not, wait 5-10 minutes and try again.
“502 Bad Gateway” errors
This means NPM can’t reach your service. Check:
# From inside NPM container
docker exec npm curl http://your-service-container:port
# Or if using localhost
docker exec npm curl http://localhost:8080
If the container is running but unreachable, make sure it’s on the same Docker network or accessible from the host.
HTTPS to your app, but NPM is HTTP
If your app runs on port 443 internally, set the Forward Scheme to https. NPM will communicate with it securely.
Large file uploads failing
By default, nginx has a 1MB body size limit. Increase it per proxy host:
- Go to proxy host → Advanced tab
- Paste in the Custom Nginx Configuration field:
client_max_body_size 512M;
This allows 512MB uploads. Adjust as needed.
Maintenance & Updates
NPM updates are painless:
cd ~/docker/npm
docker compose pull
docker compose down
docker compose up -d
SSL certificates renew automatically 30 days before expiry. You’ll never see a warning.
Backups are important:
# Backup the data directory
tar -czf npm-backup-$(date +%Y%m%d).tar.gz ./data ./letsencrypt
Consider a monthly cron job for this.
Performance Tips
Enable gzip compression: Go to proxy host → Advanced → add:
gzip on;
gzip_types text/plain text/css application/json application/javascript;
Set reasonable timeouts: For slow apps, increase read timeouts:
proxy_read_timeout 300s;
Use a CDN: If you’re exposing NPM to the internet, consider Cloudflare or similar for DDoS protection.
Monitoring Your Setup
Check that Let’s Encrypt certificates are renewing:
docker logs npm | grep -i certificate
Monitor disk usage for logs:
du -sh ~/docker/npm/
NPM logs accumulate. Clean old logs occasionally:
docker exec npm-db find /data/logs -mtime +30 -delete
FAQ
Can I use NPM with a self-signed certificate?
Yes, but your clients will see SSL warnings. For local-only access that’s fine. For external access, use Let’s Encrypt (it’s free).
Does NPM work on ARM (Raspberry Pi)?
Yes. The jc21/nginx-proxy-manager image supports ARM64. RPi4 handles it easily.
Can I run NPM behind another reverse proxy?
Technically yes, but it gets complex. Keep NPM at the edge (ports 80/443) when possible.
How much memory does NPM use?
Surprisingly little. The container typically uses 100-150MB. The MariaDB database might use 200-300MB depending on how many proxy hosts you have. Even on a 1GB VPS, it’s not a bottleneck.
What if my ISP blocks port 80/443?
You’re out of luck for public HTTPS. Consider a VPS with an IP address that can reach those ports, and run NPM there instead.
Can I export my proxy hosts configuration?
The database lives in ./data/mysql/. Back it up regularly. There’s no built-in export, but the database can be restored to any NPM instance.
Is NPM suitable for production?
Absolutely. We’re running it for multiple personal sites and services. Stability is excellent. That said, if you’re running a large-scale multi-tenant operation, you might outgrow it into a dedicated reverse proxy cluster.
Next Steps
Now that you have NPM managing your domains and SSL:
- Add all your self-hosted services behind it
- Set up monitoring/alerting for certificate renewal (though it’s automatic)
- Explore access control for sensitive services
- Consider adding a Wireguard VPN so you don’t expose everything to the internet
You’ve just eliminated a huge pain point in self-hosting. No more fiddling with nginx configs, no more manual certificate renewal. That’s a win.
Resources:
Stay in the loop 📬
Get self-hosting tutorials, tool reviews, and infrastructure tips delivered to your inbox. No spam, unsubscribe anytime.
Join 0 self-hosters. Free forever.