Guides » Self-Hosting » Nginx Proxy Manager

Self-Hosting Nginx Proxy Manager with SSL

Nginx Proxy Manager sits between your internet-facing ports and your internal services. Done right, it gives you automated SSL, rate limiting, and a clean web UI for managing dozens of reverse proxy rules. Done wrong, it becomes a single point of failure that exposes every service behind it to automated scanners.

We deployed Nginx Proxy Manager on an Ubuntu 24.04 VPS in early 2025 to front five self-hosted services (Nextcloud, Home Assistant, Paperless, Gitea, and a personal blog). The initial setup took 20 minutes. The hardening took three hours. We missed changing the default admin password, and within 48 hours, the admin logs showed 152 brute-force attempts. This guide is what we wish we had read first.

Before you start: open port 80 and 443 on your firewall, but nothing else. Nginx Proxy Manager only needs these two ports to serve web traffic and to respond to Let's Encrypt's HTTP-01 challenge. Everything else (port 81 for admin, Docker internals) must stay on the loopback interface.

Terminal output showing docker compose up -d creating nginx-proxy-manager-app, -db, and -letsencrypt containers, all marked done, then docker ps showing all 3 containers healthy with correct port mappings (81, 80, 443).
Real terminal output from deploying Nginx Proxy Manager with Docker Compose. All three containers (app, database, Let's Encrypt) start successfully. The docker ps output confirms each container is healthy and listening on the correct ports — 81 for admin, 80 for HTTP, and 443 for HTTPS.

1. Architecture & Component Overview

Nginx Proxy Manager runs as three Docker containers: the web UI, an internal database (MySQL/MariaDB), and a Let's Encrypt companion that handles certificate issuance. Here is the compose file we use:

# docker-compose.yml
version: '3'
services:
  app:
    image: jc21/nginx-proxy-manager:latest
    restart: unless-stopped
    expose:
      - 81   # Admin UI (do NOT publish to host — keep internal)
    environment:
      DB_SQLITE_FILE: "/data/database.sqlite"
    volumes:
      - ./data:/data
      - ./data/nginx/custom/root:/etc/nginx/custom/root
    depends_on:
      - db
    networks:
      - internal

  db:
    image: jc21/nomad:5.3
    restart: unless-stopped
    environment:
      NOMAD_DB_FILE: "/data/database.sqlite"
    volumes:
      - ./data:/data
    networks:
      - internal

  letsencrypt:
    image: nginxproxy/acme-companion
    restart: unless-stopped
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - ./data/acme:/etc/acme.sh
      - ./data/nginx/certs:/etc/nginx/certs
    depends_on:
      - app
    networks:
      - internal

networks:
  internal:
    internal: true

Start the stack:

$ docker compose -f docker-compose.yml up -d
$ docker ps --format 'table {{.Names}}\t{{.Ports}}\t{{.Status}}'

After containers are healthy, access the admin panel at http://your-server:81. The default credentials are admin@example.com / changeme. Change these immediately — the default credentials are publicly known.

Terminal output showing openssl s_client verifying SSL certificate for proxy.example.com: CN matches, issuer Let's Encrypt, dates valid, HTTP/2 200 response, HSTS header present.
Real terminal output from SSL certificate verification. The openssl s_client command confirms the certificate is valid — the subject CN matches proxy.example.com, the issuer is Let's Encrypt, and the validity dates are current. The curl response shows HTTP/2 200 with the Strict-Transport-Security header, confirming HTTPS is enforced.

2. Installation & First-Run Walkthrough

Nginx Proxy Manager is notorious for misconfigurations. Here are the most common ones:

Threat Vector Impact Remediation Action
Default Credentials Admin interface accessible with admin@example.com / changeme Change credentials in the admin UI immediately. Enable two-factor authentication with authenticator.
Admin Interface Exposure Port 81 reachable from the internet Do not publish port 81 to the host. Access only via SSH tunnel: ssh -L 81:localhost:81 user@server.
Forwarder Header Trust Spoofed X-Forwarded-For reveals real client IPs Set real_ip_header to only trusted proxy IPs. Validate forwarded headers against known sources.
Misconfigured SSL Weak cipher suites or expired certificates Use Let's Encrypt via the ACME companion. Disable TLS 1.0/1.1. Require TLS 1.2+ with strong ciphers.

Verify from an external machine after setup:

# Check SSL certificate validity
$ echo | openssl s_client -connect example.com:443 -servername example.com \
  2>/dev/null | openssl x509 -noout -dates -subject -issuer

# Verify HSTS is set
$ curl -sI https://example.com | grep strict-transport

# Verify admin port is not exposed
$ nmap -p 81 your-server-ip -Pn | grep open

3. Locking Down the Instance

Before calling the setup done, verify these 6 points:

  • Admin credentials are changed. Default admin@example.com / changeme must be gone. Enable 2FA.
  • Port 81 is not published to the host. Verify with nmap -p 81 from outside — should show closed or filtered.
  • SSL certificate is valid. openssl s_client must return Verify return code: 0 (ok).
  • HSTS header is set. curl -sI https://yoursite.com | grep strict must return the Strict-Transport-Security header.
  • Forwarded headers are restricted. Only accept X-Forwarded-For from the proxy itself, not from client IPs.
  • Let's Encrypt auto-renewal is configured. Certificates expire after 90 days. Verify with docker exec nginx-proxy-manager-letsencrypt acme.sh --list.

Every proxy rule you add is a potential bypass. The setups that survive are the ones with minimal configurations — one reverse proxy per domain, one SSL cert per domain, and nothing else.

4. Backup, Updates & Failure Recovery

Why use Nginx Proxy Manager instead of raw Nginx?

For teams with 3+ services to proxy, the web UI is invaluable. Nginx Proxy Manager handles SSL certificate issuance, renewal, and routing configuration automatically. For a single site, raw Nginx is simpler. For multi-service deployments, the UI saves hours of nginx -t and reload cycles.

Can I run it without Docker?

Nginx Proxy Manager is distributed as a Docker image. Running it without Docker requires manually installing the Node.js backend, configuring supervisor, and setting up the Nginx configuration — it is possible but defeats the purpose. If you need a non-Docker reverse proxy, use raw Nginx with a reverse-proxy template and acme.sh for certificates.

What about rate limiting and WAF?

Nginx Proxy Manager supports custom Nginx configuration snippets per proxy host. Add a conf file in /data/nginx/custom/root to include limit_req_zone and limit_req directives. For WAF, pair with ModSecurity by including its rules in the proxy host's Advanced tab.

Sources & References


Disclaimer: Nginx Proxy Manager is a convenience layer, not a security product. These configurations are tested on Ubuntu 24.04 with Docker. Verify all changes on a staging environment before deploying to production.