How to Install Nginx Proxy Manager on Proxmox LXC 2026
homelab

How to Install Nginx Proxy Manager on Proxmox LXC 2026

Ricardo Gil
September 21, 2026
12 min read
#proxmox #lxc #nginx-proxy-manager #reverse-proxy #self-hosting

I'll be honest: I ran Traefik for almost eight months in my homelab and I respected it. But every time I needed to add a new proxy host, I was back in my terminal editing YAML, reloading containers, and chasing down typos in provider configs. When I finally tried Nginx Proxy Manager on a whim last spring, I set up three proxy hosts with SSL in under ten minutes β€” all through a web UI. That was it. NPM has lived in a dedicated Proxmox LXC on my Beelink ever since, and I've never looked back.

In this guide I'll walk you through exactly how I set up Nginx Proxy Manager in a Proxmox LXC container, configure Let's Encrypt SSL certificates, and route traffic to the rest of my homelab services. If you're running any self-hosted apps β€” Vaultwarden, Jellyfin, Immich, whatever β€” and you want HTTPS without the config-file headache, this is the setup I'd recommend today.

Why Nginx Proxy Manager Instead of Traefik

I want to be upfront: Traefik is genuinely more powerful than NPM for complex setups. If you're running Kubernetes or need dynamic service discovery via Docker labels, Traefik is the right tool. I wrote about setting up Traefik on Proxmox LXC and it's still a great guide for those use cases. But for a personal homelab where you're managing a dozen or so services by hand, NPM's GUI offers something Traefik's YAML doesn't: speed.

With NPM, adding a new proxy host is four form fields and a checkbox. The SSL certificate β€” via Let's Encrypt β€” is issued automatically during the same flow. There's no waiting for config reloads or wondering if your indentation is off. I'm a full-stack developer; I'm comfortable in the terminal all day. But when I'm setting up a new service on a Sunday afternoon, I'd rather click three buttons than open another YAML file.

NPM also ships with a built-in access list feature for IP-based restrictions, and a dead-simple 404/502 error page system. For a homelab running on a compact mini PC β€” I use a GMKtec G3 N100 Mini PC (~$189) as my primary server β€” the resource footprint of NPM is meaningfully lighter than a full Traefik instance with its middleware stack. It's a more honest fit for the use case.

Prerequisites Before You Start

Before creating the LXC, make sure you have a few things in order. First, you need a domain name with DNS you control. NPM can issue SSL certificates via HTTP challenge (port 80 must be reachable) or DNS challenge (no port forwarding needed β€” I'll cover both). I use Cloudflare for DNS and strongly recommend it; the DNS challenge integration in NPM is excellent.

Second, you should have a basic understanding of LXC containers on Proxmox. If you're new to running containers on Proxmox, check out my complete LXC self-hosting guide first β€” it covers template downloads, networking, and the basics of getting a container online. This post assumes you can already SSH into your Proxmox host and create a container.

Third, a quick note on networking: NPM needs to reach port 80 and 443 from the internet (or at minimum your local network) to serve as a reverse proxy. In my setup, my router forwards 80 and 443 to the NPM LXC's static IP. If you're exposing services only internally, the HTTP challenge won't work β€” use DNS challenge instead. Either way, NPM handles the routing.

Creating the Proxmox LXC Container

Log into your Proxmox web UI and create a new LXC container. I use a Debian 12 template for NPM β€” it's lean and stable. Here are the resource specs I actually run in production on my homelab:

  • CPU: 1 core (NPM is not CPU-intensive)
  • RAM: 512 MB (256 MB minimum; I give it 512 for headroom)
  • Disk: 4 GB (enough for logs, certs, and the SQLite database)
  • Network: Static IP, e.g. 192.168.1.50/24, gateway your router

You can create the container through the UI, or from the Proxmox shell with pct create. I'll show the shell method since it's faster to reproduce:

bash
# Download Debian 12 template first if you haven't
pveam update
pveam available | grep debian-12
pveam download local debian-12-standard_12.7-1_amd64.tar.zst

# Create the LXC container (adjust IDs and IP to your environment)
pct create 110 local:vztmpl/debian-12-standard_12.7-1_amd64.tar.zst \
  --hostname nginx-proxy-manager \
  --memory 512 \
  --cores 1 \
  --rootfs local-lvm:4 \
  --net0 name=eth0,bridge=vmbr0,ip=192.168.1.50/24,gw=192.168.1.1 \
  --nameserver 1.1.1.1 \
  --unprivileged 1 \
  --features nesting=1 \
  --start 1

The --features nesting=1 flag is required for Docker to run inside the LXC. Without it, the Docker daemon won't start. I learned this the hard way the first time I tried to run a Docker-based app in an LXC β€” the error messages are not the most helpful. The --unprivileged 1 flag keeps the container secure; NPM doesn't need root-level host access.

Once the container is running, drop into it with pct enter 110 (or SSH to 192.168.1.50 after setting a root password with pct exec 110 -- passwd root). From here, run a quick update before installing anything:

bash
apt update && apt upgrade -y
apt install -y curl ca-certificates gnupg2

Installing Nginx Proxy Manager

NPM is distributed as a Docker Compose stack. The official way to run it β€” and the way I run it β€” is with Docker and Compose. Install Docker first:

bash
# Install Docker
curl -fsSL https://get.docker.com | sh

# Verify Docker is running
docker --version
systemctl status docker

Now create the directory structure for NPM and its persistent data:

bash
mkdir -p /opt/nginx-proxy-manager/data
mkdir -p /opt/nginx-proxy-manager/letsencrypt
cd /opt/nginx-proxy-manager

Create the docker-compose.yml file:

yaml
version: '3.8'
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'      # NPM Admin UI
    volumes:
      - ./data:/data
      - ./letsencrypt:/etc/letsencrypt
    environment:
      DISABLE_IPV6: 'true'   # Optional: disable if you don't use IPv6

Pull the image and start the stack:

bash
docker compose up -d

# Watch the startup logs
docker compose logs -f npm

The first startup takes about 30–60 seconds while NPM initializes its SQLite database and generates an internal SSL certificate. Once you see Server Listening on port: 81 in the logs, you're ready to open the admin UI.

First Login and Initial Configuration

Open your browser and navigate to http://192.168.1.50:81 (substitute your LXC's IP). You'll see the NPM login screen. The default credentials are:

  • Email: admin@example.com
  • Password: changeme

Log in immediately and change both the email and password. NPM will prompt you to do this on first login β€” don't skip it. I use a strong unique password managed in Vaultwarden for all my homelab services, and NPM is no exception.

Take a moment to explore the UI. The left sidebar has Proxy Hosts, Redirection Hosts, Streams, 404 Hosts, SSL Certificates, and Access Lists. You'll spend most of your time in Proxy Hosts and SSL Certificates. The dashboard shows active hosts, SSL cert status, and recent traffic β€” it's clean and informative without being overwhelming. I have 14 proxy hosts configured right now and the dashboard gives me an at-a-glance view of which certs are valid and which are expiring soon.

Before adding any proxy hosts, I recommend going to SSL Certificates β†’ Add SSL Certificate and setting up a wildcard certificate for your domain. This way every subdomain you create gets HTTPS automatically without triggering a new Let's Encrypt request each time. For wildcard certs, you need to use DNS challenge β€” I'll cover that next.

Setting Up Wildcard SSL with DNS Challenge

This is the part of NPM that I find most impressive. Getting a wildcard cert (*.yourdomain.com) the traditional way involves ACME DNS validation and, historically, some painful scripting. NPM makes it a form. Click SSL Certificates β†’ Add SSL Certificate β†’ Let's Encrypt, enter *.yourdomain.com and yourdomain.com, toggle Use a DNS Challenge, and pick your DNS provider from the dropdown.

For Cloudflare (my setup), NPM asks for a Cloudflare API token with DNS:Edit permissions. Create a scoped token in your Cloudflare dashboard β€” go to Profile β†’ API Tokens β†’ Create Token β†’ Edit zone DNS template, scope it to your specific zone. Paste the token into NPM and hit Save. Within about 30 seconds, the certificate is issued and appears in your list with a green status badge.

bash
# If you want to verify the cert was issued correctly, check on the LXC:
ls /opt/nginx-proxy-manager/letsencrypt/live/
# Should show: yourdomain.com

The cert auto-renews 30 days before expiration β€” NPM handles this completely automatically in the background. I set this up on my Beelink in April and I've never had to touch it since. The first time you see a cert renew itself at 3am while you're asleep, it feels like magic. NPM also sends a notification in the UI when renewals succeed or fail, which I've found more reliable than watching cron job logs.

Adding Your First Proxy Host

Now the fun part. Click Proxy Hosts β†’ Add Proxy Host. Here's what a typical entry looks like β€” in this example, I'm proxying Vaultwarden running in its own LXC at 192.168.1.60:8080:

  • Domain Names: vault.yourdomain.com
  • Scheme: http (the internal connection between NPM and the service)
  • Forward Hostname / IP: 192.168.1.60
  • Forward Port: 8080
  • Check Block Common Exploits
  • Check Websockets Support (required for apps like Home Assistant)

Switch to the SSL tab, select your wildcard certificate from the dropdown, and enable Force SSL and HTTP/2 Support. Click Save. That's it β€” https://vault.yourdomain.com is now live and HTTPS-secured. Point your DNS A record (or CNAME) to your home's public IP, and external access works too if you've forwarded ports 80 and 443 to the NPM LXC.

I've proxied 14 services through NPM this way: Immich, Jellyfin, Navidrome, Home Assistant, n8n, Headscale, Grafana, and more. Each one takes about two minutes to set up. The consistency of the UI means I never have to remember different config syntax for different services β€” every proxy host follows the same four-tab form.

Access Lists and Security Hardening

One NPM feature I use constantly is Access Lists β€” IP-based allowlists for specific proxy hosts. Some of my services are for internal use only and should never be reachable from the public internet, even though they sit behind a domain. NPM lets me create an access list with my home IP range (e.g. 192.168.1.0/24) and apply it to any proxy host. Anything outside that range gets a 403.

To create one: Access Lists β†’ Add Access List. Give it a name like "LAN Only", add your subnet under Allow, toggle Satisfy Any off (so all conditions must match), and save. Then on any proxy host where you want LAN-only access, go to the Access List dropdown and select it. I apply this to my Proxmox web UI proxy, Grafana, and anything else that has no business being internet-facing.

I also enable Block Common Exploits on every proxy host by default. This toggle activates a set of Nginx rules that block common attack patterns β€” SQL injection attempts, path traversal, etc. It's not a WAF, but it catches a lot of noise. Pair it with Cloudflare's free proxy (if you're using CF for DNS) and your attack surface is meaningfully reduced for a homelab setup. My TP-Link 2.5G 8-Port Switch (~$49) connects the NPM LXC and all my service LXCs on the same local segment, so the internal routing never leaves the physical switch β€” that matters for latency when NPM is proxying high-throughput services like Jellyfin.

Performance, Resource Usage, and Keeping NPM Running

After several months of daily use, NPM on my Beelink is remarkably lightweight. The Docker container typically sits at around 20–30 MB RAM and less than 1% CPU at idle. Even under load β€” serving Jellyfin streams, handling n8n webhook calls, and routing Immich API requests simultaneously β€” it rarely exceeds 50 MB RAM or 2% CPU. The 512 MB allocation I gave the LXC is generous; you could probably run NPM in 256 MB if you're tight on resources.

For keeping NPM updated, I have a simple habit: every few weeks I run:

bash
cd /opt/nginx-proxy-manager
docker compose pull
docker compose up -d

This pulls the latest jc21/nginx-proxy-manager:latest image and restarts the container. Downtime is under 5 seconds in my experience. All your proxy hosts, SSL certs, and access lists are stored in the ./data volume, so they persist across updates. I back up the entire /opt/nginx-proxy-manager directory as part of my Proxmox Backup Server schedule β€” since this is the single point of entry for most of my homelab, losing it would be inconvenient.

Speaking of reliability: I run my homelab on a APC Pure Sine Wave UPS 1500VA (~$189). NPM is the kind of service where a surprise power cut during a cert renewal could corrupt the SQLite database. The UPS gives my Beelink 15–20 minutes of runtime on battery, which is more than enough to survive most outages cleanly. For a production-grade homelab setup, I'd consider it non-negotiable.

Common Gotchas and Troubleshooting

A few things tripped me up when I first set up NPM that I want to save you from. The most common issue is the 502 Bad Gateway error after adding a proxy host. This almost always means NPM can't reach the forward host/port. Check: (1) is the target service actually running? (2) is the IP and port correct? (3) is there a firewall rule on the target LXC blocking the connection? On Debian/Ubuntu LXCs I sometimes forget to allow the incoming port with ufw allow.

Second gotcha: if you're proxying a service that expects to know its own URL (like Vaultwarden or Immich), make sure to set the correct base URL in that service's config. NPM handles the SSL termination and passes traffic as HTTP internally, but the app needs to know it's being served at https://vault.yourdomain.com, not just http://localhost:8080. Most apps have an DOMAIN or BASE_URL environment variable for this.

Third: WebSocket support. If a service stops working after proxying and you see connection errors in the browser console, toggle on Websockets Support in the proxy host's Advanced tab. Home Assistant, n8n, and several other apps use WebSockets heavily. It's an easy fix once you know to look for it β€” I just enable it by default now on every host.

Need help setting up your homelab?

I help individuals and small teams design and deploy Proxmox-based self-hosted infrastructure β€” from hardware selection to running services in production. Let's talk β†’

Ricardo Gil is a full-stack developer (.NET/Angular) with 6+ years of experience. He runs a personal homelab on a Beelink mini PC with Proxmox VE, self-hosting everything from Jellyfin to local LLMs with Ollama. More about Ricardo β†’
πŸ“¬Weekly Newsletter

Get the best home lab & AI content

No spam. One email per week. Unsubscribe anytime.

Share this article