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

  1. Log in to the NPM Admin UI and navigate to Hosts -> Proxy Hosts -> Add Proxy Host.
  2. Domain Names: Enter blog.example.com.
  3. Scheme: http.
  4. Forward Hostname/IP: Set this to target-app (the exact service name defined in the target application's docker-compose file).
  5. Forward Port: Set this to the internal container port (for Ghost, this is 2368).
  6. 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)

  1. Under the SSL tab of your Proxy Host config, select Request a new SSL Certificate from the dropdown menu.
  2. Enable Force SSL and HTTP/2 Support.
  3. Enter a valid email address for Let's Encrypt expiration notifications.
  4. 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.

  1. Navigate to SSL Certificates -> Add SSL Certificate -> Let's Encrypt.
  2. Enter Domain Names: example.com and *.example.com.
  3. Enable Use a DNS Challenge.
  4. Select your DNS Provider (e.g., Cloudflare).
  5. In the configuration text area, paste your API token details: ini dns_cloudflare_api_token = your_cloudflare_api_token_here
  6. 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;