Reading time: ~14 minutes Audience: Homelabbers who want clean domains and HTTPS for all their Docker services


Why You Need a Reverse Proxy

You’ve got 10 Docker containers running — Nextcloud on port 8080, Pi-hole on port 8180, Portainer on port 9443 — and you’re typing http://192.168.1.50:32400 to access Plex. Every service has a different port, you have no HTTPS, and your browser keeps screaming “Not Secure.” There’s a better way.

A reverse proxy sits at the edge of your network, receives all incoming requests, and routes them to the correct backend service. Instead of memorizing ports, you access cloud.yourdomain.com, admin.yourdomain.com, and photos.yourdomain.com — all on standard port 443 with valid SSL certificates.

Nginx Proxy Manager (NPM) is the most beginner-friendly reverse proxy for homelab use. It wraps the powerful Nginx engine in a clean web GUI. No configuration files to edit. No CLI flags to memorize. Let’s Encrypt integration is built-in — one click and you have HTTPS.

By the end of this guide, you’ll have:

  • A running Nginx Proxy Manager instance on Docker
  • Custom subdomains for all your services (*)
  • Auto-renewing free SSL certificates from Let’s Encrypt
  • Portainer integration for easy stack management

What Is Nginx Proxy Manager?

Overview

Nginx Proxy Manager is an open-source web interface for managing Nginx reverse proxy hosts. It was created by Jamie Curnow (jc21) and has grown to over 33,000 GitHub stars. The goal: make Nginx accessible to people who don’t want to write config files.

Instead of hand-editing nginx.conf blocks, you fill in a web form: domain name, backend IP and port, select SSL options — done. The GUI updates Nginx configuration automatically and reloads the service.

Key Features

Feature Description
GUI Proxy Host Manager Add, edit, and delete proxy hosts through a web dashboard
Automatic Let’s Encrypt SSL HTTP and DNS challenge support; wildcard certificates
Access Lists IP-based allow/deny rules for individual proxy hosts
404 Redirects Custom landing pages for unconfigured subdomains
Stream Proxies Proxy raw TCP/UDP streams (ideal for game servers, SSH)
Custom Nginx Snippets Inject raw Nginx config for advanced headers, CORS, or WebSocket
Let’s Encrypt Auto-Renewal Certificates renew automatically every 60 days
Docker Healthcheck Built-in health status endpoint

NPM vs Traefik vs Caddy: Which Reverse Proxy?

NPM isn’t the only reverse proxy for homelab use. Here’s how the three main contenders compare:

Feature Nginx Proxy Manager Traefik Caddy
GUI ✅ Full web UI ❌ Config files only ❌ Config files only
Learning curve Low High Medium
Docker auto-discovery ❌ Manual setup ✅ Native Docker label discovery ❌ Manual (or Caddy-Docker-Proxy plugin)
Let’s Encrypt Built-in (+ DNS Challenge) Built-in Built-in (automatic)
Configuration syntax Web form YAML / labels Caddyfile (simple DSL)
Stream proxy (TCP/UDP) ✅ (Caddy L4)
Best for Beginners, mixed teams, quick setup Kubernetes, Docker Swarm, auto-scaling Developers who like Caddyfile simplicity
GitHub Stars 33k+ 55k+ 60k+

When to pick NPM: You want a GUI, you’re managing a handful of services, you don’t need automatic Docker container discovery, and you want to stop editing config files.

When to pick Traefik: You run dozens of containers, want auto-discovery via Docker labels, and are comfortable with YAML configuration.

When to pick Caddy: You like a simple config language (Caddyfile), want automatic HTTPS with zero configuration, and don’t need a web GUI.

For 90% of homelabbers, Nginx Proxy Manager is the right choice. It’s fast to set up, forgiving of mistakes, and the GUI means you’ll actually remember how it works when you come back to it six months later.


Prerequisites

Hardware Requirements

  • Any x86_64 or ARM64 device running Docker (mini PC, Raspberry Pi 4/5, Proxmox LXC, or old laptop)
  • 512MB RAM minimum (1GB recommended)
  • 2GB storage for configuration and certificate data

Software Requirements

  • Docker Engine 24.x+ with Docker Compose v2+
  • Linux host (Debian 12, Ubuntu 22.04/24.04, or Proxmox LXC)
  • Firewall rules allowing ports 80 and 443 (if using HTTP challenge or public access)

Networking Requirements

  • A domain name (e.g., from Cloudflare, Namecheap, or Porkbun) — or local DNS via Pi-hole for internal-only use
  • DHCP reservation or static IP for your Docker host
  • Ports 80 and 443 forwarded on your router (if exposing services publicly) — or use Cloudflare Tunnel to skip port forwarding entirely

Step 1: Create the Docker Compose File

First, create a dedicated directory and the compose file:

mkdir -p ~/nginx-proxy-manager && cd ~/nginx-proxy-manager

Create docker-compose.yml:

version: '3.8'

services:
  nginx-proxy-manager:
    image: jc21/nginx-proxy-manager:latest
    container_name: npm
    restart: unless-stopped          # Auto-restart on crash or host reboot
    ports:
      - "80:80"                       # HTTP — Let's Encrypt HTTP challenge
      - "443:443"                     # HTTPS — secure traffic to your services
      - "81:81"                       # Admin web UI (accessible at http://host:81)
    volumes:
      - ./data:/data                  # NPM configuration (proxy hosts, SSL certs, settings)
      - ./letsencrypt:/etc/letsencrypt # Let's Encrypt certificate storage
    environment:
      - DISABLE_IPV6=true             # Set to false if you use IPv6
    networks:
      - npm_network                   # Custom bridge for inter-container communication
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:81"]
      interval: 30s
      timeout: 10s
      retries: 3

networks:
  npm_network:
    driver: bridge                    # Isolated network for NPM + your services

What each piece does:

  • jc21/nginx-proxy-manager:latest — Official upstream image from the project’s Docker Hub, maintained by the NPM creator.
  • restart: unless-stopped — Ensures NPM comes back up after a power outage or host reboot.
  • Port 81 — The admin dashboard. Change the host port if 81 is already in use (e.g., "8081:81").
  • npm_network — A custom Docker bridge network. All containers you want NPM to reach must join this same network. Without it, NPM can’t resolve container names.
  • healthcheck — Docker checks the admin UI every 30 seconds. The container status in docker ps reflects actual health, not just “running.”

⚠️ Important: If you’re running Pi-hole or another service that binds port 80, stop it first. NPM must own ports 80 and 443. Pi-hole can use alternative ports — adjust its compose file to map "8180:80" and "8143:443".


Step 2: Deploy and Log In

Start the stack:

docker compose up -d

Check that it’s running:

docker compose ps

You should see npm with status “healthy” after a few seconds.

Open your browser and navigate to http://[your-host-ip]:81. Log in with the default credentials:

Change the password immediately. Click your email address in the top-right corner → Edit Details → enter your full name, a real email address, and a strong password.

🛡️ Security tip: Change the default admin email to a real one. Let’s Encrypt sends expiration warnings to this email address. If you leave it as [email protected], you’ll never get renewal failure alerts.

Dashboard Overview

Section Purpose
Proxy Hosts Main view — add and manage domain-to-backend mappings
Redirection Hosts HTTP-to-HTTPS redirects or domain-to-other-domain redirects
Streams Raw TCP/UDP forwarding (SSH, game servers, RTMP)
404 Hosts Custom “page not found” landings for unknown subdomains
SSL Certificates Request, renew, and manage Let’s Encrypt certificates
Access Lists IP-based allow/deny rules you can attach to proxy hosts
Users Create additional admin or viewer accounts
Audit Log See who changed what and when

Step 3: Add Your First Proxy Host

Let’s expose a service at a clean subdomain. We’ll use a Nextcloud container as an example — assume it’s running on the same Docker host with container name nextcloud, listening on port 80 internally.

Make sure Nextcloud is on the same Docker network. Attach it to npm_network:

docker network connect npm_network nextcloud

Or, better, define the network in every service’s docker-compose.yml:

# In your Nextcloud's docker-compose.yml:
networks:
  npm_network:
    external: true   # Use the pre-existing NPM network

Now, in the NPM admin dashboard:

  1. Click Proxy HostsAdd Proxy Host
  2. Domain Names: cloud.yourdomain.com
  3. Scheme: http
  4. Forward Hostname / IP: nextcloud (Docker container name resolves automatically on the shared bridge network)
  5. Forward Port: 80 (the port inside the container, not the host-mapped port)
  6. Cache Assets: Leave off (Nextcloud handles its own caching)
  7. Block Common Exploits: ✅ Enable this — it’s a one-click SQL injection and path traversal guard
  8. Websockets Support: Enable if the app requires live updates (Portainer, Home Assistant, Immich, Uptime Kuma)
  9. Click Save

You can now access http://cloud.yourdomain.com and NPM routes traffic to Nextcloud. No port number needed.

Enabling WebSockets for Real-Time Apps

Some apps need WebSocket connections to function properly. If you see “Connection lost” or blank dashboards, enable Websockets Support in the Advanced tab of that proxy host entry.

App Needs WebSockets?
Portainer ✅ Yes (live container logs)
Home Assistant ✅ Yes (real-time state updates)
Uptime Kuma ✅ Yes (monitor status pulses)
Immich ✅ Yes (live photo uploads)
Grafana ✅ Yes (dashboard refresh)
Nextcloud ❌ No (standard HTTP is fine)
Pi-hole ❌ No
Plex / Jellyfin ❌ No

Custom Nginx Location Snippets

For advanced setups, the Advanced tab lets you paste raw Nginx directives. This is useful for:

# Override upload size limit (Nextcloud needs this)
client_max_body_size 10G;

# Add security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;

# Allow Home Assistant ingress
proxy_buffering off;

Step 4: Enable Free SSL with Let’s Encrypt

HTTP is fine for testing, but production services need HTTPS. Nginx Proxy Manager makes SSL certificates dead simple with two challenge options.

Option A: HTTP Challenge (Simplest, Requires Port 80 Forwarded)

  1. Go to your proxy host → SSL tab
  2. Select Request a new SSL Certificate
  3. Choose HTTP Challenge
  4. Enter your email address (for Let’s Encrypt expiry alerts)
  5. Agree to the Let’s Encrypt Terms of Service
  6. Click Save

NPM handles the ACME challenge, fetches the certificate, and auto-reloads Nginx. Your service now responds on https://cloud.yourdomain.com.

Option B: DNS Challenge (Better — Works Without Public Ports, Supports Wildcards)

DNS Challenge is the preferred method because:

  • It works even if your ISP blocks ports 80/443
  • You can request wildcard certificates (*.yourdomain.com) and use one cert for all subdomains
  • No HTTP listener needed on the public internet

Step-by-step for Cloudflare:

  1. Go to Cloudflare Dashboard → your domain → API Tokens
  2. Create a token with permissions: Zone:Read + DNS:Edit for your zone
  3. Copy the token

Back in NPM:

  1. Navigate to SSL CertificatesAdd SSL CertificateLet’s Encrypt
  2. Enter *.yourdomain.com and yourdomain.com in the Domain Names field (wildcard + apex)
  3. Select Use a DNS Challenge
  4. Choose Cloudflare as the DNS provider
  5. Paste your API token into the credentials field
  6. Agree to the Terms of Service
  7. Click Save

NPM creates a TXT record in your Cloudflare DNS, validates it with Let’s Encrypt, and stores the wildcard certificate. This process takes about 15 seconds.

Now apply it: go back to any proxy host → SSL tab → select your new *.yourdomain.com certificate from the dropdown → Save.

Force SSL (HTTPHTTPS Redirect)

In the SSL tab of each proxy host, enable Force SSL. This adds an automatic HTTP-to-HTTPS redirect so nobody accidentally uses the insecure URL.

Certificate Auto-Renewal

NPM automatically attempts renewal 30 days before expiry. Renewals use the same challenge method as the original request. If a renewal fails, NPM logs the error and retries daily. Let’s Encrypt also sends expiry warnings to the email you provided.

# Check renewal logs
docker logs npm 2>&1 | grep -i "renew"

Step 5: Integrate with Portainer (Optional)

If you run Portainer for container management, you can deploy NPM as a Portainer stack for a unified workflow.

In Portainer:

  1. Go to StacksAdd Stack
  2. Name: npm
  3. Paste the docker-compose.yml from Step 1
  4. Under Network, ensure the stack is attached to an external network (npm_network)
  5. Click Deploy the stack

Portainer now shows NPM’s container status, logs, and resource usage in one dashboard. You can restart or update NPM without touching the CLI.

For services managed via Portainer stacks, add them to the NPM network by editing their stack’s compose file:

networks:
  default:
    name: npm_network
    external: true

This ensures every service in the stack can be reached by NPM via its container name.

💡 Pro tip: Keep NPM in its own stack, separate from your application stacks. This way, you can restart or update services without affecting the reverse proxy.


Troubleshooting FAQ

Q: “502 Bad Gateway” when I try to access my service

This is the most common error and almost always means NPM can’t reach the backend container.

Checklist:

  1. Are both containers on the same Docker network? Run docker network inspect npm_network and confirm both containers are listed.
  2. Is the Forward Port correct? Use the internal container port (e.g., 80), not the host-mapped port (e.g., 8080).
  3. Is the container actually running? docker ps | grep your-container
  4. Can NPM resolve the container name? Try docker exec npm ping nextcloud — if it fails, they’re not on the same network.
  5. Use the IP address instead of container name temporarily: docker inspect -f '{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}' nextcloud

Q: My SSL certificate shows as invalid or expired

  • Check the NPM logs: docker logs npm 2>&1 | grep -i "error\|ssl\|cert"
  • For DNS challenges: verify the API token still has Zone:Read and DNS:Edit permissions
  • For HTTP challenges: verify port 80 is accessible from the internet (use canyouseeme.org)
  • Force manual renewal: in SSL Certificates → click the three-dot menu → Renew Now
  • Check the email address on your SSL certificate entry — Let’s Encrypt sends warnings to it

Q: WebSockets not working for Portainer / Home Assistant

In the proxy host’s Advanced tab, you need both the WebSockets Support toggle and sometimes additional headers:

proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 86400;

Q: Can’t access NPM admin panel on port 81

  • Check if another service is using port 81: sudo lsof -i :81
  • Verify your firewall: sudo ufw status — if UFW is active, allow port 81: sudo ufw allow 81
  • If the host firewall is iptables-based, check Docker’s rules: sudo iptables -L DOCKER
  • Try mapping a different port in docker-compose.yml: change "81:81" to "8181:81"

Q: Do I need NPM if I use Cloudflare Tunnel?

They solve different problems:

  • Cloudflare Tunnel securely exposes your services to the public internet without opening router ports. It runs as a daemon on your server and proxies traffic through Cloudflare’s edge.
  • Nginx Proxy Manager routes traffic within your LAN and provides local HTTPS with custom domains.

Real-world combo: Use Cloudflare Tunnel for public access (cloud.yourdomain.com from anywhere), and NPM for local access (cloud.home) — both pointing at the same backend containers. Best of both worlds.

Q: How do I back up my NPM configuration?

Back up two directories:

# Stop NPM first (optional but safer)
docker compose down

# Copy the data and certificate volumes
sudo cp -r ~/nginx-proxy-manager/data ~/backups/npm-data-$(date +%Y%m%d)
sudo cp -r ~/nginx-proxy-manager/letsencrypt ~/backups/npm-letsencrypt-$(date +%Y%m%d)

# Restart
docker compose up -d

Restoring is the reverse: copy the directories back and start the container. NPM picks up all proxy hosts and SSL certificates automatically.

Q: Can I use NPM for local-only services (no public domain)?

Yes. Use Pi-hole or AdGuard Home for local DNS:

  1. In Pi-hole: Local DNSDNS Records
  2. Add: cloud.home192.168.1.50 (your Docker host IP)
  3. In NPM: add a proxy host for cloud.home pointing at your container
  4. Request an SSL certificate using DNS Challenge (no public port 80 needed)

This gives you valid HTTPS for internal-only services — even on .home or .local domains — using DNS Challenge with Cloudflare or DuckDNS.


Next Steps


References


Have questions or want to share your NPM setup? Join us in the r/selfhosted community. If this guide helped you, check out our other Docker Compose tutorials — every one is copy-paste ready.