The Real-World Problem: Managing Multiple Services on One VPS
If you’re running multiple applications on the same VPS — say Nextcloud, Gitea, an API backend, and a WordPress site — you’ll quickly run into a classic problem: they all want to listen on ports 80 and 443, but only one process can bind to each port at a time.
The traditional solution is to manually configure Nginx as a reverse proxy: every time you add a new service, you create a new config file, request an SSL certificate from Let’s Encrypt, and reload Nginx. It’s not technically difficult, but it’s repetitive and error-prone — especially when you’re juggling 5–10 subdomains at once.
Three Common Approaches — An Honest Comparison
1. Vanilla Nginx + Manual Certbot
Pros: Lightweight, full control, no additional tooling required.
Cons: Every time you add a domain, you have to run certbot, edit files under /etc/nginx/sites-available/, then run nginx -t && systemctl reload nginx. With 10 subdomains, this becomes a real maintenance burden.
2. Traefik
Pros: Great Docker integration, automatic container discovery via labels, built-in Let’s Encrypt support.
Cons: Configuration through YAML files and labels can get complex, and the learning curve is steeper. There’s no intuitive web UI to visualize routing status. If you prefer a label-driven approach, check out Traefik: The Ultimate Reverse Proxy Solution for Docker for a full setup walkthrough.
3. Nginx Proxy Manager (NPM)
Pros: Clean web interface, adding a proxy host is just filling out a form, one-click automatic Let’s Encrypt SSL, supports access lists, redirects, and stream proxying.
Cons: Slightly heavier since it includes a database (SQLite or MariaDB), and you need a basic understanding of reverse proxying to use it correctly.
When Should You Choose Nginx Proxy Manager?
NPM is the best fit when:
- You’re managing a personal VPS or small team setup and prioritize deployment speed over resource optimization
- Some team members aren’t comfortable editing Nginx config files by hand
- You frequently add or remove subdomains and don’t want to SSH into the server every time
- You want a dashboard to quickly check the status of all proxy hosts
The first time I used Docker Compose for a real project, I made quite a few basic mistakes that are almost funny in hindsight — including trying to configure Nginx manually outside Docker while my containers were running inside their own network. Discovering that NPM runs directly inside the Docker network completely solved that problem.
Deploying Nginx Proxy Manager with Docker Compose
Prerequisites
- A VPS running Linux (Ubuntu 22.04 or Debian 12 recommended)
- Docker and Docker Compose installed
- A domain with its A record pointing to your VPS IP
- Ports 80, 443, and 81 open on your firewall — be aware that Docker can bypass UFW rules, so verify your iptables configuration carefully
Step 1: Create the Directory and docker-compose.yml
mkdir -p ~/npm && cd ~/npm
Create the docker-compose.yml file:
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' # Web UI
volumes:
- ./data:/data
- ./letsencrypt:/etc/letsencrypt
networks:
default:
name: npm_network
driver: bridge
This setup uses SQLite by default — perfectly sufficient for a personal VPS. If you need easier scaling or backups, you can add MariaDB to the stack.
Step 2: Start NPM
docker compose up -d
# Check the logs
docker compose logs -f npm
After about 30 seconds, visit http://<YOUR-VPS-IP>:81 to access the management interface.
Default login credentials:
- Email:
[email protected] - Password:
changeme
Change these immediately after your first login.
Step 3: Add Your First Proxy Host
Suppose you have an application running in a container named myapp, listening on port 3000 inside the npm_network Docker network.
- Go to Proxy Hosts → Add Proxy Host
- Domain Names: enter
app.yourdomain.com - Scheme:
http - Forward Hostname/IP: the container name (
myapp) or its internal IP - Forward Port:
3000 - Enable Block Common Exploits and Websockets Support if needed
- Go to the SSL tab → select Request a new SSL Certificate → enable Force SSL and HTTP/2 Support
- Enter your Let’s Encrypt email → check the agreement box → click Save
NPM will automatically obtain a certificate from Let’s Encrypt and configure HTTPS. The entire process takes under 30 seconds.
Step 4: Connect Your Docker Applications to the Same Network
For NPM to forward requests to other containers, they must share the same Docker network. The easiest approach is to declare an external network in the application’s docker-compose.yml:
services:
myapp:
image: myapp:latest
container_name: myapp
# No need to expose ports to the host
networks:
- npm_network
networks:
npm_network:
external: true
This way, myapp doesn’t need to publish any ports to the host — only NPM receives traffic from the internet, while all other containers remain fully isolated. For an alternative approach that avoids opening any inbound ports at all, Cloudflare Tunnel is worth considering alongside NPM.
Common Real-World Scenarios
Redirect HTTP to HTTPS for All Subdomains
In the SSL section of each Proxy Host, simply enable Force SSL. NPM handles the 301 redirect automatically.
Add Basic Authentication to an Internal Subdomain
Go to the Access Lists tab → create a new list with a username and password → assign it to the Proxy Host you want to protect. This is handy for tools like Grafana, Kibana, or other internal utilities that don’t have their own authentication.
Automatic SSL Renewal
NPM automatically renews certificates via an internal cron job. You don’t need to do anything — unlike a manual Certbot setup where you’d need to check systemctl status certbot.timer.
Backup and Restore
All data (config, certificates, database) lives in the ./data and ./letsencrypt directories you mounted. Backing up is straightforward:
tar -czf npm-backup-$(date +%Y%m%d).tar.gz ~/npm/data ~/npm/letsencrypt
For automated, scheduled backups of Docker volumes to cloud storage, the sidecar container approach for backing up to S3 and Google Drive pairs well with this setup.
Things to Keep in Mind
- Port 81 should only be open temporarily or restricted by IP. Once you’re done configuring, close port 81 on your firewall and only open it when you need to make changes.
- Let’s Encrypt rate limits: A maximum of 5 new certificates per domain per 7 days. Don’t repeatedly retry during testing — use the staging environment first.
- Wildcard SSL: NPM supports wildcard certificates (
*.yourdomain.com), but they require DNS challenge verification, which means additional configuration for your DNS provider (Cloudflare, AWS Route53, etc.). - Version pinning: Replace
jc21/nginx-proxy-manager:latestwith a specific tag in production environments to avoid breaking changes during updates.

