Running multiple self-hosted services on one server creates a problem: they all need different ports. Nextcloud on 8080, Jellyfin on 8096, Grafana on 3000 — and you’re expected to remember all of them. Worse, none of them have SSL unless you set it up manually.
Nginx Proxy Manager (NPM) fixes this by giving you a clean web UI to manage reverse proxy hosts, automatically provision SSL certificates from Let’s Encrypt, and control access — all without touching a single Nginx config file.
It’s the most beginner-friendly reverse proxy option in the self-hosting world, and this guide will get you from zero to fully configured in about 15 minutes.
What is Nginx Proxy Manager?
Nginx Proxy Manager is a Docker-based application that wraps Nginx with an admin interface. Instead of writing server blocks and location directives by hand, you point and click.
Key features:
- Web-based UI for managing proxy hosts
- Automatic SSL via Let’s Encrypt with one-click renewal
- Access lists for basic authentication and IP restrictions
- Redirection hosts for URL forwarding
- Stream proxying for TCP/UDP services (databases, game servers)
- Custom Nginx config when you need more control
- Multi-user support with role-based permissions
Prerequisites
Before starting, make sure you have:
- A Linux server (Ubuntu, Debian, or any distro with Docker support)
- Docker and Docker Compose installed
- A domain name pointing to your server’s public IP (for SSL)
- Ports 80 and 443 open on your firewall/router
If you don’t have Docker installed yet, run:
curl -fsSL https://get.docker.com | sh
sudo usermod -aG docker $USER
Log out and back in for the group change to take effect.
Step 1: Create the Docker Compose File
Create a directory for Nginx Proxy Manager and set up your compose file:
mkdir -p ~/nginx-proxy-manager && cd ~/nginx-proxy-manager
Create docker-compose.yml:
version: "3.8"
services:
npm:
image: jc21/nginx-proxy-manager:latest
container_name: nginx-proxy-manager
restart: unless-stopped
ports:
- "80:80" # HTTP
- "443:443" # HTTPS
- "81:81" # Admin UI
volumes:
- ./data:/data
- ./letsencrypt:/etc/letsencrypt
environment:
- TZ=America/New_York
That’s it. No database configuration needed — NPM uses SQLite by default, which is perfect for home lab use.
Optional: Use MariaDB for Larger Deployments
If you’re proxying 50+ services or want better performance, add MariaDB:
version: "3.8"
services:
npm:
image: jc21/nginx-proxy-manager:latest
container_name: nginx-proxy-manager
restart: unless-stopped
ports:
- "80:80"
- "443:443"
- "81:81"
volumes:
- ./data:/data
- ./letsencrypt:/etc/letsencrypt
environment:
- TZ=America/New_York
- DB_MYSQL_HOST=db
- DB_MYSQL_PORT=3306
- DB_MYSQL_USER=npm
- DB_MYSQL_PASSWORD=your_secure_password
- DB_MYSQL_NAME=npm
depends_on:
- db
db:
image: mariadb:10.11
container_name: npm-db
restart: unless-stopped
environment:
- MYSQL_ROOT_PASSWORD=your_root_password
- MYSQL_DATABASE=npm
- MYSQL_USER=npm
- MYSQL_PASSWORD=your_secure_password
volumes:
- ./mysql:/var/lib/mysql
For most home lab setups, stick with the SQLite version. It’s simpler and performs well for dozens of proxy hosts.
Step 2: Start the Container
docker compose up -d
Wait about 30 seconds for the container to initialize, then check the logs:
docker logs nginx-proxy-manager
You should see Nginx starting and the admin interface becoming available.
Step 3: Access the Admin Panel
Open your browser and navigate to:
Log in with the default credentials:
- Email:
[email protected] - Password:
changeme
You’ll be immediately prompted to change these. Do it now — use a strong password and your real email address. The email is used for Let’s Encrypt certificate notifications.
Step 4: Add Your First Proxy Host
Let’s say you’re running Portainer on port 9443 and want to access it at portainer.yourdomain.com.
- Click Hosts → Proxy Hosts → Add Proxy Host
- Fill in the Details tab:
- Domain Names:
portainer.yourdomain.com - Scheme:
https(Portainer uses HTTPS by default) - Forward Hostname/IP:
YOUR_SERVER_IP(or container name if on same Docker network) - Forward Port:
9443 - Block Common Exploits: ✅ Enable
- Websockets Support: ✅ Enable (many services need this)
- Domain Names:
- Click the SSL tab:
- SSL Certificate: Request a New SSL Certificate
- Force SSL: ✅ Enable
- HTTP/2 Support: ✅ Enable
- HSTS Enabled: ✅ Enable
- Email Address: Your email for Let’s Encrypt
- Agree to Terms of Service: ✅
- Click Save
NPM will automatically request an SSL certificate from Let’s Encrypt and configure the proxy. Within a few seconds, https://portainer.yourdomain.com will be live with a valid SSL certificate.
Step 5: Docker Networking (The Right Way)
Using server IP addresses works, but it’s better to put NPM and your services on the same Docker network. This keeps traffic internal and lets you use container names instead of IPs.
Create a shared network:
docker network create proxy
Update your NPM docker-compose.yml:
services:
npm:
image: jc21/nginx-proxy-manager:latest
container_name: nginx-proxy-manager
restart: unless-stopped
ports:
- "80:80"
- "443:443"
- "81:81"
volumes:
- ./data:/data
- ./letsencrypt:/etc/letsencrypt
networks:
- proxy
- default
networks:
proxy:
external: true
Then in any service you want to proxy (e.g., Nextcloud), add:
networks:
- proxy
Now in NPM, you can use the container name (like nextcloud) as the Forward Hostname instead of an IP address. If the container restarts with a different IP, everything still works.
Step 6: Set Up Access Lists
Access lists let you restrict who can reach certain services. This is useful for admin panels you don’t want exposed to the internet.
- Go to Access Lists → Add Access List
- Name:
Admin Only - Under the Authorization tab, add username/password combinations
- Under the Access tab, add allowed IP ranges (e.g.,
192.168.1.0/24for your local network) - Save
Now when editing a proxy host, you can assign this access list under the Details tab. Anyone outside your allowed IPs will get a 403, and anyone inside will need the username/password.
Step 7: Redirection Hosts
Want yourdomain.com to redirect to www.yourdomain.com? Or redirect an old URL to a new one?
- Go to Hosts → Redirection Hosts → Add Redirection Host
- Domain Names:
yourdomain.com - Forward Scheme:
https - Forward Domain:
www.yourdomain.com - HTTP Code:
301(permanent redirect) - Preserve Path: ✅ Enable
- Set up SSL the same as proxy hosts
This is also useful for creating short-link-style redirects for services with long URLs.
Step 8: Custom SSL Certificates
While Let’s Encrypt works for most cases, you might want to use wildcard certificates or certificates from another provider.
Wildcard Certificates with DNS Challenge
For *.yourdomain.com certificates, you’ll need DNS challenge support. NPM supports this for many DNS providers:
- Go to SSL Certificates → Add SSL Certificate → Let’s Encrypt
- Domain Names:
*.yourdomain.comandyourdomain.com - Enable DNS Challenge: ✅
- DNS Provider: Select yours (Cloudflare, DigitalOcean, Route53, etc.)
- Credentials File Content: Add your API token
For Cloudflare, the credentials look like:
Generate a Cloudflare API token with Zone:DNS:Edit permissions for your domain.
Once you have a wildcard certificate, you can reuse it for every proxy host under that domain — no more waiting for individual cert provisioning.
Step 9: Stream Proxying (TCP/UDP)
Need to proxy non-HTTP traffic? NPM can handle TCP and UDP streams. This is useful for:
- Database connections (MySQL, PostgreSQL)
- Game servers (Minecraft, Valheim)
- SSH on custom ports
- DNS services
Go to Hosts → Streams → Add Stream:
- Incoming Port: The port users will connect to
- Forward Host: The server/container running the service
- Forward Port: The actual service port
- TCP/UDP Forwarding: Select the protocol
Securing Nginx Proxy Manager
Move the Admin Panel Off Port 81
Exposing the admin panel on port 81 is convenient but not ideal. Options:
Option 1: Proxy NPM through itself. Create a proxy host for npm.yourdomain.com pointing to 127.0.0.1:81, then block port 81 in your firewall:
sudo ufw deny 81
Option 2: Bind port 81 to localhost only:
ports:
- "80:80"
- "443:443"
- "127.0.0.1:81:81"
Then access via SSH tunnel: ssh -L 81:localhost:81 user@server
Enable Fail2Ban
Add brute-force protection by mounting the NPM log directory and configuring Fail2Ban:
sudo apt install fail2ban
Create /etc/fail2ban/filter.d/npm.conf:
[Definition]
failregex = ^<HOST> .* 4\d\d
ignoreregex =
Create /etc/fail2ban/jail.d/npm.conf:
[npm]
enabled = true
filter = npm
logpath = /path/to/nginx-proxy-manager/data/logs/proxy-host-*_access.log
maxretry = 5
bantime = 3600
findtime = 600
Troubleshooting
“502 Bad Gateway”
This means NPM can reach Nginx but Nginx can’t reach your service.
- Check the service is running:
docker psto verify the container is up - Check the port: Make sure you’re using the correct internal port
- Check the scheme: Some services use HTTPS internally (like Portainer)
- Check networking: If using container names, make sure both containers are on the same Docker network
SSL Certificate Fails to Provision
- DNS not pointing to your server: Run
dig yourdomain.comand verify the A record - Ports 80/443 blocked: Let’s Encrypt needs to reach your server on these ports
- Rate limiting: Let’s Encrypt has rate limits — if you’ve requested too many certs recently, wait an hour
“504 Gateway Timeout”
The backend service is too slow to respond.
Add custom Nginx config to the proxy host under the Advanced tab:
proxy_read_timeout 300s;
proxy_connect_timeout 300s;
proxy_send_timeout 300s;
Admin Panel Not Loading
If port 81 isn’t responding:
docker logs nginx-proxy-manager
docker restart nginx-proxy-manager
Check if another service is using port 81: sudo lsof -i :81
Tips and Best Practices
- Always enable “Block Common Exploits” — it adds basic protection against common attack patterns
- Enable Websockets Support by default — most modern apps need it and it doesn’t hurt if they don’t
- Use Docker networks instead of IP addresses — more reliable and secure
- Back up the data directory — it contains your certs, configs, and database
- Set up wildcard certs if you have more than 3-4 services — saves time and avoids rate limits
- Keep NPM updated:
docker compose pull && docker compose up -d - Monitor certificate expiry — NPM auto-renews, but check the SSL Certificates page occasionally
Backing Up Nginx Proxy Manager
Your entire NPM configuration lives in the data and letsencrypt directories. Back them up:
tar -czf npm-backup-$(date +%Y%m%d).tar.gz data letsencrypt
To restore, extract the archive into a fresh NPM directory and start the container. Everything — proxy hosts, SSL certs, access lists — comes back exactly as it was.
Conclusion
Nginx Proxy Manager takes the most intimidating part of self-hosting — reverse proxies and SSL — and makes it approachable. With a few clicks, you get proper domain names and HTTPS for all your services.
Once it’s running, adding a new service takes about 30 seconds: create the proxy host, point it at the container, request an SSL cert, done. It’s the kind of infrastructure that makes everything else easier.
Next steps:
- Set up Authelia for single sign-on across all your proxied services
- Configure Uptime Kuma to monitor your proxy hosts
- Explore Traefik if you want automatic service discovery with Docker labels
Your services deserve real URLs and real certificates. Nginx Proxy Manager makes that trivially easy.