nginx als Reverse Proxy einrichten

Veröffentlicht am 14 Min. Lesezeit

Von proxy_pass bis WebSocket-Upgrade: die vollständige Anleitung für nginx als Reverse Proxy, inklusive der vier Header, ohne die Ihre Anwendung jeden Besucher für 127.0.0.1 hält.

Fast jede moderne Anwendung lauscht irgendwo auf einem hohen Port: Node auf 3000, ein Docker-Container auf 8080, Gunicorn auf 8000, ein Java-Anwendungsserver auf 8443. Direkt ins Internet stellt man das nicht. Davor gehört ein Reverse Proxy, und in der Praxis ist das nginx.

Die Grundkonfiguration dafür passt in fünf Zeilen. Genau das ist das Problem: Die fünf Zeilen funktionieren scheinbar, und drei Wochen später fällt auf, dass jeder Besucher im Anwendungs-Log als 127.0.0.1 steht, die Anmeldung per Rate-Limit alle gleichzeitig aussperrt und die Passwort-Zurücksetzen-Mail einen Link auf http://127.0.0.1:3000 enthält. Dieser Artikel behandelt genau diese Stellen.

Voraussetzung ist ein installiertes nginx. Falls noch nicht geschehen, hilft nginx auf Debian und Ubuntu installieren.

Was ein Reverse Proxy technisch wirklich tut

nginx nimmt die Verbindung des Besuchers entgegen und öffnet daraufhin eine eigene, zweite Verbindung zur Anwendung. Das ist der entscheidende Punkt, aus dem sich alle weiteren Probleme ableiten lassen.

Aus Sicht der Anwendung ist der Client nicht der Besucher, sondern nginx. Die Absender-IP ist 127.0.0.1. Das Protokoll ist http, auch wenn draußen HTTPS lief. Der Host-Header lautet standardmäßig 127.0.0.1:3000 und nicht app.example.com. Und HTTP/1.0 statt HTTP/1.1, weshalb WebSockets ohne Zusatzkonfiguration grundsätzlich scheitern.

Alles, was die Anwendung über den echten Besucher wissen soll, muss nginx ihr aktiv als HTTP-Header mitgeben. Von allein passiert das nicht.

Die Grundkonfiguration, und wo sie hingehört

Der Ablageort unterscheidet sich je nach System, das wird regelmäßig verwechselt.

Debian und Ubuntu: Die Konfiguration liegt in /etc/nginx/sites-available/app.conf und wird per Symlink in /etc/nginx/sites-enabled/ aktiviert. Der Standard-Serverblock default fängt sonst alle Anfragen ab.

AlmaLinux, Rocky, RHEL und Oracle Linux: Dort existiert sites-available überhaupt nicht. Die Datei kommt direkt nach /etc/nginx/conf.d/app.conf und ist damit sofort aktiv.

server {
    listen 80;
    listen [::]:80;
    server_name app.example.com;

    location / {
        proxy_pass http://127.0.0.1:3000;
    }
}

Aktivieren auf Debian und Ubuntu:

ln -s /etc/nginx/sites-available/app.conf /etc/nginx/sites-enabled/
rm -f /etc/nginx/sites-enabled/default
nginx -t
systemctl reload nginx

nginx -t vor jedem Neuladen ist keine Höflichkeit, sondern Pflicht. Ein systemctl reload mit fehlerhafter Konfiguration lässt den alten Prozess zwar weiterlaufen, aber ein späterer Neustart des Servers bringt nginx dann gar nicht mehr hoch.

Alle Befehle dieses Abschnitts setzen root-Rechte voraus, arbeiten Sie sonst mit vorangestelltem sudo. Das gilt auch für die reinen Prüfbefehle: nginx -t und nginx -T als normaler Benutzer scheitern nicht an der Konfiguration, sondern an einer Datei, die sie gar nicht schreiben dürfen. Die Meldung [emerg] open() "/run/nginx.pid" failed (13: Permission denied) gefolgt von configuration file /etc/nginx/nginx.conf test failed heißt also nicht, dass Ihre Konfiguration kaputt ist.

Diese Konfiguration leitet weiter. Sie ist trotzdem kaputt, und zwar auf eine Art, die sich erst später zeigt.

Die vier Header, ohne die die Anwendung blind ist

Diese vier Zeilen gehören in jeden location-Block mit proxy_pass:

location / {
    proxy_pass http://127.0.0.1:3000;

    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;
}

Host

Ohne diese Zeile setzt nginx Host: 127.0.0.1:3000. Die Symptome: Django antwortet mit Invalid HTTP_HOST header und liefert 400. Laravel und WordPress erzeugen absolute URLs auf 127.0.0.1, sichtbar in Weiterleitungen und in jeder verschickten Mail. Ein Backend, das mehrere Domains bedient, liefert immer den falschen Mandanten aus.

$host ist dabei die richtige Wahl und nicht $http_host: $host enthält den Hostnamen ohne Port und fällt auf den server_name zurück, falls ein Client gar keinen Host-Header schickt. $http_host reicht durch, was auch immer ankommt, inklusive Portangabe.

X-Real-IP

$remote_addr ist die IP, mit der nginx tatsächlich spricht. Genau eine Adresse, kein Komma, kein Parsen nötig. Für Anwendungen, die nur ein Feld für die Client-IP kennen, ist das der einfachste Weg.

X-Forwarded-For

$proxy_add_x_forwarded_for nimmt ein eventuell schon vorhandenes X-Forwarded-For und hängt $remote_addr rechts an. Bei mehreren Proxys entsteht so eine Kette.

Und hier liegt eine Sicherheitslücke, die in fast keiner Anleitung steht: Ein Client kann selbst ein X-Forwarded-For mitschicken. Steht Ihr nginx direkt am Internet, dann macht $proxy_add_x_forwarded_for aus einer frei erfundenen Angabe des Angreifers und Ihrer echten Client-IP eine gemeinsame Liste. Wenn Ihre Anwendung nun den ersten Eintrag als Client-IP liest, glaubt sie jede beliebige Adresse. Rate-Limits, IP-Sperren und Geo-Logik lassen sich damit aushebeln.

Zwei saubere Konsequenzen:

  • Die Kette wird immer von rechts gelesen. Der letzte Eintrag ist der einzige, den Ihr eigener Proxy geschrieben hat.
  • Wenn nginx die einzige Instanz vor der Anwendung ist, überschreiben Sie die Kette lieber komplett statt sie zu ergänzen:
proxy_set_header X-Forwarded-For $remote_addr;

Damit verschwindet jede gefälschte Vorgeschichte. Nur wenn davor noch ein Loadbalancer oder ein CDN sitzt, dem Sie wirklich vertrauen, ist $proxy_add_x_forwarded_for richtig. In dem Fall gehört zusätzlich das realip-Modul konfiguriert, damit schon nginx selbst die echte IP kennt und nicht die des Vorgängers protokolliert:

set_real_ip_from 10.0.0.0/8;
real_ip_header   X-Forwarded-For;
real_ip_recursive on;

Die set_real_ip_from-Angabe ist eine Whitelist. Ohne sie ist das Modul wirkungslos, mit einer zu weiten Angabe wie 0.0.0.0/0 ist es eine offene Tür.

X-Forwarded-Proto

Die Anwendung sieht eine reine HTTP-Verbindung, egal was draußen passiert. Fehlt dieser Header, passiert typischerweise Folgendes: Die Anwendung merkt "kein HTTPS" und leitet auf HTTPS um, nginx nimmt die Anfrage entgegen, entschlüsselt, reicht sie wieder als HTTP weiter, die Anwendung leitet erneut um. Der Browser meldet ERR_TOO_MANY_REDIRECTS. Ebenso häufig: Cookies mit dem Secure-Flag werden nicht gesetzt, Anmeldungen schlagen ohne Fehlermeldung fehl, und Seiten laden Bilder und Skripte über http://, was der Browser als Mixed Content blockiert.

Nutzen Sie $scheme und nicht den festen Wert "https". Sonst behauptet auch der Port-80-Block, es sei verschlüsselt gewesen.

Der Beweis, dass die Header wirklich ankommen

Statt zu raten, bauen Sie sich einen Spiegel. Dieser zusätzliche Serverblock beantwortet jede Anfrage mit den empfangenen Headern im Klartext:

server {
    listen 127.0.0.1:9999;
    default_type text/plain;

    location / {
        return 200 "Host: $host\nX-Real-IP: $http_x_real_ip\nX-Forwarded-For: $http_x_forwarded_for\nX-Forwarded-Proto: $http_x_forwarded_proto\nProtokoll: $server_protocol\n";
    }
}

Zeigen Sie proxy_pass testweise auf http://127.0.0.1:9999, laden Sie nginx neu und rufen Sie die Seite auf. Was dort steht, ist exakt das, was Ihre Anwendung sonst bekommen hätte. Sind alle vier Zeilen gefüllt und passt die IP zu Ihrem echten Anschluss, dann stimmt die Konfiguration. Danach den Testblock wieder entfernen.

Die Anwendung muss die Header allerdings auch auswerten. Express braucht app.set('trust proxy', 1), Symfony die trusted_proxies-Einstellung, Django USE_X_FORWARDED_HOST und SECURE_PROXY_SSL_HEADER. Ohne diesen Schalter ignorieren die Frameworks die Header bewusst, aus genau dem oben beschriebenen Spoofing-Grund.

proxy_pass und der Schrägstrich, der alles verändert

Der häufigste stille Fehler der gesamten nginx-Konfiguration. Ein einziges Zeichen entscheidet über den Pfad, der beim Backend ankommt.

KonfigurationAnfrageBackend erhält
location /api/ { proxy_pass http://127.0.0.1:3000; }/api/users/api/users
location /api/ { proxy_pass http://127.0.0.1:3000/; }/api/users/users
location /api/ { proxy_pass http://127.0.0.1:3000/v2/; }/api/users/v2/users

Die Regel: Sobald hinter Host und Port irgendein Pfad steht, und sei es nur ein Schrägstrich, ersetzt nginx den Teil der URL, der auf das location-Präfix passt. Ohne Pfadangabe wird die komplette URL unverändert durchgereicht.

Das Fehlerbild ist charakteristisch: Die Startseite funktioniert, aber alles unterhalb eines Präfixes liefert 404, und im Backend-Log stehen Pfade mit doppeltem Präfix wie /api/api/users. Ein Blick ins Anwendungs-Log klärt das in Sekunden, Raten am nginx dagegen kostet Stunden.

Zwei Sonderfälle, die eigene Fehlermeldungen erzeugen. In einem location mit regulärem Ausdruck ist eine Pfadangabe verboten, nginx bricht sonst beim Start ab mit nginx: [emerg] "proxy_pass" cannot have URI part in location given by regular expression, inside named location, or inside "if" statement. Und sobald Sie eine Variable in proxy_pass verwenden, etwa proxy_pass http://$backend;, löst nginx den Namen nicht mehr beim Start auf, sondern zur Laufzeit. Ohne eine resolver-Zeile im Serverblock endet das in no resolver defined to resolve ... und einem 502.

WebSockets weiterleiten

nginx spricht mit dem Backend standardmäßig HTTP/1.0. HTTP/1.0 kennt den Upgrade-Mechanismus nicht. Deshalb scheitert jeder WebSocket an der Grundkonfiguration, unabhängig davon, wie korrekt alles andere ist.

Typische Symptome: Die Browser-Konsole meldet WebSocket connection to 'wss://app.example.com/ws' failed, oft mit dem Zusatz Error during WebSocket handshake: Unexpected response code: 400. Socket.io fällt stillschweigend auf Long-Polling zurück, die Anwendung wirkt nur zäh, und im Access-Log wiederholen sich endlos Zeilen mit /socket.io/?EIO=4&transport=polling.

Zuerst die Zuordnung, die entscheidet, ob eine Verbindung überhaupt ein Upgrade will. Diese gehört in den http-Kontext, also am besten in eine eigene Datei /etc/nginx/conf.d/websocket.conf:

map $http_upgrade $connection_upgrade {
    default upgrade;
    ''      close;
}

Landet dieser Block versehentlich in einem server- oder location-Block, startet nginx nicht mehr: nginx: [emerg] "map" directive is not allowed here.

Dann im location-Block:

location / {
    proxy_pass http://127.0.0.1:3000;

    proxy_http_version 1.1;
    proxy_set_header Upgrade    $http_upgrade;
    proxy_set_header Connection $connection_upgrade;

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

    proxy_read_timeout 3600s;
    proxy_send_timeout 3600s;
}

Warum die Zuordnung und nicht einfach proxy_set_header Connection "upgrade";? Weil dann auch ganz normale HTTP-Anfragen ein Upgrade anfordern. Manche Backends antworten darauf mit 400, und die Keepalive-Wiederverwendung fällt weg. Die Zuordnung schickt upgrade nur dann, wenn der Client tatsächlich eines angefordert hat, und sonst close.

Der Test: In den Entwicklerwerkzeugen des Browsers muss die WebSocket-Anfrage den Status 101 Switching Protocols zeigen. Alles andere, insbesondere 200 oder 400, bedeutet, dass das Upgrade nicht durchgekommen ist. Auf der Kommandozeile geht es auch ohne Browser:

curl -sSi -o /dev/null -w '%{http_code}\n' \
  -H 'Connection: Upgrade' -H 'Upgrade: websocket' \
  -H 'Sec-WebSocket-Version: 13' -H 'Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==' \
  http://127.0.0.1/ws

Zeitüberschreitungen richtig setzen

Bricht eine Verbindung reproduzierbar nach exakt 60 Sekunden ab, ist das kein Zufall, sondern der Standardwert von proxy_read_timeout. Wichtig zum Verständnis: Der Wert begrenzt nicht die Gesamtdauer der Anfrage, sondern die Pause zwischen zwei Lesevorgängen. Ein Download, der zehn Minuten dauert, aber ständig Daten liefert, läuft problemlos durch. Ein WebSocket, auf dem 61 Sekunden lang nichts passiert, fliegt raus.

proxy_connect_timeout 10s;
proxy_send_timeout    60s;
proxy_read_timeout    60s;

proxy_connect_timeout gilt nur für den Verbindungsaufbau und ist auf 75 Sekunden gedeckelt. Höher setzen bringt nichts. Bei einem lokalen Backend sind 10 Sekunden großzügig, ein niedriger Wert lässt Sie schneller merken, dass der Dienst gar nicht läuft.

Für WebSockets ist der bessere Weg nicht proxy_read_timeout 86400s;, sondern ein Heartbeat in der Anwendung, der alle 30 Sekunden ein Ping sendet. Damit bleibt die Zeitüberschreitung als Schutz gegen hängende Verbindungen erhalten, statt sie faktisch abzuschalten.

Zwei weitere Standardwerte, die regelmäßig zuschlagen. client_max_body_size steht auf 1 MB, jeder größere Upload endet in 413 Request Entity Too Large und der Log-Zeile client intended to send too large body. Und bei Server-Sent Events oder Streaming-Antworten kommt beim Besucher lange nichts an, weil nginx puffert. Dann hilft proxy_buffering off; im betreffenden location-Block, gezielt und nicht global.

HTTPS davor setzen

Der bequemste Weg ist certbot mit dem nginx-Plugin. Es liest den bestehenden Serverblock, ergänzt den TLS-Teil und richtet die Erneuerung ein:

apt-get install -y certbot python3-certbot-nginx
certbot --nginx -d app.example.com

Diese Paketnamen gelten für Debian und Ubuntu. Auf AlmaLinux, Rocky Linux und Oracle Linux liegt certbot nicht in den Basis-Repositories, ein bloßes dnf install -y certbot python3-certbot-nginx endet dort mit Error: Unable to find a match. EPEL ist zwingend nötig:

dnf install -y epel-release
dnf install -y certbot python3-certbot-nginx

Auf Oracle Linux 9 heißt das EPEL-Paket oracle-epel-release-el9, gegebenenfalls muss das Repository vorher mit dnf config-manager --enable ol9_developer_EPEL freigeschaltet werden.

Für mehrere Subdomains lohnt ein Blick auf Let's Encrypt Wildcard-Zertifikate.

Ein Versionsunterschied, der beim Kopieren fremder Konfigurationen Warnungen erzeugt: Bis nginx 1.24 aktiviert man HTTP/2 in der listen-Zeile, ab nginx 1.25.1 gibt es dafür eine eigene Direktive. Debian 13 liefert nginx 1.26.3 und will die neue Schreibweise, Debian 12 (1.22.1), Ubuntu 24.04 (1.24.0) und Ubuntu 22.04 (1.18.0) die alte. Auf der Red-Hat-Seite verläuft die Grenze genauso: AlmaLinux 10 bringt 1.26.3 mit und damit die neue Form, AlmaLinux 9, Rocky Linux 9 und Oracle Linux 9 bringen 1.20.1 mit und brauchen die alte.

# nginx ab 1.25.1, also unter anderem Debian 13
listen 443 ssl;
http2 on;

# nginx bis 1.24, also Debian 12, Ubuntu 24.04 und 22.04
listen 443 ssl http2;

Verwenden Sie die alte Form auf einem neuen nginx, meldet nginx -t: nginx: [warn] the "listen ... http2" directive is deprecated, use the "http2" directive instead. Das ist nur eine Warnung, funktioniert also weiter. Die neue Form auf altem nginx dagegen ist ein harter Startfehler: unknown directive "http2".

Zum Abschluss der wichtigste Punkt der ganzen Übung: Das Backend darf nicht selbst am Internet hängen. Ein Reverse Proxy nützt nichts, wenn http://server-ip:3000 weiterhin direkt erreichbar ist, denn dann kann jeder die Header selbst setzen, wie es ihm passt. Binden Sie den Dienst auf 127.0.0.1.

Bei Docker ist das eine besonders scharfe Falle: -p 3000:3000 veröffentlicht den Port auf allen Adressen und trägt dafür Regeln ein, die eine ufw-Firewall schlicht umgehen. Richtig ist -p 127.0.0.1:3000:3000. Details zur Einrichtung stehen in Docker auf Debian und Ubuntu installieren.

Falls Sie ausnahmsweise auf ein HTTPS-Backend proxyen, braucht nginx eine zusätzliche Zeile, sonst schickt es keinen SNI-Namen und das Gegenüber liefert das falsche Zertifikat:

proxy_pass https://backend.intern:8443;
proxy_ssl_server_name on;

Wenn es schiefgeht: die Meldungen im Wortlaut

Erste Anlaufstelle ist immer tail -f /var/log/nginx/error.log. Die Browser-Fehlerseite sagt nichts, das Log sagt alles.

Eine Kleinigkeit vorweg, damit die erste Zeile nicht schon irritiert: Auf AlmaLinux, Rocky Linux und Oracle Linux existiert /var/log/nginx/error.log direkt nach der Installation noch gar nicht, sie entsteht erst beim ersten Start von nginx. tail antwortet dann mit cannot open ... No such file or directory. Auf Debian und Ubuntu legt das Paket access.log und error.log bereits bei der Installation an. Robust über beide Welten hinweg:

tail -n 50 /var/log/nginx/error.log 2>/dev/null || journalctl -u nginx -n 50 --no-pager
Meldung im error.logBedeutung und Abhilfe
connect() failed (111: Connection refused) while connecting to upstreamNichts lauscht auf dem Zielport. Dienststatus prüfen und mit ss -ltnp kontrollieren, ob Port und Adresse zur proxy_pass-Zeile passen. Fehlt ss, liefert es auf Debian und Ubuntu das Paket iproute2, auf der Red-Hat-Familie iproute (dort ohne die 2 im Namen).
connect() failed (113: No route to host)Eine Firewall zwischen nginx und Backend blockt. Bei Containern häufig ein falsches Netzwerk.
connect() to 127.0.0.1:3000 failed (13: Permission denied) while connecting to upstreamAuf AlmaLinux, Rocky, RHEL und Oracle Linux fast immer SELinux. Nachweis mit ausearch -m AVC -ts recent, Abhilfe mit setsebool -P httpd_can_network_connect 1. Auf Debian und Ubuntu tritt das nicht auf.
upstream timed out (110: Connection timed out) while reading response header from upstreamErgibt 504. Das Backend antwortet zu langsam. Erst dort nachsehen, erst danach proxy_read_timeout anheben.
upstream prematurely closed connection while reading response headerErgibt 502. Der Backend-Prozess ist während der Anfrage gestorben, oft durch den OOM-Killer. Siehe Swap einrichten.
upstream sent too big header while reading response header from upstreamZu große Antwort-Header, klassisch bei vielen oder langen Cookies. proxy_buffer_size 32k; und proxy_buffers 8 32k; setzen.
no live upstreams while connecting to upstreamBei einem upstream-Block wurden alle Ziele als ausgefallen markiert. Health-Verhalten über max_fails und fail_timeout steuern.

Für die Sonderfälle rund um 502 gibt es eine eigene Anleitung: nginx 502 Bad Gateway beheben. Wenn Ihr Backend noch gar kein sauber startendes Dienstpaket ist, lohnt vorher einen systemd-Service erstellen.

Woran Sie erkennen, dass es wirklich läuft

Fünf Prüfungen, die zusammen aussagekräftig sind:

  1. nginx -t meldet syntax is ok und test is successful.
  2. nginx -T zeigt die vollständige zusammengesetzte Konfiguration. Suchen Sie darin nach proxy_set_header und zählen Sie: Alle vier Header müssen in jedem relevanten location-Block stehen. Ein proxy_set_header in einem inneren Block hebt alle geerbten Header des äußeren Blocks auf, das ist die häufigste Ursache für "eigentlich hatte ich das doch gesetzt".
  3. Im Anwendungs-Log steht die echte Besucher-IP und nicht 127.0.0.1.
  4. Der Spiegel-Serverblock von oben liefert alle vier Werte gefüllt, mit X-Forwarded-Proto: https beim Aufruf über HTTPS.
  5. Für WebSockets: Status 101 in den Entwicklerwerkzeugen, und die Verbindung übersteht mehr als 60 Sekunden Stille.

Hilfreich ist außerdem ein Log-Format, das die weitergereichte IP mitschreibt. So sehen Sie sofort, ob nginx und Anwendung dieselbe Adresse meinen:

log_format proxied '$remote_addr xff="$http_x_forwarded_for" '
                   'host=$host "$request" $status $body_bytes_sent '
                   'upstream=$upstream_addr rt=$request_time urt=$upstream_response_time';

access_log /var/log/nginx/app.access.log proxied;

Die beiden Zeitwerte am Ende sind Gold wert: $request_time ist die Gesamtdauer aus Sicht des Besuchers, $upstream_response_time die Dauer der Backend-Antwort. Liegen beide dicht beieinander, ist das Backend langsam. Klafft eine Lücke, liegt es an der Leitung zum Client oder an der Pufferung.

Damit steht ein Reverse Proxy, der die Anwendung nicht nur erreichbar macht, sondern ihr auch alles mitgibt, was sie über ihre Besucher wissen muss. Wenn Sie diese Konfiguration auf einem frischen System aufsetzen, ist die Checkliste für neue Rootserver ein guter Startpunkt für alles, was davor gehört.

Häufige Fragen

Warum sieht meine Anwendung 127.0.0.1 statt der echten Besucher-IP?
Weil nginx eine eigene, zweite Verbindung zum Backend öffnet. Aus Sicht der Anwendung ist damit nginx der Client. Die echte Adresse muss aktiv als Header mitgegeben werden, über proxy_set_header X-Real-IP $remote_addr und proxy_set_header X-Forwarded-For. Zusätzlich muss das Framework die Auswertung erlauben, etwa app.set('trust proxy', 1) bei Express oder SECURE_PROXY_SSL_HEADER bei Django.
Was ist der Unterschied zwischen X-Real-IP und X-Forwarded-For?
X-Real-IP enthält genau eine Adresse, nämlich die des direkt verbundenen Clients. X-Forwarded-For ist eine kommagetrennte Kette, an die jeder Proxy seinen Vorgänger anhängt. Die Kette muss immer von rechts gelesen werden, denn nur der letzte Eintrag stammt sicher von Ihrem eigenen Proxy. Alles weiter links kann ein Client gefälscht haben.
Warum funktionieren meine WebSockets hinter nginx nicht?
nginx spricht mit dem Backend standardmäßig HTTP/1.0, und HTTP/1.0 kennt keinen Upgrade-Mechanismus. Nötig sind proxy_http_version 1.1, proxy_set_header Upgrade $http_upgrade und proxy_set_header Connection $connection_upgrade, wobei die Variable $connection_upgrade über einen map-Block im http-Kontext definiert werden muss. Erfolgreich ist es, wenn der Browser Status 101 Switching Protocols zeigt.
Warum bricht meine Verbindung immer nach genau 60 Sekunden ab?
60 Sekunden ist der Standardwert von proxy_read_timeout. Der Wert begrenzt nicht die Gesamtdauer, sondern die Pause zwischen zwei Lesevorgängen. Für lang laufende Verbindungen kann man ihn erhöhen, besser ist bei WebSockets aber ein Heartbeat in der Anwendung, damit die Zeitüberschreitung als Schutz erhalten bleibt.
Was bewirkt der Schrägstrich am Ende von proxy_pass?
Steht hinter Host und Port ein Pfad, auch nur ein einzelner Schrägstrich, ersetzt nginx den auf das location-Präfix passenden Teil der URL. Bei location /api/ mit proxy_pass http://127.0.0.1:3000/ wird aus /api/users beim Backend /users. Ohne Schrägstrich kommt /api/users unverändert an. Typisches Fehlerbild bei falscher Wahl sind 404 unterhalb des Präfixes oder doppelte Pfade wie /api/api/users.
Warum bekomme ich auf AlmaLinux ein 502 mit Permission denied?
Auf der Red-Hat-Familie ist SELinux standardmäßig aktiv und verbietet dem Webserver-Kontext ausgehende Netzwerkverbindungen. Im error.log steht dann connect() failed (13: Permission denied) while connecting to upstream. Abhilfe schafft setsebool -P httpd_can_network_connect 1, die Änderung wirkt sofort ohne Neuladen von nginx. Auf Debian und Ubuntu tritt das Problem nicht auf.

nginx Reverse Proxy proxy_pass WebSocket X-Forwarded-For HTTPS Linux Serververwaltung