Nginx Proxy Manager: The Easiest Reverse Proxy for Self-Hosting
If you're running more than a couple of self-hosted services, you've probably hit the point where remembering port numbers gets old. Nextcloud is on :8080, Grafana on :3000, Gitea on :3001 -- and you're typing http://192.168.1.50:8080 into your browser like it's 2005. A reverse proxy fixes this by letting you access everything through clean subdomains like cloud.yourdomain.com and git.yourdomain.com, with HTTPS handled automatically.
The problem is that the most popular reverse proxy, Nginx, is configured through text files with a syntax that punishes small mistakes. Miss a semicolon, forget a closing brace, or get a proxy_pass directive slightly wrong, and you're staring at a white screen or a cryptic error. Nginx Proxy Manager (NPM) wraps all of that complexity in a web UI. You fill out a form, click save, and your service is live with a valid SSL certificate. No config files, no command line, no memorizing Nginx directives.
What Is a Reverse Proxy?
A reverse proxy sits between the internet and your services. Instead of exposing each application directly on its own port, all traffic comes through the proxy on ports 80 (HTTP) and 443 (HTTPS). The proxy looks at the hostname in the request -- cloud.yourdomain.com vs git.yourdomain.com -- and forwards it to the right backend service.
This gives you:
- Clean URLs -- subdomains instead of port numbers
- HTTPS everywhere -- the proxy handles SSL certificates for all your services
- A single point of entry -- only ports 80 and 443 need to be open, not dozens of application ports
- Centralized access control -- add authentication or IP restrictions in one place
Why Nginx Proxy Manager Exists
Nginx itself is battle-tested, extremely fast, and powers a huge chunk of the internet. But configuring it means writing server blocks like this:
server {
listen 443 ssl http2;
server_name cloud.yourdomain.com;
ssl_certificate /etc/letsencrypt/live/cloud.yourdomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/cloud.yourdomain.com/privkey.pem;
location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
Multiply that by ten services, add WebSocket support for some, configure certificate renewal with Certbot, and you're spending more time managing your proxy than running your actual services.
Nginx Proxy Manager was created by Jamie Curnow to solve exactly this problem. It's a Docker-based application that runs a full Nginx instance behind a clean admin UI. You get the performance and reliability of Nginx without ever touching a config file -- unless you want to.
Features
- Web-based admin UI -- manage everything from your browser
- Automatic Let's Encrypt SSL -- request and renew certificates with a checkbox
- Proxy hosts -- route subdomains to backend services with a form
- Redirection hosts -- set up 301/302 redirects without writing rewrite rules
- Streams -- proxy raw TCP/UDP traffic (useful for databases, game servers, or anything non-HTTP)
- Access lists -- restrict access with basic authentication or IP whitelists
- Custom Nginx configuration -- drop down to raw Nginx directives when the UI isn't enough
- Multi-user support -- create accounts with limited permissions
Setting Up Nginx Proxy Manager
Prerequisites
- A server with Docker and Docker Compose installed
- A domain name with DNS A records pointing to your server (or a wildcard
*.yourdomain.comrecord) - Ports 80 and 443 available on the host (no other web server running)
Docker Compose
Create a directory for NPM and add a 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" # Admin UI
volumes:
- npm_data:/data
- npm_letsencrypt:/etc/letsencrypt
networks:
- proxy
networks:
proxy:
name: proxy
volumes:
npm_data:
npm_letsencrypt:
Start it up:
docker compose up -d
That's it. Open http://your-server-ip:81 in your browser.
First Login
The default credentials are:
- Email:
[email protected] - Password:
changeme
You'll be prompted to change these immediately. Do it -- this interface controls all your routing and SSL certificates.
Port 81 is the admin interface. Once you're comfortable with the setup, you should proxy the admin UI itself behind a subdomain (like npm.yourdomain.com) and restrict access to it. More on that below.
Adding Your First Proxy Host
Let's say you're running Grafana on port 3000 and want it available at grafana.yourdomain.com.
- Make sure
grafana.yourdomain.comhas a DNS A record pointing to your server's IP - In the NPM admin UI, go to Hosts > Proxy Hosts and click Add Proxy Host
- Fill in the details:
- Domain Names:
grafana.yourdomain.com - Scheme:
http - Forward Hostname / IP:
172.17.0.1(or the container name if on the same Docker network) - Forward Port:
3000
- Domain Names:
- Click the SSL tab:
- Select Request a new SSL Certificate
- Check Force SSL (redirects HTTP to HTTPS)
- Check HTTP/2 Support
- Enter your email for Let's Encrypt
- Agree to the terms of service
- Click Save
Within a few seconds, https://grafana.yourdomain.com is live with a valid certificate. NPM handles the Nginx config generation, the Let's Encrypt challenge, and the certificate installation -- all from that one form.
A note on networking
If your backend services run in Docker, the easiest approach is to put them on the same Docker network as NPM. In the compose example above, we created a network called proxy. Add your services to this network:
services:
grafana:
image: grafana/grafana:latest
container_name: grafana
restart: unless-stopped
networks:
- proxy
networks:
proxy:
external: true
Then in NPM, use the container name (grafana) as the Forward Hostname instead of an IP address. This is more reliable than using IP addresses, which can change when containers restart.
SSL Certificate Renewal
Let's Encrypt certificates expire after 90 days. NPM automatically renews them before expiration -- you don't need to configure anything. The renewal process runs in the background and you'll only notice it if something goes wrong (usually a DNS issue or port 80 being blocked).
Access Lists
Not every service has built-in authentication. Some dashboards, admin panels, or internal tools are wide open by default. NPM's access lists let you add a layer of protection.
Basic Authentication
- Go to Access Lists and click Add Access List
- Name it (e.g., "Require Login")
- Under the Authorization tab, add username/password pairs
- Under the Access tab, optionally restrict by IP range
- Save the access list
- Edit your proxy host and select this access list from the dropdown
Now visitors must enter a username and password before reaching the backend service. This isn't a replacement for proper application-level authentication, but it's a useful first line of defense for services that lack it.
IP Restrictions
Access lists also support IP-based filtering. You can allow only your home IP, your VPN subnet, or a specific range. This is particularly useful for admin interfaces that should never be accessible from the public internet.
A common setup: create an access list that allows your local network (192.168.1.0/24) and your VPN range, then apply it to sensitive services. Everything else gets a basic auth prompt.
Redirection Hosts and Streams
Redirections
Need www.yourdomain.com to redirect to yourdomain.com? Or old.yourdomain.com to redirect to new.yourdomain.com? The Redirection Hosts section handles 301 and 302 redirects without any Nginx knowledge.
Streams
The Streams feature proxies raw TCP and UDP traffic -- anything that isn't HTTP. This is useful for:
- Database connections (PostgreSQL, MySQL)
- Game servers
- Mail servers (SMTP, IMAP)
- Custom TCP-based protocols
Streams route based on port number rather than hostname, since non-HTTP protocols don't have a Host header.
Custom Nginx Configuration
For 90% of use cases, the UI handles everything. But sometimes you need a specific Nginx directive -- custom headers, WebSocket upgrades for a particular path, cache settings, or client body size limits.
Every proxy host has an Advanced tab where you can paste raw Nginx configuration. This gets injected into the server block for that host. For example, to increase the maximum upload size for a Nextcloud instance:
client_max_body_size 10G;
proxy_buffering off;
proxy_request_buffering off;
Or to add WebSocket support for a specific path:
location /ws {
proxy_pass http://your-backend:8080;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
This is the escape hatch that makes NPM practical for advanced users. You get the convenience of the UI for the common stuff and raw Nginx power when you need it.
NPM vs. Other Reverse Proxies
| Feature | Nginx Proxy Manager | Traefik | Caddy | Raw Nginx |
|---|---|---|---|---|
| Configuration | Web UI | Docker labels + YAML | Caddyfile | Config files |
| Learning curve | Very low | Moderate-steep | Low-moderate | Moderate-steep |
| Auto HTTPS | Yes (Let's Encrypt) | Yes (Let's Encrypt) | Yes (built-in) | Manual (Certbot) |
| Docker auto-discovery | No | Yes (built-in) | No | No |
| Adding a new service | Fill out a form | Add labels to container | Edit Caddyfile | Edit config, reload |
| Web dashboard | Full management UI | Read-only dashboard | None | Third-party |
| Access control (built-in) | Basic auth, IP lists | Basic auth, forward auth | Basic auth | Via config |
| TCP/UDP proxying | Yes (Streams) | Yes | Yes (experimental) | Yes |
| Resource usage | ~150-200 MB RAM | ~50-80 MB RAM | ~30-50 MB RAM | ~10-30 MB RAM |
| Custom configuration | Advanced tab (per host) | File provider, labels | Caddyfile directives | Full control |
| Best for | Beginners, small setups | Docker-heavy, many containers | Simple config-file setups | Maximum control |
When to pick what
Nginx Proxy Manager is the right choice if you want the fastest path from "I have services on random ports" to "I have clean subdomains with HTTPS." The web UI means anyone in your household can understand what's going on, and you don't need to remember Nginx syntax. The downside is that every new service requires a manual trip to the UI.
Traefik is the better choice if you're running a Docker-heavy setup where containers come and go frequently. Traefik auto-discovers services through Docker labels, so adding a new service means adding a few labels to your compose file -- no separate proxy configuration to manage. The trade-off is a steeper learning curve and verbose label syntax. (We have a detailed Traefik guide if you want to explore that path.)
Caddy is a strong middle ground. Its Caddyfile syntax is human-readable and automatic HTTPS is built in with zero configuration. No web UI, but the config file is so simple it hardly matters. Good for people who are comfortable editing a text file but don't want Nginx's complexity.
Raw Nginx gives you maximum performance and maximum control. It's the right choice if you already know Nginx well, need every last bit of performance, or have requirements that no UI can accommodate. The cost is managing config files, certificate renewal, and reloads yourself.
Performance Considerations
NPM runs a full Nginx instance under the hood, so the actual proxying performance is identical to raw Nginx -- which is to say, excellent. Nginx handles thousands of concurrent connections with minimal resource usage.
The overhead of NPM itself comes from the management layer: a Node.js backend, a SQLite database for configuration, and the admin UI. This adds roughly 100-150 MB of RAM on top of what Nginx alone would use. For a homelab, this is negligible. If you're running on a Raspberry Pi with 1 GB of RAM, it's worth noting but still manageable.
The UI does not affect request processing. Once a proxy host is configured, requests flow through Nginx directly -- the Node.js management layer is not in the request path.
Common Issues
Port 80 or 443 already in use
If another web server (Apache, a different Nginx instance, or another application) is already listening on port 80 or 443, NPM's container won't start. Check with ss -tlnp | grep ':80\|:443' and stop the conflicting service. On many Linux distributions, Apache is installed and running by default.
DNS not set up correctly
NPM can't issue Let's Encrypt certificates if your domain doesn't resolve to your server's public IP. The Let's Encrypt HTTP challenge works by requesting a file from http://yourdomain.com/.well-known/acme-challenge/ -- if that request doesn't reach NPM, certificate issuance fails. Verify your DNS with dig yourdomain.com and make sure port 80 is reachable from the internet.
Certificate renewal failures
Renewals can fail silently if port 80 becomes blocked (firewall change, ISP issue) or DNS records change. Check the NPM logs periodically:
docker logs nginx-proxy-manager
If renewals are failing, the most common fixes are ensuring port 80 is open and that the DNS A record still points to the correct IP.
502 Bad Gateway
This means NPM received the request but couldn't reach the backend service. Common causes:
- The backend container isn't running
- The forward hostname or port is wrong
- The backend is on a different Docker network than NPM
- The scheme should be
httpsinstead ofhttp(some services only listen on HTTPS)
WebSocket connections dropping
Some applications (chat services, real-time dashboards, VS Code Server) use WebSockets. NPM handles WebSocket connections by default for most setups, but if you experience connection drops, add this to the Advanced tab of the proxy host:
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 86400;
Securing the Admin Interface
The NPM admin UI on port 81 should not be exposed to the internet without protection. Best practices:
- Proxy it through itself -- create a proxy host for
npm.yourdomain.compointing tolocalhost:81, add SSL, and then close port 81 on your firewall so it's only accessible through the HTTPS subdomain. - Apply an access list -- restrict access to your home IP or VPN subnet.
- Use a strong password -- the admin account controls your entire routing configuration.
Should You Use Nginx Proxy Manager?
Yes, if:
- You're new to reverse proxies and want the gentlest possible on-ramp
- You have a small to medium number of services that don't change frequently
- You prefer a visual interface over config files
- You want to set up HTTPS without learning Certbot, ACME challenges, or certificate management
- Multiple people in your household need to understand or manage the proxy
Probably not, if:
- You add and remove Docker containers frequently -- Traefik's auto-discovery will save you time
- You're comfortable with config files and want something lighter -- Caddy does the same job in fewer resources
- You need maximum performance on constrained hardware -- raw Nginx uses a fraction of the RAM
- You already know Nginx well -- NPM's UI will feel like a slower way to do what you can already do
The honest take: Nginx Proxy Manager is the easiest way to go from exposed ports to proper subdomains with HTTPS. It democratizes reverse proxy management in a way that raw Nginx, Traefik, and Caddy don't -- you genuinely don't need to understand any networking concepts beyond "this service runs on this port." That simplicity comes at the cost of manual per-service configuration and slightly higher resource usage. For most people starting out with self-hosting, NPM is the right first reverse proxy. If you outgrow it, Traefik and Caddy are natural next steps.
Resources
- Nginx Proxy Manager documentation
- Nginx Proxy Manager GitHub
- Let's Encrypt documentation
- Nginx documentation -- for understanding the advanced configuration options