Skip to content

Nginx Proxy Manager

Nginx Proxy Manager (NPM) ist die komfortable UI für nginx als Reverse-Proxy. Let’s Encrypt automatisch, WebSocket-Support, Access-Lists per Mausklick. Läuft bei mir als zentraler Reverse-Proxy für alle Sites auf einem Server.

Quick-Links: Server erstinstallation · Server härten


Macht:

  • HTTP/HTTPS-Routing von Domain → Backend-Container
  • Automatische Let’s-Encrypt-Zertifikate (HTTP-01 + DNS-01)
  • WebSocket-Proxy (für Astro/Next/Vite-Dev-Server etc.)
  • Streams (TCP/UDP) für z.B. SSH-over-443
  • Access-Lists (Basic-Auth, IP-Whitelist)

Macht nicht:

  • App-Logik (das macht dein Backend)
  • DNS-Management (CNAMEs musst du beim DNS-Provider setzen)
  • Rate-Limiting für komplexe Policies (geht, aber nginx direkt ist flexibler)

┌──────────────────────┐
Internet ─────────► Cloudflare / DNS │
└──────────┬───────────┘
┌──────────────────────┐
│ Nginx Proxy Manager │ Port 80 + 443
│ (Docker, npm_default)│
└──────────┬───────────┘
│ (Docker-Netzwerk)
┌─────────────────────────────────┐
│ Backend-Container │
│ (dama-site, dama-docs, ...) │
│ typisch: Port 3000/8080 intern │
└─────────────────────────────────┘

Wichtig: NPM und Backends laufen im gleichen Docker-Netzwerk (npm_default). Backend-Ports werden nicht nach außen exponiert — nur NPM hat 80/443 offen.


  • Docker + Docker Compose installiert
  • DNS-Records (A/AAAA) zeigen auf die Server-IP (für ACME-Challenge)
  • Port 80 und 443 in der Firewall offen
  • Domain ist frisch (oder bestehende Let’s-Encrypt-Limits beachten: 5 Zerts/Woche, 50 Duplikate/Woche)

/opt/npm/docker-compose.yml:

services:
app:
image: jc21/nginx-proxy-manager:latest
container_name: npm-app-1
restart: unless-stopped
ports:
- "80:80" # HTTP (ACME + Redirect)
- "443:443" # HTTPS
- "81:81" # Admin-UI (NUR intern oder hinter VPN)
environment:
DISABLE_IPV6: "true"
# TRUSTED_IPS für X-Forwarded-For (dein internes Netz oder 127.0.0.1)
volumes:
- ./data:/data
- ./letsencrypt:/etc/letsencrypt
networks:
- npm_default
networks:
npm_default:
name: npm_default # explizit benennen, damit andere Container attachen können
Terminal window
cd /opt/npm
docker compose up -d
docker compose ps # Status
docker compose logs --tail=20

Browser: http://<server-ip>:81

Default-Credentials (sofort ändern!):

E-Mail: admin@example.com
Passwort: changeme

Nach Login: Settings → Users → Admin-User bearbeiten (E-Mail + Passwort ändern).

Optional: Settings → Default Site — was passiert bei Anfragen ohne Match? Empfehlung: 404-Redirect auf Hauptseite, oder ein einfacher nginx-html-Container.

Beispiel: docs.damavoi.me → interner Docker-Container dama-docs auf Port 80.

Voraussetzung: Der Backend-Container läuft im Netzwerk npm_default. In dessen docker-compose.yml:

services:
web:
# ... (wie bei dama-docs/docker-compose.yml)
networks:
- npm_default
networks:
npm_default:
external: true
name: npm_default

In NPM-UI:

  1. Hosts → Proxy Hosts → Add Proxy Host
  2. Details-Tab:
    • Domain Names: docs.damavoi.me (ggf. zusätzlich www.docs.damavoi.me)
    • Scheme: http
    • Forward Hostname/IP: dama-docs (Container-Name, nicht IP — Docker-interner DNS)
    • Forward Port: 80
    • ✅ Cache Assets
    • ✅ Block Common Exploits
    • ✅ Websockets Support (wenn App das braucht, Astro/Next/Vite Dev meistens ja)
  3. SSL-Tab:
    • SSL Certificate: Request a new SSL Certificate (Let’s Encrypt)
    • ✅ Force SSL
    • ✅ HTTP/2 Support
    • ✅ HSTS Enabled (optional, aber empfohlen)
    • E-Mail für Let’s Encrypt: deine echte Mail (für Renewal-Benachrichtigungen)
  4. Advanced-Tab (meistens leer, ggf. für Custom-Headers)
  5. Save

NPM startet nginx-Reload und ACME-Challenge läuft. Nach 10-30 Sekunden sollte https://docs.damavoi.me ein gültiges Zertifikat haben.

Im DNS-Provider (Cloudflare, Hetzner DNS, etc.):

docs.damavoi.me. 300 A <server-ip>

DNS-Propagation: 1-60 Minuten je nach TTL.

⚠️ Cloudflare-Proxy (orange Wolke): Wenn du Cloudflare als Proxy aktivierst, läuft Let’s Encrypt HTTP-01-Challenge über Cloudflare — funktioniert. DNS-01 wäre noch zuverlässiger (Wildcard-Support), aber dafür brauchst du Cloudflare-API-Key in NPM.

Phase 6: Access-Lists (Basic-Auth vor Site)

Section titled “Phase 6: Access-Lists (Basic-Auth vor Site)”

Sinnvoll für: Staging-Sites, /admin-Pfade, Pre-Launch.

Access Lists → Add Access List:

  • Name: Staging-Only
  • Satisfy Any: ❌
  • Items:
    • 192.168.1.0/24 (Authorization: Allow)
    • ODER Username/Password (Authorization: Deny)

In Proxy Host Details-Tab unten: Access List = Staging-Only.

Für Dienste, die kein HTTP sind: SSH auf alternativem Port, Minecraft, MQTT, etc.

Streams → Add Stream:

  • Incoming Port: 2222
  • Forwarding Host: git.internal
  • Forwarding Port: 22

Achtung: Streams sind Layer-4, kein TLS-Termination durch NPM möglich. SSH-Server braucht eigene Zertifikate oder bleibt unverschlüsselt.


Problem Ursache Fix
ACME-Challenge hängt DNS zeigt noch auf alte IP, oder AAAA-Record fehlt dig +short docs.damavoi.me prüfen, beide Records (A + AAAA oder nur A) konsistent
Rate Limit (“too many certificates”) Subdomain oft neu erstellt warten (5/Woche pro Domain), oder Wildcard-Zertifikat mit DNS-01
Cert renewal failed nach 60 Tagen nginx-Container hat kein Schreibzugriff auf Volume chown -R 1000:1000 /opt/npm/letsencrypt
“Could not obtain Let’s Encrypt certificate” Port 80 von außen blockiert Firewall-Check, ISP-Block, Cloudflare-Pause
SSL funktioniert im Browser, aber ACME-Log zeigt Fehler DNS-Propagation noch nicht abgeschlossen 5-10 Min warten, dann “Renew” manuell triggern

/opt/npm/data enthält alle Configs (Proxy-Hosts, Access-Lists, Cert-Cache) als JSON. /opt/npm/letsencrypt die Zertifikate selbst.

Terminal window
# In restic-Backup aufnehmen
restic -r <repo> backup /opt/npm

Restore = Volume zurückspielen, Container neu starten.

Terminal window
cd /opt/npm
docker compose pull
docker compose up -d
# Compose erkennt "image changed" und recreated.
# Daten bleiben in ./data + ./letsencrypt.

Major-Version-Sprünge (z.B. 2.x → 3.x) Changelog lesen — DB-Schema-Migrationen können manuelle Schritte brauchen.

Migration zu “nur nginx” (wann es sinnvoll wird)

Section titled “Migration zu “nur nginx” (wann es sinnvoll wird)”

NPM ist bequem. Sobald du merkst:

  • Du brauchst Custom-Locations (location /api { ... } mit spezifischem Routing)
  • Rate-Limiting pro Route
  • Eigene Header-Policies pro Backend
  • Lua-Logic (OpenResty)

… wird’s Zeit für eine direkte nginx-Conf. NPM bleibt für die 90%-Cases (statische Sites + Let’s Encrypt), die Special-Cases ziehen in eine separate Config um.


Muss ich Port 81 wirklich offen lassen? Nein — wenn du VPN/Tailscale hast, binde 81 nur auf 127.0.0.1:81:81 und greife über SSH-Tunnel zu.

Warum “http” und nicht “https” zum Backend? Docker-intern. Das TLS-Termination macht NPM am Edge. Backend darf plaintext hören, weil es im privaten Docker-Netz ist.

Was ist mit HTTP/3 (QUIC)? NPM-basierte nginx-Builds haben HTTP/3 noch nicht stabil. Aktivieren via Custom-Config möglich, aber Support ist experimentell.

Kann ich NPM hinter Cloudflare bündeln? Ja. Cloudflare Proxy → NPM → Backend. SSL-Termination in Cloudflare + Let’s Encrypt in NPM ist redundant aber ok (Cloudflare-Pinning mit CF-Connecting-IP Header für echte Client-IP).