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
certbotcron 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:
- Email:
[email protected] - Password:
changeme
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:
- Navigate to SSL Certificates > Add SSL Certificate > Let’s Encrypt.
- In Domain Names, add both your base domain and the wildcard entry:
example.comand*.example.com. - Toggle Use a DNS Challenge on.
- 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
- 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:
- Navigate to Hosts > Proxy Hosts > Add Proxy Host.
- 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).
- Domain Names:
- 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.
- 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.

