How to Set Up Nginx Proxy Manager on Docker for HomeLab SSL and Routing

HomeLab tutorial - IT technology blog
HomeLab tutorial - IT technology blog

Reverse Proxy Approaches for Self-Hosted Infrastructure

When you start running multiple web interfaces on your local server—dashboards, media servers, or automation engines—remembering port numbers like :8096, :8123, or :9000 quickly turns messy. Exposing these services securely with proper encryption requires a reverse proxy layer sitting at the entry point of your network.

Over the years running various environments, I usually see homelabbers take one of three distinct routes:

  • Manual Web Server Configuration (Bare-metal Nginx / Apache): Writing server blocks and virtual hosts by hand inside text configuration files, pairing them with standalone certbot cron triggers.
  • Enterprise Edge Proxies (Traefik / Caddy): Leveraging dynamic container discovery via Docker labels or minimalist auto-HTTPS configuration files.
  • GUI-Managed Reverse Proxies (Nginx Proxy Manager): Running a pre-packaged web interface backed by Nginx and SQLite/MariaDB, handling Let’s Encrypt validation, proxy hosts, and access lists visually.

Weighing the Options: Pros and Cons

1. Manual Nginx Configuration

Directly managing /etc/nginx/sites-available/ gives you granular control over every buffer size, header injection, and upstream keepalive setting. However, maintaining dozens of configuration blocks by hand introduces human error. Renewing certificates across multiple split-DNS internal subdomains often leads to broken configs when certbot hooks fail.

2. Traefik and Label-Driven Proxies

Traefik automatically picks up new containers as soon as they boot up via Docker socket polling. It removes manual host creation, but debugging label typos or syntax changes between major Traefik releases can take hours of log parsing. For a homelab where half your services might run on dedicated VMs or Raspberry Pis outside the primary Docker daemon, label discovery loses much of its simplicity.

3. Nginx Proxy Manager (NPM)

Nginx Proxy Manager strikes a pragmatic middle ground. You get the stability of standard Nginx underneath, but with a clean administration interface to issue wildcard SSL certificates, build access lists, and toggle WebSocket support with a single click. In my real-world experience, this is one of the essential skills to master when you want your self-hosted ecosystem to stay resilient without becoming a full-time system administration burden.

The Recommended HomeLab Architecture

To avoid single-point routing failures and container dependency issues, here is the architectural setup I recommend:

  • Isolated Bridge Network: Create an external Docker network (for instance, proxy-network). Any container you want to publish joins this network, allowing Nginx Proxy Manager to reference target containers by their container name rather than fluctuating host IP addresses.
  • Decoupled Database: While NPM can run with embedded SQLite, deploying it alongside MariaDB prevents database lockups and corrupted state files during unexpected host power losses.
  • DNS Challenge for Wildcard SSL: Instead of opening port 80 to the public internet for HTTP-01 ACME challenges, configure DNS-01 verification with your DNS provider (such as Cloudflare). This issues valid Let’s Encrypt wildcard certificates (*.yourdomain.com) entirely inside your LAN without exposing internal management ports to the world.

Implementation Guide: Deploying Nginx Proxy Manager

Step 1: Prepare the Dedicated Docker Network

Before launching the stack, create an external network. This allows other independent Docker compose stacks to attach to the proxy cleanly without co-locating all your services into one giant file.

docker network create proxy-network

Step 2: Create the Docker Compose Configuration

Create a directory on your host (e.g., ~/homelab/npm) and define the deployment file:

mkdir -p ~/homelab/npm && cd ~/homelab/npm
nano docker-compose.yml

Paste the following production-tested compose configuration:

version: "3.8"

services:
  app:
    image: "jc21/nginx-proxy-manager:latest"
    container_name: "nginx-proxy-manager"
    restart: unless-stopped
    ports:
      - "80:80"      # Public HTTP Traffic
      - "81:81"      # NPM Admin Web UI
      - "443:443"    # Public HTTPS Traffic
    environment:
      DB_MYSQL_HOST: "npm-db"
      DB_MYSQL_PORT: 3306
      DB_MYSQL_USER: "npm_user"
      DB_MYSQL_PASSWORD: "StrongDbSecretPasswordHere"
      DB_MYSQL_NAME: "npm_database"
    volumes:
      - ./data:/data
      - ./letsencrypt:/etc/letsencrypt
    networks:
      - proxy-network
      - npm-internal
    depends_on:
      - db

  db:
    image: "jc21/mariadb-aria:latest"
    container_name: "npm-db"
    restart: unless-stopped
    environment:
      MYSQL_ROOT_PASSWORD: "SuperRootSecretPasswordHere"
      MYSQL_DATABASE: "npm_database"
      MYSQL_USER: "npm_user"
      MYSQL_PASSWORD: "StrongDbSecretPasswordHere"
    volumes:
      - ./mysql:/var/lib/mysql
    networks:
      - npm-internal

networks:
  proxy-network:
    external: true
  npm-internal:
    driver: bridge

Bring the containers up in detached mode:

docker compose up -d

Step 3: Initial Setup and Hardening

Once both containers show healthy status, access the web panel by navigating to your host’s local IP on port 81 (for example, http://192.168.1.100:81).

Use the default credentials to log in:

The panel will immediately prompt you to change the administrator name, email, and password. Complete this step first.

Step 4: Automating SSL Certificates via DNS-01 Challenge

Rather than dealing with renewals failing because port 80 was firewalled, request a wildcard certificate using DNS validation:

  1. Navigate to SSL Certificates > Add SSL Certificate > Let’s Encrypt.
  2. In Domain Names, add both your base domain and the wildcard entry: example.com and *.example.com.
  3. Toggle Use a DNS Challenge on.
  4. Select your DNS provider (for example, Cloudflare) and paste your scoped DNS API token into the credentials box:
dns_cloudflare_api_token = your_scoped_api_token_here
  1. Agree to the Let’s Encrypt Terms of Service and click Save. NPM requests the certificate, verifies ownership via temporary TXT records, and schedules auto-renewal background jobs.

Step 5: Adding Your First Proxy Host

Now connect a sample application running on your server. If you have an application container attached to proxy-network named uptime-kuma listening on port 3001, follow these steps:

  1. Navigate to Hosts > Proxy Hosts > Add Proxy Host.
  2. Fill in the Details tab:
    • Domain Names: status.example.com
    • Scheme: http
    • Forward Hostname / IP: uptime-kuma (Docker’s built-in DNS handles container name resolution).
    • Forward Port: 3001
    • Enable Block Common Exploits and Websockets Support (mandatory for apps relying on real-time sockets).
  3. Switch to the SSL tab:
    • Select your wildcard certificate from the dropdown.
    • Check Force SSL and HTTP/2 Support.
    • Check HSTS Enabled to prevent protocol downgrade attacks.
  4. Click Save.

Hands-on Tips from the Field

  • Keep Port 81 Off the Public Internet: Never port-forward port 81 on your edge router. Keep the administration interface accessible only inside your local LAN or via a private WireGuard/Tailscale mesh.
  • Custom Nginx Tweaks: If you run services requiring large file uploads, navigate to the host’s Advanced tab in NPM and add:
client_max_body_size 512M;
proxy_read_timeout 600s;
proxy_send_timeout 600s;

With this setup running, every new service you add to your homelab gets an authenticated, SSL-encrypted domain in under thirty seconds, leaving you free to focus on building projects instead of wrestling with web server configs.

Share: