← All Guides
intermediate

How to Set Up Nginx Proxy Manager With Docker (Complete Guide)

Install Nginx Proxy Manager with Docker Compose, add your first proxy host, and get HTTPS working for every service on your homelab.

Budget Homelab ·
networkingssl-certificatesreverse-proxydocker

A reverse proxy is what turns http://192.168.1.10:8096 (Jellyfin, by default) into https://jellyfin.yourdomain.com. Nginx Proxy Manager handles this with a web UI, automatic SSL certificate renewal via Let’s Encrypt, and no need to edit nginx config files directly.

I run NPM at 192.168.1.100:81 as the frontend for every service in my homelab. This guide covers the full setup.

Before you start: You need Docker and Docker Compose installed. If you don’t have that yet, see the Docker setup guide. You’ll also need a domain name — a cheap one from Cloudflare Registrar works fine.

The compose file

Create a directory for NPM:

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

Create docker-compose.yml:

services:
  nginx-proxy-manager:
    image: jc21/nginx-proxy-manager:latest
    container_name: nginx-proxy-manager
    restart: unless-stopped
    ports:
      - "80:80"
      - "81:81"
      - "443:443"
    volumes:
      - ./data:/data
      - ./letsencrypt:/etc/letsencrypt
    environment:
      DISABLE_IPV6: "true"

Start it:

docker compose up -d

First login

Open http://your-server-ip:81 in a browser.

Default credentials:

You’ll be prompted to change both immediately. Do that.

Adding your domain to Cloudflare

For HTTPS to work cleanly, your domain needs to be in Cloudflare’s DNS. If it’s not already:

  1. Add your domain to Cloudflare (free plan works)
  2. Update your domain registrar’s nameservers to point to Cloudflare
  3. Wait for DNS propagation (usually a few minutes with Cloudflare)

Create an A record for your server:

Creating your first proxy host

In the NPM web UI, go to Proxy Hosts → Add Proxy Host.

Domain Names: Enter the domain or subdomain you want (e.g., jellyfin.yourdomain.com)

Scheme: http (NPM connects to your service over HTTP internally; HTTPS is handled at the NPM layer)

Forward Hostname/IP: The IP address of your server, or the container name if both services are on the same Docker network

Forward Port: The port your service runs on (e.g., 8096 for Jellyfin)

Cache Assets: Off (unless you know what you’re doing)

Block Common Exploits: On

Websockets Support: On (required for many services — safer to leave it on)

Click Save.

Adding SSL

With your proxy host created, click the three-dot menu → Edit, then go to the SSL tab.

SSL Certificate: Select “Request a new SSL Certificate”

Force SSL: On

HTTP/2 Support: On

For the certificate request to work, Let’s Encrypt needs to verify you own the domain. There are two methods:

Option 1: HTTP challenge (simpler, requires port 80 open)

If your server is publicly reachable on port 80 (i.e., your router forwards port 80 to this machine), the HTTP challenge works automatically. Let’s Encrypt sends a request to yourdomain.com/.well-known/acme-challenge/ and NPM handles the response.

This is what I use. It doesn’t require any open ports — Let’s Encrypt verifies ownership by checking for a DNS TXT record that NPM creates via the Cloudflare API. This is how you get wildcard certs too.

To use the DNS challenge:

  1. In Cloudflare, go to My Profile → API Tokens → Create Token
  2. Use the “Edit zone DNS” template
  3. Set Zone Resources to your specific domain
  4. Copy the token

Back in NPM’s SSL tab, check Use a DNS Challenge, select Cloudflare as the provider, and paste your API token.

NPM will request the certificate, create a temporary DNS record to prove ownership, and retrieve the cert. The whole process takes about 30 seconds.

Wildcard certificates

If you’re running multiple subdomains (mealie.yourdomain.com, paperless.yourdomain.com, npm.yourdomain.com), a wildcard cert covers all of them with one certificate.

In NPM, go to SSL Certificates → Add SSL Certificate → Let’s Encrypt.

Domain Names: Enter *.yourdomain.com (and optionally yourdomain.com as a second entry)

DNS Challenge: Required for wildcards — use Cloudflare with the API token from above.

Once the wildcard cert exists, you can assign it to any proxy host instead of requesting individual certs per subdomain.

More on wildcard certs and why they’re worth it →

Setting up internal-only services

If a service should only be accessible on your local network (or via Tailscale), don’t forward port 80/443 from your router to your server. NPM will still serve HTTPS internally — the certificate just needs to use the DNS challenge method since HTTP challenge won’t work without a public route.

The Cloudflare DNS challenge approach works perfectly for this. My entire homelab runs this way: HTTPS everywhere, zero ports forwarded, accessible remotely via Tailscale.

How Tailscale fits into this setup →

Access control lists

NPM has basic access control built in — you can restrict proxy hosts to specific IP ranges or add HTTP basic auth in front of any service. For more robust SSO with 2FA across all services, look at Authelia, which integrates with NPM’s forward auth feature.

Authelia setup with NPM →

Troubleshooting

Certificate request fails: Usually a DNS propagation issue. Wait a few minutes and try again. If using DNS challenge, double-check the API token has the right permissions.

502 Bad Gateway: NPM can’t reach your service. Check that the forward IP and port are correct, and that the service is actually running (docker ps to verify).

Service loads but HTTPS warning persists: Make sure Force SSL is enabled and you’re not loading a cached HTTP version. Try clearing the browser cache or opening a private window.

Port 80/81/443 already in use: Another service (possibly Apache or another nginx) is already running on those ports. Find it with sudo lsof -i :80 and stop it.