Nginx Proxy Manager Setup: Free SSL and Reverse Proxy Made Easy
Deploy Nginx Proxy Manager using Docker Compose. Manage reverse proxies, custom redirects, and automated Let's Encrypt SSL certificates with ease.
VPS Environment Setup and Port Allocation
Before deploying Nginx Proxy Manager (NPM), your host VPS must be prepared. This setup assumes a Debian/Ubuntu-based distribution, though the Docker commands translate universally.
Ensure the system is updated and the necessary dependencies are installed:
sudo apt update && sudo apt upgrade -y
sudo apt install -y curl git ufw
Nginx Proxy Manager requires binding to three host ports: * Port 80: For HTTP traffic and Let's Encrypt HTTP-01 challenge verification. * Port 443: For secure HTTPS traffic. * Port 81: For the NPM Web Administration Interface.
Configure your firewall (ufw) to permit traffic on these ports:
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw allow 81/tcp
sudo ufw enable
[!WARNING] Ensure that port 81 is not publicly exposed on production systems. It is highly recommended to bind port 81 to 127.0.0.1 and access it via an SSH tunnel or restrict access using UFW to specific IP addresses.To bind the administration interface to localhost only, modify the port mapping in the compose file to 127.0.0.1:81:81.
The Docker Compose Configuration
Create a dedicated directory to house the NPM deployment configuration:
mkdir -p ~/nginx-proxy-manager
cd ~/nginx-proxy-manager
Deploying NPM using a relational database (MariaDB) is superior to using SQLite for production workloads, as it handles concurrent database transactions and backups more efficiently. Save the following content as docker-compose.yml:
version: '3.8'
services:
app:
image: 'jc21/nginx-proxy-manager:latest'
restart: unless-stopped
ports:
- '80:80' # Public HTTP Port
- '443:443' # Public HTTPS Port
- '81:81' # Admin Web Port (Consider changing to '127.0.0.1:81:81' for security)
environment:
# Database connection configuration
DB_MYSQL_HOST: "db"
DB_MYSQL_PORT: 3306
DB_MYSQL_USER: "npm"
DB_MYSQL_PASSWORD: "npm_secure_db_password"
DB_MYSQL_NAME: "npm"
volumes:
- ./data:/data
- ./letsencrypt:/etc/letsencrypt
depends_on:
db:
condition: service_healthy
networks:
- npm-network
db:
image: 'jc21/mariadb-aria:latest'
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD: "npm_root_secure_password"
MYSQL_DATABASE: "npm"
MYSQL_USER: "npm"
MYSQL_PASSWORD: "npm_secure_db_password"
volumes:
- ./mysql:/var/lib/mysql
healthcheck:
test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
interval: 10s
timeout: 5s
retries: 5
networks:
- npm-network
networks:
npm-network:
name: npm-network
driver: bridge
Execute the stack in detached mode:
docker compose up -d
Verify that both containers are running and healthy:
docker compose ps
Initial Administration Interface Setup
Once running, navigate to http://<your-vps-ip>:81. Log in using the default administrative credentials:
- Email:
admin@example.com - Password:
changeme
Immediately upon login, NPM will force you to update the administrator email and password. Utilize a cryptographically secure password manager to generate these credentials.
Configuring Reverse Proxy and Docker Networks
To route traffic to other Dockerized services on the same VPS without exposing their ports to the public internet, place them on the same bridge network as Nginx Proxy Manager.
1. Define the External Network in the Target Service
When launching a container (e.g., a Ghost blog or a Node.js API), reference the pre-existing network npm-network in its docker-compose.yml:
version: '3.8'
services:
target-app:
image: ghost:latest
restart: always
environment:
url: https://blog.example.com
networks:
- npm-network
networks:
npm-network:
external: true
2. Configure the Proxy Host in NPM Admin UI
- Log in to the NPM Admin UI and navigate to Hosts -> Proxy Hosts -> Add Proxy Host.
- Domain Names: Enter
blog.example.com. - Scheme:
http. - Forward Hostname/IP: Set this to
target-app(the exact service name defined in the target application's docker-compose file). - Forward Port: Set this to the internal container port (for Ghost, this is
2368). - Enable Block Common Exploits and Websockets Support if required by the application.
Let's Encrypt SSL Configuration
NPM automates SSL certificate issuance and renewal via Let's Encrypt.
HTTP-01 Challenge (Standard)
- Under the SSL tab of your Proxy Host config, select Request a new SSL Certificate from the dropdown menu.
- Enable Force SSL and HTTP/2 Support.
- Enter a valid email address for Let's Encrypt expiration notifications.
- Agree to the Terms of Service and click Save.
DNS-01 Challenge (For Wildcard Certificates)
To secure *.example.com using a wildcard certificate, you must use a DNS-01 challenge. This allows SSL generation without exposing port 80/443.
- Navigate to SSL Certificates -> Add SSL Certificate -> Let's Encrypt.
- Enter Domain Names:
example.comand*.example.com. - Enable Use a DNS Challenge.
- Select your DNS Provider (e.g., Cloudflare).
- In the configuration text area, paste your API token details:
ini dns_cloudflare_api_token = your_cloudflare_api_token_here - Click Save. NPM will verify ownership via TXT records automatically.
Security Hardening and Custom Configurations
1. Global Security Headers
To mitigate Cross-Site Scripting (XSS) and Clickjacking, add security headers to your hosts. In the Advanced tab of your Proxy Host configuration, paste the following Nginx directive:
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Content-Security-Policy "upgrade-insecure-requests" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
2. Increase Client Upload Limit
By default, Nginx limits file uploads to 1MB. If you run a file sharing app or blog, raise this threshold in the Advanced tab:
client_max_body_size 100M;
3. Rate Limiting Direct IPs
To prevent brute-force attacks on your proxied services, add basic request limiting:
limit_req_zone $binary_remote_addr zone=ddos_limit:10m rate=10r/s;
limit_req zone=ddos_limit burst=20 nodelay;