← All articles
NETWORKING Nginx Proxy Manager: The Easy Reverse Proxy for Your... 2026-02-09 · nginx · reverse-proxy · ssl

Nginx Proxy Manager: The Easy Reverse Proxy for Your Home Lab

Networking 2026-02-09 nginx reverse-proxy ssl networking

Once you're running more than a couple of services in your home lab, you hit an annoying problem: everything is accessed by IP address and port number. Jellyfin is at 192.168.1.50:8096, Grafana is at 192.168.1.50:3000, Nextcloud is at 192.168.1.50:8081. Nobody wants to remember port numbers, and you definitely don't want to expose multiple ports to the internet.

A reverse proxy solves this by sitting in front of all your services and routing traffic based on hostname. You access jellyfin.home.example.com, and the reverse proxy forwards the request to the right service on the right port. One entry point, clean hostnames, and the ability to add SSL certificates so everything is encrypted.

Nginx Proxy Manager (NPM) puts a user-friendly web UI on top of Nginx, one of the most battle-tested web servers in existence. You get the power and reliability of Nginx without ever touching a configuration file. If you want a reverse proxy that just works and doesn't require you to learn a domain-specific configuration language, NPM is the answer.

Why a Reverse Proxy?

Before diving into setup, here's what a reverse proxy actually gives you:

Single port exposure. Instead of opening ports 3000, 8080, 8096, 8443, 9000, and a dozen others on your firewall, you expose port 80 and 443. The reverse proxy handles everything behind those two ports.

Hostname-based routing. Access services by name instead of IP:port. grafana.home.lab, jellyfin.home.lab, nextcloud.home.lab — each resolves to the same IP but routes to a different backend.

SSL everywhere. NPM integrates with Let's Encrypt for automatic, free SSL certificates. Every service gets HTTPS with zero effort after initial setup.

Access control. Block services from external access, require authentication for sensitive dashboards, or restrict access by IP range — all configurable through the web UI.

Centralized logging. Every request flows through the proxy, giving you a single place to monitor traffic and debug issues.

Installing Nginx Proxy Manager

NPM runs in Docker. Create a directory and a compose file:

mkdir -p ~/docker/nginx-proxy-manager
cd ~/docker/nginx-proxy-manager
# docker-compose.yml
services:
  npm:
    image: jc21/nginx-proxy-manager:latest
    container_name: nginx-proxy-manager
    restart: unless-stopped
    ports:
      - "80:80"      # HTTP
      - "443:443"    # HTTPS
      - "81:81"      # Admin UI
    volumes:
      - ./data:/data
      - ./letsencrypt:/etc/letsencrypt
    environment:
      TZ: "America/New_York"

Start it:

docker compose up -d

Wait about 30 seconds, then access the admin UI at http://YOUR_SERVER_IP:81.

Default Login

You'll be prompted to change these immediately on first login. Do it. Set a real email address and a strong password.

DNS Setup

Before NPM can route traffic by hostname, those hostnames need to resolve to your server's IP. You have several options:

Option 1: Local DNS (Pi-hole, AdGuard Home, or Router)

If you run Pi-hole or AdGuard Home, add local DNS records:

grafana.home.lab    -> 192.168.1.50
jellyfin.home.lab   -> 192.168.1.50
nextcloud.home.lab  -> 192.168.1.50

All hostnames point to the same IP — the server running NPM. NPM differentiates based on the hostname in the HTTP request.

Option 2: Wildcard DNS

Some DNS servers let you create a wildcard record. In your router or DNS server:

*.home.lab -> 192.168.1.50

Now any subdomain of home.lab resolves to your server. This is the most convenient approach.

Option 3: /etc/hosts (Quick and Dirty)

On each client machine, edit /etc/hosts:

192.168.1.50  grafana.home.lab jellyfin.home.lab nextcloud.home.lab

This works but doesn't scale. You'll need to update every client when you add a service. Fine for testing, bad for production.

Option 4: Real Domain with Split DNS

If you own a domain (e.g., example.com), create A records or a wildcard in your public DNS pointing to your server's local IP:

*.home.example.com -> 192.168.1.50

This works for devices on your local network. For external access, point to your public IP and configure port forwarding on your router.

Adding Your First Proxy Host

From the NPM dashboard, click Hosts > Proxy Hosts > Add Proxy Host.

Fill in:

Click Save. That's it. Visit http://grafana.home.lab in your browser, and you'll see Grafana.

If Your Backend Is Also in Docker

When NPM and the target service are on the same Docker host, you can use the container name instead of the IP — but only if they're on the same Docker network.

# Create a shared network
docker network create proxy-network

Add the network to both NPM and the target service:

# In NPM's docker-compose.yml
services:
  npm:
    # ... existing config ...
    networks:
      - proxy-network
      - default

networks:
  proxy-network:
    external: true
# In Grafana's docker-compose.yml
services:
  grafana:
    # ... existing config ...
    networks:
      - proxy-network

networks:
  proxy-network:
    external: true

Now in NPM, set the forward hostname to the container name (e.g., grafana) and the forward port to the container's internal port (e.g., 3000). No need for ports mapping on the backend service at all — NPM connects directly via the Docker network.

This is the preferred setup. Backend services don't expose any ports to the host, and all traffic flows through NPM.

Automatic SSL with Let's Encrypt

NPM makes SSL trivially easy. When adding or editing a proxy host, click the SSL tab:

  1. SSL Certificate: Select "Request a New SSL Certificate"
  2. Force SSL: Enable (redirects HTTP to HTTPS)
  3. HTTP/2 Support: Enable
  4. HSTS Enabled: Enable (tells browsers to always use HTTPS)
  5. Email Address: Your email for Let's Encrypt notifications
  6. Check "I Agree to the Let's Encrypt Terms"

Click Save, and NPM requests a certificate from Let's Encrypt automatically. Certificates are valid for 90 days and auto-renew.

Requirements for Let's Encrypt

Let's Encrypt validates domain ownership via HTTP-01 or DNS-01 challenges.

HTTP-01 (default): Requires that port 80 on your public IP reaches NPM. This means:

DNS-01: Validates via a DNS TXT record. NPM supports this for several DNS providers (Cloudflare, DigitalOcean, AWS Route 53, and others). This is the better option because:

To use DNS-01 with Cloudflare:

  1. In NPM, go to SSL Certificates > Add SSL Certificate > Let's Encrypt
  2. Enter *.home.example.com as the domain
  3. Check "Use a DNS Challenge"
  4. Select Cloudflare as the provider
  5. Enter your Cloudflare API token (create one with Zone:DNS:Edit permissions)

NPM creates the validation TXT record automatically and retrieves the wildcard certificate. Apply this certificate to any proxy host under home.example.com.

Self-Signed Certificates (Internal Only)

For purely internal services where you don't have a public domain, NPM can generate self-signed certificates. Your browser will warn about them, but the connection will still be encrypted.

Under the SSL tab, select "Request a New SSL Certificate" with the self-signed option. Or upload your own certificates from a private CA if you've set one up.

Access Lists

Access lists control who can reach a service. Useful for restricting admin panels, development services, or anything you don't want publicly accessible.

Go to Access Lists > Add Access List:

Or create a password-protected access list:

Apply access lists to proxy hosts from the host's settings. Multiple proxy hosts can share the same access list.

Practical Access List Examples

Admin services (local only):

allow 192.168.1.0/24
allow 10.0.0.0/8
deny all

VPN users + local:

allow 192.168.1.0/24
allow 100.64.0.0/10  # Tailscale range
deny all

Everyone, but with authentication: Create an authorization entry with username/password. Useful for services that don't have their own auth.

Custom Locations and Advanced Configuration

Sometimes you need more than simple proxying. NPM supports custom locations and advanced Nginx configuration.

Custom Locations

In a proxy host's settings, click the Custom Locations tab. Add a location:

This routes /api/* requests to a different backend than the main path. Useful for microservice architectures or splitting traffic.

Advanced Configuration

The Advanced tab lets you inject raw Nginx directives. This is where you go for anything NPM's UI doesn't cover:

# Increase upload size limit (default is 1MB)
client_max_body_size 100M;

# Custom headers
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

# WebSocket timeout adjustment
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;

# Cache static assets
location ~* \.(jpg|jpeg|png|gif|ico|css|js)$ {
    expires 7d;
    add_header Cache-Control "public, no-transform";
}

Common reasons to use the advanced tab:

NPM vs. Traefik vs. Caddy

NPM isn't the only reverse proxy option for home labs. Here's how it compares to the two other popular choices.

Traefik

Traefik is a cloud-native reverse proxy that integrates deeply with Docker and Kubernetes. Instead of configuring routes in a UI, you add labels to your Docker containers and Traefik auto-discovers them.

Traefik is better if:

NPM is better if:

Traefik's learning curve is steeper. Its configuration system (providers, routers, services, middlewares, entrypoints) is powerful but verbose. When it works, it's elegant. When something breaks, debugging is harder than NPM's straightforward approach.

Caddy

Caddy is a modern web server that handles HTTPS automatically. Its configuration file (Caddyfile) is remarkably simple:

grafana.home.lab {
    reverse_proxy localhost:3000
}

jellyfin.home.lab {
    reverse_proxy localhost:8096
}

That's the entire config for two services, including automatic HTTPS.

Caddy is better if:

NPM is better if:

The Honest Recommendation

If you're comfortable with the command line and prefer clean config files, Caddy is the best home lab reverse proxy. Its Caddyfile syntax is a joy.

If you want a GUI and point-and-click simplicity, NPM is the right choice. It's especially good for home labs that mix Docker and non-Docker services.

If you're running Kubernetes or want deep Docker integration with zero manual route configuration, Traefik wins.

All three are excellent. You won't regret any of them. NPM is the one most people start with because the learning curve is the flattest.

Monitoring and Maintenance

Checking Logs

NPM stores access and error logs that you can view from the dashboard. For more detail:

# View NPM container logs
docker logs nginx-proxy-manager -f --tail 100

# Access Nginx logs inside the container
docker exec nginx-proxy-manager cat /data/logs/fallback_access.log

Updating NPM

cd ~/docker/nginx-proxy-manager
docker compose pull
docker compose up -d

NPM's data persists in the ./data and ./letsencrypt volumes, so updates are non-destructive.

Backing Up NPM

The entire NPM state lives in the ./data directory (SQLite database and Nginx configs) and ./letsencrypt (SSL certificates):

# Backup
tar czf npm-backup-$(date +%Y%m%d).tar.gz data/ letsencrypt/

# Restore
tar xzf npm-backup-20260209.tar.gz
docker compose up -d

Common Issues

Port 80 or 443 already in use. If another service is using these ports, NPM can't start. Check what's occupying them:

sudo ss -tlnp | grep -E ':80|:443'

Stop the conflicting service or change its ports.

SSL certificate not renewing. Certificates auto-renew 30 days before expiration. If renewal fails, check that:

You can manually trigger renewal from the SSL Certificates page in the admin UI.

502 Bad Gateway. NPM can reach the internet but not your backend service. Check:

504 Gateway Timeout. The backend is reachable but too slow. Add to the advanced config:

proxy_connect_timeout 300s;
proxy_read_timeout 300s;
proxy_send_timeout 300s;

Putting It All Together

A typical NPM setup for a home lab looks like this:

  1. NPM runs in Docker on ports 80, 443, and 81
  2. Wildcard DNS (*.home.lab) points to the Docker host
  3. Backend services run in Docker on the same network, no ports exposed to the host
  4. SSL certificates via Let's Encrypt DNS-01 challenge (wildcard cert)
  5. Access lists restrict admin UIs to the local network
  6. External access via port forwarding 80/443 to NPM, with only specific services exposed

This gives you a clean, secure, maintainable setup where adding a new service takes about 60 seconds in the NPM UI. No config files to edit, no Nginx syntax to remember, no certificates to manage manually. It's the lowest-friction way to get professional-grade reverse proxying in a home lab.