Nginx Proxy Manager Setup: The Complete Homelab Guide for Secure Reverse Proxying
Step-by-step guide to install and configure Nginx Proxy Manager in your homelab using Docker Compose, with SSL, security hardening, and troubleshooting.
Introduction
In any homelab, exposing multiple web services (like Grafana, Nextcloud, or a self-hosted AI dashboard) to your local network or the internet requires a robust reverse proxy. Manually configuring Nginx is powerful but error-prone, especially when you have dozens of services and need SSL certificates for each. This is where Nginx Proxy Manager (NPM) shines—it provides a clean web UI to manage reverse proxy hosts, automatic Let's Encrypt SSL certificates, and access lists, all without touching a single Nginx config file.
This guide is written for homelab enthusiasts who have basic Docker knowledge but want a production-ready setup. You will learn how to deploy NPM using Docker Compose, configure your first proxy host, enable SSL with Let's Encrypt, and apply security hardening. We'll also cover common pitfalls like port conflicts and database permission errors, with actionable solutions.
By the end of this guide, you'll have a fully functional reverse proxy that centralizes access to all your homelab services, complete with HTTPS and fine-grained access control. The entire process takes about 30 minutes, and every command is copy-paste ready.
Prerequisites
Before you begin, ensure your homelab server meets the following minimum requirements. NPM is lightweight, but your underlying services will consume more resources.
| Component | Minimum Requirement | Recommended |
|---|---|---|
| CPU | 1 core | 2+ cores |
| RAM | 512 MB free | 1 GB free |
| Storage | 2 GB free | 10 GB free (for logs and backups) |
| OS | Ubuntu 20.04+, Debian 11+, or any Linux with Docker | Same as minimum |
| Docker Engine | 20.10+ | Latest stable |
| Docker Compose | v2 (plugin or standalone) | Latest stable |
| Network | Open ports 80 and 443 (for SSL) | Static IP or domain name |
Software prerequisites:
- Docker and Docker Compose installed. If not, run:
curl -fsSL https://get.docker.com | sh sudo usermod -aG docker $USER newgrp docker - A domain name (or subdomain) pointing to your server's public IP if you want internet-facing SSL. For local-only, you can use self-signed or skip SSL.
- Basic familiarity with the command line and Docker concepts.
Step-by-Step Installation
Step 1: Create a Dedicated Directory
Create a directory to hold NPM's configuration and data. Using a dedicated path makes backups and upgrades easier.
mkdir -p ~/nginx-proxy-manager
cd ~/nginx-proxy-manager
Step 2: Create Docker Compose File
Create a docker-compose.yml file with the following content. This file defines the NPM service, its ports, and persistent volumes.
version: '3.8'
services:
npm:
image: 'jc21/nginx-proxy-manager:latest'
container_name: npm
restart: unless-stopped
ports:
- '80:80' # HTTP
- '443:443' # HTTPS
- '81:81' # Admin UI
environment:
- DB_MYSQL_HOST=db
- DB_MYSQL_PORT=3306
- DB_MYSQL_USER=npm
- DB_MYSQL_PASSWORD=npm_secret_change_this
- DB_MYSQL_NAME=npm
volumes:
- ./data:/data
- ./letsencrypt:/etc/letsencrypt
networks:
- npm_network
depends_on:
- db
db:
image: 'mariadb:10.6'
container_name: npm-db
restart: unless-stopped
environment:
- MYSQL_ROOT_PASSWORD=root_secret_change_this
- MYSQL_DATABASE=npm
- MYSQL_USER=npm
- MYSQL_PASSWORD=npm_secret_change_this
volumes:
- ./mysql:/var/lib/mysql
networks:
- npm_network
networks:
npm_network:
driver: bridge
Important: Change the passwords in the environment variables to strong unique values. The data and letsencrypt folders will store NPM's configuration and SSL certificates, respectively.
Step 3: Start the Stack
Launch the containers in detached mode. This will pull the images and start the services.
docker compose up -d
Verify that both containers are running:
docker ps
You should see npm and npm-db in the list, both with status Up.
Step 4: Access the Admin UI
Open your browser and go to http://your-server-ip:81. The default login credentials are:
- Email:
admin@example.com - Password:
changeme
You will be forced to change the password on first login. Use a strong password and store it in a password manager.
Step 5: Change Default Admin Credentials
After logging in, go to Settings > Administration and change the email and password. Also, update the default user's name to something identifiable.
Step 6: Configure Your First Proxy Host
Suppose you have a service running on port 3000 (e.g., Grafana). To proxy it:
- In the NPM admin UI, go to Hosts > Proxy Hosts.
- Click Add Proxy Host.
- In the Details tab:
- Domain Names: Enter
grafana.yourdomain.com(orlocalhostif testing locally). - Scheme:
http - Forward Hostname / IP:
192.168.1.100(or the container name if on the same Docker network, e.g.,grafana). - Forward Port:
3000 - Cache Assets: Optional, enable for static files.
- Block Common Exploits: Recommended to enable.
- Websockets Support: Enable if the service uses WebSockets (e.g., Jupyter).
- Domain Names: Enter
- Click Save.
Step 7: Enable SSL with Let's Encrypt
To secure your proxy host with a valid SSL certificate:
- Go to Hosts > Proxy Hosts and click the Edit icon on your newly created host.
- Switch to the SSL tab.
- Select Request a new SSL Certificate.
- Check Force SSL and HTTP/2 Support.
- Enter your email address for Let's Encrypt notifications.
- Click Save. NPM will automatically obtain and renew the certificate.
If you are using a local domain or IP, you can use the Self-Signed option, but browsers will show warnings.
Step 8: Set Up Access Lists (Optional)
To restrict access to certain services, create an access list:
- Go to Access Lists > Add Access List.
- Give it a name (e.g.,
home-only). - Under Access Rules, click Add Rule.
- Choose Allow or Deny, and enter an IP range or a specific IP (e.g.,
192.168.1.0/24). - Save the list.
- Edit your proxy host and assign this access list under the Access List dropdown in the Details tab.
Step 9: Test the Proxy
Open your browser and navigate to https://grafana.yourdomain.com (or http://localhost:81 if you used localhost). You should see your service. If not, check the logs in NPM by running:
docker logs npm
Step 10: Verify SSL Renewal
Let's Encrypt certificates are valid for 90 days. NPM handles renewal automatically, but you can verify by checking the certificate expiry in the UI under Hosts > Proxy Hosts > Edit > SSL. The certificate details show the expiry date.
Step 11: Backup Configuration
Regularly back up the data and letsencrypt folders. You can also use the built-in backup feature in NPM (Settings > Backup) to download a JSON backup. For a full backup, stop the stack and copy the folders:
docker compose down
cp -r ./data ./backups/data_$(date +%Y%m%d)
cp -r ./letsencrypt ./backups/letsencrypt_$(date +%Y%m%d)
docker compose up -d
Step 12: Update NPM
To update to the latest version, simply pull the new image and recreate the containers:
docker compose pull
docker compose up -d
This preserves your configuration and certificates.
Advanced Configuration and Optimization
Reverse Proxy for Docker Containers on the Same Network
If your services are running in Docker on the same network as NPM, you can use the container name as the forward hostname instead of an IP. For example, if your Grafana container is named grafana, set Forward Hostname / IP to grafana. This eliminates the need for port mapping on the host.
To do this, ensure your NPM container and the service container are on the same Docker network. You can add your service to the npm_network by modifying its docker-compose.yml:
services:
grafana:
networks:
- npm_network
networks:
npm_network:
external: true
Then, restart the service.
SSL Hardening
In the SSL tab of a proxy host, you can enable HTTP/2 and Force SSL. Additionally, you can set a custom HSTS policy by adding a custom header in the Advanced tab:
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
This forces browsers to always use HTTPS.
Security Hardening for NPM Itself
- Change the default admin port (81) to a non-standard port, e.g.,
8181, in the Docker Compose file:'8181:81'. Then access the UI viahttp://your-server-ip:8181. - Use a firewall to restrict access to port 81/8181 to your local network only. For example, with UFW:
sudo ufw allow from 192.168.1.0/24 to any port 8181 proto tcp sudo ufw deny 8181 - Enable 2FA in NPM's settings (Settings > Two-Factor Authentication) for admin accounts.
Rate Limiting and Protection
NPM includes basic protection like Block Common Exploits. You can also add custom Nginx locations to block specific paths or set rate limits. For example, to limit requests to /api to 10 per second, add a custom location in the Advanced tab:
location /api {
limit_req zone=mylimit burst=10 nodelay;
proxy_pass http://your-backend:3000;
}
Using a Custom SSL Certificate
If you have a wildcard certificate from a different CA, you can upload it in NPM under Settings > SSL Certificates > Add SSL Certificate > Custom. You'll need the certificate, key, and optionally the chain. Then assign it to a proxy host in the SSL tab.
Troubleshooting Common Issues
| Error | Cause | Solution |
|---|---|---|
Error: listen EADDRINUSE: address already in use :::80 |
Port 80 or 443 is already used by another service (e.g., Apache). | Stop the conflicting service or change NPM's port mapping in docker-compose.yml (e.g., '8080:80'), but note that SSL will not work on port 80. |
502 Bad Gateway when accessing a service |
The forward hostname/IP or port is incorrect, or the backend service is not reachable from the NPM container. | Verify the service is running and accessible from the NPM container: docker exec npm curl http://service-name:port. Check the service's network configuration. |
403 Forbidden after enabling access list |
Your IP is not in the allowed range. | Edit the access list and add your current IP (check with curl ifconfig.me). |
| Let's Encrypt validation fails | Domain does not point to your server's public IP, or port 80 is not reachable from the internet. | Ensure your DNS A record points to your server's IP. Check firewall rules allow inbound TCP/80 and TCP/443. |
| Database connection error on NPM startup | The MariaDB container is not ready or credentials mismatch. | Check the docker-compose.yml environment variables. Restart the stack with docker compose restart. If the database is corrupted, delete the ./mysql folder and recreate it (but this will lose your NPM config). |
| Admin UI not loading | Port 81 is blocked or the container is not running. | Check docker ps and access http://server-ip:81. If using a firewall, allow port 81 from your IP. |
Conclusion
Nginx Proxy Manager is an essential tool for any homelab that runs multiple web services. With this guide, you've set up a secure reverse proxy with automatic SSL, access controls, and a user-friendly interface. The Docker Compose setup ensures persistence and easy updates, while the troubleshooting table helps you resolve common issues quickly.
Remember to regularly back up your configuration and monitor your SSL certificates. As you add more services, you can simply create new proxy hosts in the UI without touching any configuration files. This setup will save you hours of manual Nginx editing and keep your homelab secure and accessible.
FAQ
1. Can I use NPM with Docker containers on the same host without exposing their ports?
Yes. Put both NPM and your service containers on the same Docker network (e.g., npm_network). Then use the container name as the forward hostname in NPM. This way, the service's port is not exposed to the host, reducing attack surface.
2. How do I renew Let's Encrypt certificates automatically? NPM handles renewal automatically for certificates it issued. It checks for renewal daily and renews certificates that are within 30 days of expiry. You don't need to do anything, but ensure port 80 is always reachable for the HTTP-01 challenge.
3. Is it safe to expose NPM's admin UI to the internet? No. The admin UI should only be accessible from your local network or via a VPN. Change the default port and restrict access with a firewall. If you must access it remotely, use a secure tunnel like WireGuard or Tailscale.
4. Can I use NPM to proxy non-HTTP services like SSH?
NPM is designed for HTTP/HTTPS traffic. For SSH or other TCP protocols, you need a different solution like nginx stream or a dedicated proxy like frp. NPM does not support raw TCP proxying.
5. What happens if I stop the MariaDB container?
If the database container stops, NPM will lose connection to its backend and may show errors or fail to start. NPM's configuration is stored in the database, so it's critical to keep the database running. Use restart: unless-stopped and monitor both containers with health checks.