Nginx Proxy Manager
Nginx Proxy Manager
Section titled “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
Was NPM macht (und was nicht)
Section titled “Was NPM macht (und was nicht)”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)
Architektur (typisches Setup)
Section titled “Architektur (typisches Setup)” ┌──────────────────────┐ 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.
Phase 1: Voraussetzungen
Section titled “Phase 1: Voraussetzungen”- 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)
Phase 2: docker-compose.yml
Section titled “Phase 2: docker-compose.yml”/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önnencd /opt/npmdocker compose up -ddocker compose ps # Statusdocker compose logs --tail=20Phase 3: Admin-UI — erste Schritte
Section titled “Phase 3: Admin-UI — erste Schritte”Browser: http://<server-ip>:81
Default-Credentials (sofort ändern!):
E-Mail: admin@example.comPasswort: changemeNach 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.
Phase 4: Ersten Proxy Host anlegen
Section titled “Phase 4: Ersten Proxy Host anlegen”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_defaultIn NPM-UI:
- Hosts → Proxy Hosts → Add Proxy Host
- Details-Tab:
- Domain Names:
docs.damavoi.me(ggf. zusätzlichwww.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)
- Domain Names:
- 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)
- Advanced-Tab (meistens leer, ggf. für Custom-Headers)
- Save
NPM startet nginx-Reload und ACME-Challenge läuft. Nach 10-30 Sekunden sollte https://docs.damavoi.me ein gültiges Zertifikat haben.
Phase 5: DNS zeigen lassen
Section titled “Phase 5: DNS zeigen lassen”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.
Phase 7: Streams (TCP/UDP-Forwarding)
Section titled “Phase 7: Streams (TCP/UDP-Forwarding)”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.
ACME / Let’s Encrypt — Stolpersteine
Section titled “ACME / Let’s Encrypt — Stolpersteine”| 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 |
Backup von NPM
Section titled “Backup von NPM”/opt/npm/data enthält alle Configs (Proxy-Hosts, Access-Lists, Cert-Cache) als JSON. /opt/npm/letsencrypt die Zertifikate selbst.
# In restic-Backup aufnehmenrestic -r <repo> backup /opt/npmRestore = Volume zurückspielen, Container neu starten.
Upgrade
Section titled “Upgrade”cd /opt/npmdocker compose pulldocker 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.
Mini-FAQ
Section titled “Mini-FAQ”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).