← Alle Beiträge
NginxDockerReverse ProxySelf-Hosting

Nginx Proxy Manager einrichten: Wildcard-Zertifikate, Radarr, Pocket ID und eine Anmeldung davor

Für die ersten drei Dienste ist ein Reverse Proxy mit Oberfläche tatsächlich die Abkürzung, als die er beworben wird. Danach fängt die Arbeit an: ein Wildcard-Zertifikat für Dienste, die von außen nie erreichbar sind, eine Anwendung, die hinter dem Proxy plötzlich 502 wirft, und die Frage, wer eigentlich vor der Tür steht. Diese Anleitung geht den Weg vom leeren Server bis zum abgesicherten Stack mit eigenem Anmeldedienst, mit jeder Zeile zum Kopieren.

Intellize17 Min. LesezeitStand: NPM 2.15.1, OpenResty 1.29.2.5, Certbot 5.6.0

Was am Ende bei dir läuft

  • Ein Proxy im eigenen Docker-Netz, dessen Oberfläche aus dem Internet nicht erreichbar ist.
  • Ein Wildcard-Zertifikat per DNS-Challenge, das sich selbst erneuert und auch für Dienste gilt, die niemals nach außen zeigen.
  • Sechs typische Dienste dahinter, jeweils mit der einen Einstellung, an der es sonst hängt: Radarr, Pocket ID, Immich, Jellyfin, qBittorrent, Home Assistant.
  • Eine Anmeldung per Passkey vor allem, was selbst keine mitbringt.
  • Eine 502-Diagnose, die in zwei Minuten zum Ergebnis führt statt in zwei Abenden.

Vorausgesetzt: ein Linux-Server mit Docker und Docker Compose, eine eigene Domain, deren DNS über eine Programmierschnittstelle erreichbar ist (Cloudflare, Hetzner, INWX, netcup und 82 weitere), sowie freie Ports 80 und 443. An Vorkenntnissen genügen docker compose up und ein Texteditor auf dem Server. nginx musst du vorher nicht können, lesen können solltest du es danach.

Die Oberfläche hat sieben Schalter, die Datei hat vierzig Zeilen

Der Nginx Proxy Manager ist kein eigener Webserver. Er ist eine Oberfläche über OpenResty, einer nginx-Variante, dazu Certbot für die Zertifikate und eine Vorlagen-Maschine, die aus jedem angelegten Proxy-Host eine echte nginx-Konfiguration schreibt: eine nummerierte Datei unter /data/nginx/proxy_host/. Version 2.15.1 vom 3. Juni 2026 bringt OpenResty 1.29.2.5 und Certbot 5.6.0 mit.

Diese Trennung erklärt beides: warum das Werkzeug so angenehm ist, und warum es an einem bestimmten Punkt aufhört zu helfen. Domain auf einen Container zeigen, Zertifikat holen, HTTP/2 anhaken: drei Klicks, fertig. Sobald aber ein Dienst eine größere Puffergröße braucht, einen zusätzlichen Header erwartet oder Uploads jenseits von zwei Gigabyte annehmen soll, schreibst du wieder nginx. Und zwar in ein Textfeld mit der Beschriftung „Advanced", ohne Syntaxhervorhebung und ohne einen Hinweis darauf, an welcher Stelle der erzeugten Datei dein Text eigentlich landet.

Was die Oberfläche abnimmt und was nicht

Die obere Hälfte ist der Grund, warum man das Werkzeug nimmt. Die untere Hälfte ist dieser Beitrag.

AufgabeIn der OberflächeWas du selbst tust
Domain auf einen Dienst zeigenFormular mit vier Feldernnichts
Zertifikat holen und erneuernein Klick, Erneuerung automatischnichts
Wildcard über DNSAuswahl aus 86 AnbieternZugangsdaten hinterlegen
WebSockets, HSTS, HTTP/2je ein Hakennichts
Passwort und IP-SperreAccess Listden Satisfy-Modus verstehen
Uploadgrenze, Timeouts, Puffernicht vorgesehenAdvanced-Feld
Header setzen oder ändernnicht vorgeseheneigener location-Block
Anmeldung vor Dienste legennicht vorgesehenzweiter Dienst plus auth_request

Stand Version 2.15.1. Eine Anmeldung vor Dienste ohne eigenen Login zu legen, ist die einzige Zeile, für die ein zweiter Dienst nötig ist.

Die untere Hälfte der Tabelle ist kein Versäumnis des Projekts. Ein Reverse Proxy soll weiterleiten, und alles, was darüber hinausgeht, gehört in eine Konfigurationsdatei, die jemand gelesen hat. Nur wird das selten so gesagt, und deshalb kursieren Anleitungen, die drei Zeilen in das Advanced-Feld setzen, von denen zwei wirkungslos sind. Warum sie wirkungslos sind, steht weiter unten und ist der eigentliche Grund für diesen Beitrag.

Schritt 1: Eine Installation, die man in zwei Jahren nicht bereut

Die verbreitete Compose-Datei aus der Dokumentation läuft, hat aber zwei Eigenschaften, die man später teuer bezahlt: Sie veröffentlicht die Verwaltungsoberfläche auf allen Netzwerkschnittstellen, und sie benutzt das bewegliche Bild latest. Beides ändern wir gleich zu Beginn, weil es nachträglich niemand mehr anfasst.

docker-compose.ymlYAML
services:
  app:
    # Feste Version statt latest: ein Neustart soll keine Hauptversion ziehen.
    image: 'jc21/nginx-proxy-manager:2.15.1'
    restart: unless-stopped
    ports:
      - '80:80'
      - '443:443'
      # Die Oberfläche hört nur auf dem Server selbst. Zugriff über
      # SSH-Tunnel, VPN oder Tailscale, nicht aus dem Internet.
      - '127.0.0.1:81:81'
    environment:
      TZ: 'Europe/Berlin'
      # Ohne diese Variablen erscheint beim ersten Aufruf der Assistent.
      # INITIAL_ADMIN_EMAIL: 'admin@example.de'
      # INITIAL_ADMIN_PASSWORD: 'beim-ersten-login-aendern'
    volumes:
      - ./data:/data
      - ./letsencrypt:/etc/letsencrypt
    healthcheck:
      test: ['CMD', '/usr/bin/check-health']
      interval: 30s
      timeout: 3s

networks:
  default:
    name: proxy
    external: true
Der Verwaltungsport hängt am Loopback, die Version ist festgenagelt, und das Netz heißt so, wie es später in jedem Proxy-Host auftaucht.

Drei Zeilen verdienen eine Erklärung. 127.0.0.1:81:81 bindet die Oberfläche ausschließlich an den Server selbst: Du erreichst sie danach über einen SSH-Tunnel (ssh -L 8181:127.0.0.1:81 server), über ein VPN oder über Tailscale, aus dem Internet gar nicht. Das feste Bild 2.15.1 sorgt dafür, dass ein Neustart nicht unangekündigt eine neue Hauptversion zieht. Und das externe Netz proxy ist die Voraussetzung dafür, dass die Dienste dahinter überhaupt keinen Port mehr veröffentlichen müssen.

services:
  radarr:
    image: lscr.io/linuxserver/radarr:latest
    restart: unless-stopped
    ports:
      # Damit hängt Radarr am Port des Servers und ist an jeder
      # Absicherung im Proxy vorbei erreichbar.
      - '7878:7878'
    volumes:
      - ./radarr:/config
Solange dieser Port veröffentlicht ist, ist alles Weitere in diesem Beitrag Dekoration.

Der Unterschied zwischen den beiden Reitern ist der wichtigste Handgriff dieses ganzen Kapitels. In der ersten Variante hängt Radarr an Port 7878 des Servers und ist damit für jeden erreichbar, der die IP kennt, komplett am Proxy vorbei. In der zweiten Variante existiert dieser Port nicht mehr nach außen; erreichbar ist der Dienst nur noch unter dem Namen radarr innerhalb des Docker-Netzes, und der einzige Container in diesem Netz mit einem offenen Port ist der Proxy.

Der erste Start dauert ein paar Minuten, weil der Container Schlüssel erzeugt, die Datenbank anlegt und die IP-Bereiche von Cloudflare und CloudFront herunterlädt. Letzteres lässt sich mit IP_RANGES_FETCH_ENABLED: 'false' abschalten, kostet dich dann aber die korrekte Besucher-IP hinter einem dieser Anbieter. Zwei weitere Variablen sind gelegentlich nötig: DISABLE_IPV6: 'true', wenn im Log „Address family not supported by protocol" auftaucht, und TZ, damit die Zeitstempel in den Logs zur eigenen Uhr passen.

Schritt 2: Was aus deinen Klicks wird

Bevor wir etwas Fortgeschrittenes bauen, lohnt ein Blick in die Datei, die der Proxy aus einem Formular erzeugt. Wer diese fünf Ebenen einmal gesehen hat, versteht in Sekunden, warum eine Zeile an der falschen Stelle nichts bewirkt, und muss danach nie wieder raten.

Wo deine Klicks in der erzeugten Datei landen

Der Nginx Proxy Manager schreibt aus jedem Proxy-Host eine echte nginx-Konfiguration. Fünf Ebenen, und nur eine davon ist das Advanced-Feld. Klick dich durch, dann weißt du, warum eine Zeile an der falschen Stelle wirkungslos bleibt.

Datei /etc/nginx/nginx.conf

Die globale Ebene steckt im Image und wird bei jedem Update überschrieben. Hier stehen die Vorgaben, die vielen Anleitungen entgehen: Uploads sind bereits bis 2 GB erlaubt, Antworten dürfen 90 Sekunden dauern, und NPM bittet die Anwendung dahinter ausdrücklich um unkomprimierte Daten.

client_max_body_size          2000m;
proxy_read_timeout            90s;
proxy_set_header              Accept-Encoding "";
set_real_ip_from              172.16.0.0/12;  # Docker-Netze
real_ip_header                X-Real-IP;
real_ip_recursive             on;

Das änderst du nicht hier Eigene globale Zeilen gehören in /data/nginx/custom/http.conf. Diese Datei liest NPM am Ende des http-Blocks ein und ein Update fasst sie nicht an.

Die entscheidende Regel steckt in der letzten Ebene und stammt nicht vom Nginx Proxy Manager, sondern von nginx selbst: proxy_set_header wird nicht ergänzt, sondern ersetzt. Sobald in einem location-Block auch nur eine einzige dieser Direktiven steht, gilt dort ausschließlich die dortige Liste und nichts von weiter oben. Die Vorlage bindet in location / die Datei proxy.conf mit sechs solchen Zeilen ein. Jeder Header, den du im Advanced-Feld setzt, wird damit still verworfen.

Der Ausweg ist kein Trick, sondern eine dokumentierte Eigenschaft: Beginnt dein Text im Advanced-Feld mit location / {, erkennt NPM das und lässt seinen eigenen Standardblock weg. Deiner ist dann der einzige, und du bist für alles darin selbst zuständig, einschließlich der Weiterleitung.

Advanced-Feld des Proxy-HostsNginx
# Beginnt der Text mit dieser Zeile, lässt NPM seinen eigenen
# location-/-Block weg und dieser hier gilt allein.
location / {
    proxy_pass       $forward_scheme://$server:$port$request_uri;

    # Vollständig wiederholen: proxy_set_header wird nicht ergänzt,
    # sondern auf dieser Ebene komplett ersetzt.
    proxy_set_header Host              $host;
    proxy_set_header X-Real-IP         $remote_addr;
    proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;

    # Der eigentliche Grund für den ganzen Block.
    proxy_set_header X-Forwarded-Ssl   on;

    proxy_http_version 1.1;
    proxy_set_header Upgrade    $http_upgrade;
    proxy_set_header Connection "upgrade";
}
Die Variablen kommen aus dem Formular: NPM setzt sie oben im server-Block. Wer sie benutzt statt fester Werte, kann Ziel und Port weiterhin in der Oberfläche ändern.

Der Reiter „Custom locations" nimmt dir diese Arbeit für zusätzliche Pfade ab, hat aber eine Eigenheit, die man kennen muss: Dort trägt die Vorlage den Zielnamen fest in die Datei ein, statt ihn wie im Standardblock über eine Variable aufzulösen. Fest eingetragene Namen löst nginx beim Start auf. Läuft der Zielcontainer gerade nicht, verweigert nginx den Dienst mit „host not found in upstream", und der Host geht offline. Der Standardblock dagegen fragt zur Laufzeit nach und merkt binnen zehn Sekunden, wenn ein Container neu gestartet wurde und eine andere IP hat.

Schritt 3: Ein Wildcard-Zertifikat per DNS-Challenge

Der übliche Weg zu einem Zertifikat ist die HTTP-Challenge: Let’s Encrypt ruft deine Domain auf Port 80 auf und erwartet dort eine Datei. Das setzt voraus, dass die Domain aus dem Internet erreichbar ist, und es liefert niemals ein Wildcard-Zertifikat. Für einen Heimserver mit zwölf internen Diensten ist beides hinderlich.

Die DNS-Challenge dreht das um. Statt einer Datei legt Certbot über die Schnittstelle deines DNS-Anbieters einen TXT-Eintrag an, und Let’s Encrypt fragt diesen Eintrag ab. Dein Server muss dafür aus dem Internet überhaupt nicht erreichbar sein. Damit bekommst du ein gültiges Zertifikat für *.intern.example.de, das für jeden Dienst gilt, den du je anlegst, auch für die, die nur im Heimnetz auflösen. NPM kennt dafür 86 Anbieter, darunter Cloudflare, Hetzner, INWX, netcup, IONOS und deSEC.

Zugangsdaten im ZertifikatsdialogText
# Cloudflare API token
dns_cloudflare_api_token = 0123456789abcdef0123456789abcdef01234567
Der Inhalt des Feldes ist die Datei, die Certbot bekommt. NPM legt sie unter /etc/letsencrypt/credentials/ mit Rechten 600 ab, weshalb dieser Ordner ins Backup gehört.

Zwei Felder darunter stehen die Wartesekunden. Der Standardwert genügt großen Anbietern, kleinen nicht: Wenn die Ausstellung mit „DNS problem: NXDOMAIN" scheitert, obwohl der Eintrag später sichtbar ist, hat Certbot zu früh nachgesehen. 120 Sekunden lösen das in fast allen Fällen. Wichtiger als der Wert ist die Reihenfolge: Erst ein Testzertifikat gegen die Staging-Umgebung anfordern, dann das echte. Let’s Encrypt begrenzt fehlgeschlagene Versuche pro Woche, und diese Grenze ist an einem Abend Fehlersuche erreicht.

Scheitert eine Erneuerung nach einem Update mit einer Fehlermeldung aus pip, ist die Erweiterung nicht sauber installiert. Dafür bringt das Bild ein Skript mit, das genau das nachholt, ohne dass du den Container neu bauen musst:

Terminal
# Containername herausfinden
docker compose ps

# Erweiterung für einen einzelnen Anbieter nachinstallieren
docker exec npm-app-1 /command/s6-setuidgid 1000:1000 \
  bash -c "/app/scripts/install-certbot-plugins cloudflare"

# Danach die Logs mitlesen und die Erneuerung erneut anstoßen
docker compose logs -f app

Schritt 4: Die Dienste dahinter

Ein Proxy-Host ist in dreißig Sekunden angelegt: Domain, Schema, Zielname, Port, Zertifikat auswählen, „Force SSL" anhaken. Was danach nicht funktioniert, liegt fast nie am Proxy, sondern an einer Einstellung in der Anwendung dahinter, und die ist pro Anwendung eine andere. Die folgende Übersicht ist die Zusammenfassung von sechs Abenden Forensuche.

Sechs Dienste, sechs verschiedene Eigenheiten

Ein Proxy-Host ist in dreißig Sekunden angelegt. Was danach nicht funktioniert, liegt fast immer an einer Einstellung in der Anwendung dahinter. Hier steht pro Dienst, welche das ist.

Die *arr-Anwendungen halten die Oberfläche über SignalR aktuell, also über eine dauerhafte Verbindung. Ohne den WebSocket-Haken lädt die Seite, aber die Warteschlange aktualisiert sich nie und im Browser-Log stapeln sich abgebrochene Verbindungen. Sonarr hört auf 8989, Prowlarr auf 9696, Lidarr auf 8686, Bazarr auf 6767.

Weiterleitung
http · radarr · 7878
WebSockets
Pflicht
Block Common Exploits
kann an bleiben
Einstellung in der App
Authentifizierung auf „Enabled", nicht auf „Disabled for Local Addresses"

Kein Eintrag im Advanced-Feld nötig. Die Voreinstellungen genügen.

Stolperstein Die Einstellung „Disabled for Local Addresses" prüft die IP des Aufrufers. Hinter dem Proxy ist das die IP des Proxys, und die ist immer lokal. Damit ist die Anmeldung für alle abgeschaltet, auch für das halbe Internet. Genau dafür gibt es CVE-2026-30975 in Sonarr.

Zwei Muster wiederholen sich darin. Erstens: Fast jede moderne Oberfläche hält eine dauerhafte Verbindung offen, um sich selbst zu aktualisieren. Der Haken „Websockets Support" ist deshalb eher die Regel als die Ausnahme, und sein Fehlen äußert sich nicht als Fehlermeldung, sondern als eine Seite, die einfach nie neuere Daten zeigt. Zweitens: Anwendungen, die etwas auf ihre eigene Adresse geben, müssen wissen, dass ein Proxy davorsteht. Home Assistant will das explizit in seiner Konfigurationsdatei hören, Jellyfin unter „Known proxies", Pocket ID über TRUST_PROXY, qBittorrent über „Server domains". Vier Dienste, vier Orte, dieselbe Frage.

Bleibt die Entscheidung zwischen einer Subdomain je Dienst (radarr.example.de) und einem Unterordner (example.de/radarr). Nimm Subdomains. Sie kosten dank Wildcard-Zertifikat nichts extra, jeder Dienst bekommt eigene Cookies, und du sparst dir die Basis-URL, die in jeder Anwendung anders heißt und die erfahrungsgemäß irgendwann jemand zurücksetzt. Unterordner lohnen sich nur, wenn du genau eine Domain hast und keine weitere bekommen kannst.

Schritt 5: Radarr, Sonarr und der Fehler, der Türen öffnet

Die *arr-Anwendungen sind der häufigste Anlass, überhaupt einen Reverse Proxy aufzusetzen, und gleichzeitig der Ort mit der gefährlichsten Fehlkonfiguration. Sie hat einen Namen: „Authentication Required" steht auf „Disabled for Local Addresses".

Diese Einstellung prüft die Adresse des Aufrufers und verzichtet bei lokalen Adressen auf die Anmeldung. Hinter einem Reverse Proxy ist die Adresse des Aufrufers aber immer die des Proxys, und die ist immer lokal. Das Ergebnis ist eine Oberfläche, die ohne Passwort öffnet, für dich im Wohnzimmer wie für jeden anderen. Sonarr hat dafür im Jahr 2026 einen Sicherheitshinweis veröffentlicht, CVE-2026-30975 mit Bewertung 8.1, samt der eindeutigen Empfehlung: Die Einstellung gehört auf „Enabled", immer.

Der zweite typische Fall betrifft die Kombination aus Zugriffsliste und Programmierschnittstelle. Legst du im Proxy eine Access List mit Passwort an, wandert diese Abfrage in den Block location /, und damit scheitern alle Anwendungen, die sich sonst mit ihrem API-Schlüssel melden: Overseerr, Prowlarr, die Handy-App, dein eigenes Skript. Weil nginx den längeren Pfad gewinnen lässt, genügt ein zusätzlicher Block für die Schnittstelle.

Advanced-Feld des Radarr-HostsNginx
# nginx nimmt den längsten passenden Pfad. /api gewinnt damit gegen /
# und erbt die Passwortabfrage nicht, die nur im Standardblock steht.
location /api {
    proxy_pass       $forward_scheme://$server:$port$request_uri;

    proxy_set_header Host            $host;
    proxy_set_header X-Real-IP       $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

    # SignalR hält die Verbindung offen, auch auf diesem Pfad.
    proxy_http_version 1.1;
    proxy_set_header Upgrade    $http_upgrade;
    proxy_set_header Connection "upgrade";
}
Ein eigener Block für /api erbt die Passwortabfrage nicht, weil die ausschließlich im Standardblock steht. Die Header müssen hier wiederholt werden, denn auch sie werden nicht vererbt.

Der API-Schlüssel bleibt damit die einzige Hürde vor der Schnittstelle. Das ist vertretbar, solange er lang und pro Anwendung verschieden ist, und es ist der Grund, warum der vorherige Absatz auf „Enabled" besteht: Ohne Anmeldung in der Anwendung selbst wäre dieser Block ein offenes Tor.

Schritt 6: Pocket ID als eigener Anmeldedienst

Pocket ID ist ein OIDC-Anbieter, der ausschließlich mit Passkeys anmeldet: kein Passwort, kein zweiter Faktor per SMS, sondern Fingerabdruck oder Gesichtserkennung auf dem Gerät, das ohnehin in der Hand liegt. Er ist klein genug, um ihn selbst zu betreiben, und genau das macht ihn für einen eigenen Server interessant. Die aktuelle Version 2.13.0 stammt vom 7. August 2026.

docker-compose.ymlYAML
services:
  pocket-id:
    image: pocketid/pocket-id:v2
    restart: unless-stopped
    # Kein ports-Eintrag: der Proxy erreicht den Dienst im Netz "proxy"
    # unter pocket-id:1411.
    environment:
      # Genau die Adresse, die der Browser sieht. Samt https.
      APP_URL: 'https://id.example.de'
      # Die Netze, deren weitergereichte Header geglaubt werden dürfen.
      TRUST_PROXY: '172.18.0.0/16'
      # Einmalig erzeugen: openssl rand -base64 32
      ENCRYPTION_KEY_FILE: /run/secrets/pocket_id_key
    volumes:
      - ./data:/app/data
    secrets:
      - pocket_id_key
    healthcheck:
      test: ['CMD', '/app/pocket-id', 'healthcheck']
      interval: 90s

secrets:
  pocket_id_key:
    file: ./.secrets/pocket_id_key.txt

networks:
  default:
    name: proxy
    external: true
APP_URL ist keine Kosmetik: Aus dieser Adresse baut Pocket ID die Weiterleitungen nach der Anmeldung. Steht dort etwas anderes als das, was der Browser sieht, landest du in einer Schleife.

TRUST_PROXY nennt die Netze, deren weitergereichte Header geglaubt werden dürfen. Ohne diese Angabe sieht Pocket ID bei jedem Zugriff die IP des Proxys, und damit greifen Zählwerke gegen zu viele Fehlversuche für alle Nutzer gemeinsam statt pro Angreifer. true vertraut allen Absendern und ist nur vertretbar, wenn der Dienst wirklich keinen anderen Weg nach außen hat; sauberer ist das Docker-Netz als CIDR.

Der Proxy-Host dazu ist unspektakulär, bis auf drei Zeilen im Advanced-Feld. Die Oberfläche ist mit SvelteKit gebaut und schickt größere Header, als nginx voreingestellt puffert. Ohne die Vergrößerung antwortet der Proxy mit 502, und zwar erst nach erfolgreicher Anmeldung, was die Suche in die falsche Richtung schickt.

Advanced-Feld des Pocket-ID-HostsNginx
# Die Oberfläche ist mit SvelteKit gebaut und schickt große Header.
# Ohne diese drei Zeilen: 502, und zwar erst nach dem Login.
proxy_buffer_size 256k;
proxy_buffers 4 512k;
proxy_busy_buffers_size 512k;
Diese Direktiven tragen einen Einzelwert und werden deshalb korrekt nach unten vererbt. Ein eigener location-Block ist hier nicht nötig.

Nach dem Start legst du unter /setup das erste Konto an und registrierst dort deinen Passkey. Danach folgt der Teil, der Pocket ID nützlich macht: Unter „OIDC Clients" entsteht für jede Anwendung, die sich hier anmelden lassen soll, ein Eintrag mit Kennung, Geheimnis und Rücksprungadresse.

Schritt 7: Eine Anmeldung vor alles legen

Jetzt fügt sich beides zusammen. Viele Selfhosting-Dienste haben gar keine Benutzerverwaltung, andere eine, der man nicht zutrauen möchte, allein im Internet zu stehen. Für solche Fälle setzt man eine Anmeldung davor, die der Proxy vor jeder Anfrage befragt. In nginx heißt dieses Verfahren auth_request: eine interne Unteranfrage, deren Antwortcode darüber entscheidet, ob die eigentliche Anfrage weiterläuft.

Was fehlt, ist der Dienst, der diese Unteranfrage beantwortet. Tinyauth (Version 5.1.3) übernimmt das, spricht selbst OIDC und kann Pocket ID als Anmeldequelle benutzen. Der Ablauf am Ende: Du rufst Radarr auf, der Proxy fragt Tinyauth, Tinyauth schickt dich zu Pocket ID, dort hältst du den Finger auf das Telefon, und du landest in Radarr.

services:
  tinyauth:
    image: ghcr.io/tinyauthapp/tinyauth:v5
    restart: unless-stopped
    environment:
      TINYAUTH_APPURL: 'https://auth.example.de'
      # Nur von diesem Netz aus wird die Absender-IP geglaubt.
      TINYAUTH_AUTH_TRUSTEDPROXIES: '172.18.0.0/16'

      # Pocket ID als Anmeldequelle. Die Kennung "pocketid" taucht in
      # der Rücksprungadresse wieder auf und muss dort gleich lauten.
      TINYAUTH_OAUTH_PROVIDERS_POCKETID_NAME: 'Pocket ID'
      TINYAUTH_OAUTH_PROVIDERS_POCKETID_CLIENTID: 'aus-pocket-id'
      TINYAUTH_OAUTH_PROVIDERS_POCKETID_CLIENTSECRET: 'aus-pocket-id'
      TINYAUTH_OAUTH_PROVIDERS_POCKETID_AUTHURL: 'https://id.example.de/authorize'
      TINYAUTH_OAUTH_PROVIDERS_POCKETID_TOKENURL: 'https://id.example.de/api/oidc/token'
      TINYAUTH_OAUTH_PROVIDERS_POCKETID_USERINFOURL: 'https://id.example.de/api/oidc/userinfo'
      TINYAUTH_OAUTH_PROVIDERS_POCKETID_REDIRECTURL: 'https://auth.example.de/api/oauth/callback/pocketid'
      TINYAUTH_OAUTH_PROVIDERS_POCKETID_SCOPES: 'openid email profile groups'
      # Direkt weiterleiten, statt eine Anbieterauswahl zu zeigen.
      TINYAUTH_OAUTH_AUTOREDIRECT: 'pocketid'

networks:
  default:
    name: proxy
    external: true
In Pocket ID entsteht dazu unter „OIDC Clients" ein Eintrag, dessen Callback-URL exakt der REDIRECTURL entspricht.

Der zweite Reiter ist der Kern. auth_request ruft bei jeder Anfrage die interne Adresse /tinyauth auf. Kommt 200 zurück, läuft alles weiter; kommt 401 oder 403, schickt error_page ... =302 den Browser an die Anmeldeseite, deren Adresse Tinyauth in einem Antwortkopf mitgeliefert hat. Dieser Umweg über den Antwortkopf existiert, weil nginx anders als Traefik oder Caddy aus einer Unteranfrage heraus nicht selbst weiterleiten kann.

Wer feiner steuern will, legt in Pocket ID Gruppen an und beschränkt einzelne Anwendungen darauf. Der Zugriff auf die Mediathek für alle im Haus, der Zugriff auf das Verwaltungswerkzeug nur für dich, und beides ohne ein einziges zusätzliches Passwort. Das ist derselbe Aufbau, den wir in Unternehmen einsetzen, dort nur mit einem größeren Anmeldedienst dahinter.

502 Bad Gateway: vier Ursachen, zwei Minuten

502 heißt nicht „etwas ist kaputt", sondern sehr genau: nginx hat das Ziel angesprochen und keine brauchbare Antwort bekommen. Damit bleiben vier Ursachen, und die lassen sich in dieser Reihenfolge in wenigen Minuten abarbeiten.

Erstens die falsche Adresse. Steht im Feld „Forward Hostname" localhost oder 127.0.0.1, zeigt das auf den Proxy-Container selbst, nicht auf den Server. Richtig ist der Dienstname aus dem gemeinsamen Docker-Netz, also radarr. Läuft der Dienst wirklich auf dem Host und nicht in Docker, nimmst du die Adresse der Docker-Brücke, üblicherweise 172.17.0.1.

Zweitens das falsche Netz. Der Name löst nur auf, wenn beide Container dasselbe Netz teilen. Ein Tippfehler im Dienstnamen sieht dabei genauso aus wie ein fehlendes Netz. Drittens das falsche Schema. Spricht der Dienst ausschließlich HTTPS und im Formular steht http, kommt eine Antwort zurück, die nginx nicht versteht. Viertens zu kleine Puffer, der Pocket-ID-Fall von oben: Im Fehlerlog steht dann wörtlich „upstream sent too big header".

Terminal
# 1. Läuft das Ziel überhaupt?
docker compose ps

# 2. Was sagt der Proxy?
tail -n 50 data/logs/proxy-host-7_error.log

# 3. Die wichtigste Zeile: aus dem Proxy-Container heraus fragen.
#    Antwortet hier etwas, liegt der Fehler in der Konfiguration.
#    Antwortet nichts, stimmen Name, Port oder Netz nicht.
docker exec npm-app-1 curl -sS -o /dev/null -w '%{http_code}\n' http://radarr:7878/

# 4. Teilen sich beide Container wirklich dasselbe Netz?
docker network inspect proxy --format '{{ range .Containers }}{{ .Name }} {{ end }}'
Die dritte Zeile ist die aussagekräftigste: Sie fragt vom Proxy-Container aus, also aus genau der Perspektive, aus der nginx scheitert.

Ein Sonderfall ist der nahe Verwandte 504. Der bedeutet, dass die Antwort zu lange gedauert hat, und die Voreinstellung dafür liegt bei 90 Sekunden. Große Uploads, Datenbank-Migrationen nach einem Update und Server-Sent-Events laufen regelmäßig darüber. Abhilfe ist ein höherer proxy_read_timeout im Advanced-Feld. Bei fortlaufenden Datenströmen gehört proxy_buffering off dazu, sonst sammelt der Proxy die Häppchen und gibt sie erst am Schluss aus. Dieselbe Zeile brauchst du übrigens für einen selbst gebauten MCP-Server, und aus demselben Grund.

Betrieb: was nach dem Livegang wichtig wird

Ein Reverse Proxy ist der eine Dienst, dessen Ausfall alle anderen mitnimmt, und gleichzeitig der einzige, den man von außen erreicht. Beides zusammen macht die folgende Liste zu einer, die man einmal durchgeht und danach nie bereut.

Sieben Punkte, die nach dem ersten Vorfall auf jeder Liste stehen

Zwei Dinge, die auf keiner Checkliste stehen, aber im Alltag Zeit sparen. Erstens: Die Zugriffs- und Fehlerlogs liegen pro Host getrennt unter /data/logs/proxy-host-<id>_access.log. Die Nummer ist die ID aus dem Dateinamen der Konfiguration, und sie steht nirgends in der Oberfläche; du findest sie, indem du den Host bearbeitest und in die Adresse des Browsers schaust. Zweitens: Diese Logs enthalten IP-Adressen und sind damit personenbezogene Daten. Wer den Proxy geschäftlich betreibt, braucht dafür eine Aufbewahrungsfrist im Verzeichnis der Verarbeitungstätigkeiten. Die mitgelieferte Rotation hält Zugriffslogs vier Wochen und Fehlerlogs zehn, und sie lässt sich über eine eigene Konfigurationsdatei ersetzen.

Aus der Praxis: derselbe Aufbau, andere Einsätze

Was hier nach Heimserver klingt, ist in kleineren Betrieben oft die gesamte Infrastruktur. Ein Handwerksbetrieb mit einem Server im Technikraum betreibt darauf typischerweise vier bis sechs Dienste: Dokumentenablage, Zeiterfassung, ein Dashboard, gelegentlich ein Sprachmodell. Genau ein Dienst davon muss von außen erreichbar sein, meistens die Terminbuchung, alle anderen nur aus dem Büro und aus dem VPN.

Diese Trennung ist mit einem Proxy und einem Wildcard-Zertifikat billiger zu haben als mit irgendeiner anderen Lösung: ein Zertifikat, eine Stelle für TLS, eine Stelle für Zugriffsregeln, und die Dienste selbst veröffentlichen keinen einzigen Port. Wie so etwas beim Kunden aussieht, steht in unseren Case Studies, etwa bei Reifen Allgäu, wo die Online-Terminbuchung gegen den echten Kalender arbeitet und deshalb als einziger Dienst nach außen zeigt.

Fazit

Der Nginx Proxy Manager nimmt dir die Zertifikate ab und macht das Weiterleiten zu einem Formular. Alles darüber hinaus bleibt nginx, und das ist keine Schwäche, sondern die Bedingung dafür, dass du es überhaupt beeinflussen kannst. Wer die fünf Ebenen aus Schritt 2 im Kopf hat, schreibt die drei Zeilen im Advanced-Feld richtig statt dreimal falsch.

Wenn du weitermachen willst, in dieser Reihenfolge: Erst das Wildcard-Zertifikat, weil es jede spätere Domain kostenlos mitnimmt. Dann die Anmeldung vor allem, was keine eigene hat, denn ein Dienst ohne Login im Internet ist eine Frage der Zeit und nicht des Glücks. Und erst danach mehr Dienste, denn sechs sauber abgesicherte sind im Alltag deutlich brauchbarer als zwanzig, bei denen niemand mehr weiß, welcher davon offen steht.

Häufige Fragen

Nginx Proxy Manager oder doch Traefik beziehungsweise Caddy?

Der Nginx Proxy Manager gewinnt überall dort, wo mehrere Leute Hosts anlegen sollen oder wo nicht alles in Docker läuft: Ein Formular ist schneller erklärt als eine Konfigurationssprache. Traefik ist stärker, wenn sich Container ständig ändern, weil es sie über Labels selbst erkennt. Caddy hat die kürzeste Konfigurationsdatei und die beste Voreinstellung für TLS. Alle drei können dasselbe; der Unterschied liegt darin, wer sie später bedient.

Brauche ich eine öffentliche Domain, wenn alles nur im Heimnetz läuft?

Für gültige Zertifikate ja. Let’s Encrypt stellt nur für Namen aus, deren Kontrolle nachweisbar ist. Der Ausweg ist genau die DNS-Challenge aus Schritt 3: Du besitzt die Domain, deine internen Namen zeigen aber auf private Adressen und sind aus dem Internet nie erreichbar. Damit hast du gültige Zertifikate ohne offenen Port. Die Alternative wäre eine eigene Zertifizierungsstelle, deren Wurzelzertifikat du auf jedem Gerät installieren müsstest, inklusive der Fernseher.

Funktioniert das hinter Cloudflare?

Ja, mit zwei Einschränkungen. Der Verschlüsselungsmodus in Cloudflare gehört auf „Full (strict)", sonst ist die Strecke zwischen Cloudflare und deinem Server unverschlüsselt. Und die echte Besucher-IP kommt nur an, weil NPM die IP-Bereiche von Cloudflare beim Start herunterlädt und diesen Absendern vertraut. Wer diesen Abruf abschaltet, sieht in allen Logs nur noch Cloudflare-Adressen, und jede IP-Sperre in einer Access List greift ins Leere.

Wie viele Proxy-Hosts verträgt eine Instanz?

Technisch mehrere hundert: Jeder Host ist eine kleine Datei, und nginx verwaltet das mühelos. Praktisch stößt man vorher an zwei andere Grenzen. Zertifikate mit sehr vielen Domainnamen werden unhandlich, weshalb ein Wildcard-Zertifikat ab etwa einem Dutzend Diensten die ruhigere Wahl ist. Und ab zwei DNS-Anbietern in derselben Instanz drohen Konflikte zwischen den Python-Paketen der Certbot-Erweiterungen. Beides spricht für mehrere kleine Instanzen statt einer großen.

Überlebt meine eigene Konfiguration ein Update?

Was im Advanced-Feld oder in den Custom Locations steht, liegt in der Datenbank unter /data und wird bei jedem Start neu in die Konfiguration geschrieben, also ja. Was du dagegen direkt in eine Datei im Container schreibst, ist nach dem nächsten Update weg. Für globale Ergänzungen gibt es deshalb den Ordner /data/nginx/custom mit festen Einhängepunkten, unter anderem http.conf für den http-Block und server_proxy.conf, das an das Ende jedes einzelnen Proxy-Hosts angehängt wird.

Was ist die häufigste Ursache dafür, dass ein Zertifikat nicht erneuert wird?

Bei der HTTP-Challenge ein geschlossener Port 80. Viele schalten ihn nach der Einrichtung ab, weil ohnehin alles auf HTTPS umgeleitet wird, und übersehen, dass die Erneuerung genau diesen Weg braucht. Bei der DNS-Challenge sind es abgelaufene oder zu knapp berechtigte Zugangsdaten des DNS-Anbieters: Ein Token, das nur lesen darf, reicht für den Prüfeintrag nicht. In beiden Fällen steht der Grund im Klartext in den Container-Logs, dreißig Tage bevor es jemand merkt.

Ein Server, den jemand im Blick hat.

Reverse Proxy, Zertifikate, Anmeldung und Backup als eingerichteter Zustand statt als Bastelprojekt. Wir klären zuerst, ob sich der Aufwand lohnt, und übernehmen dann zum Festpreis.

Lieber direkt schreiben?info@intellize.de
  • 100 % DSGVO-konform
  • Server in Deutschland
  • Festpreise
  • Antwort in 24 h