Mit einer sauberen Weiterleitung über Nginx lässt sich ein interner Dienst hinter einem öffentlichen Port verstecken, ohne dass man die Kontrolle über Header, TLS oder Zielsysteme verliert. Bei nginx port forwarding ist in der Praxis meist eine Weiterleitung auf HTTP-, TCP- oder UDP-Ebene gemeint, nicht klassisches NAT auf Router-Ebene. Genau diese Unterscheidung ist wichtig, weil davon abhängt, ob du mit `proxy_pass` im HTTP-Kontext arbeitest oder das `stream`-Modul brauchst.
Die wichtigsten Punkte auf einen Blick
- Nginx leitet Verbindungen nicht wie ein Router per NAT weiter, sondern proxy't sie auf HTTP-, TCP- oder UDP-Ebene.
- Für Webanwendungen reicht meist `proxy_pass` im HTTP-Kontext, für Protokolle wie DNS oder PostgreSQL brauchst du `stream`.
- Der Host-Header, `X-Forwarded-For` und die richtige `listen`-Konfiguration entscheiden oft darüber, ob die Weiterleitung sauber funktioniert.
- Das `stream`-Modul ist für rohe Verbindungen sehr praktisch, aber dort gibt es keine HTTP-Header und keine Pfadlogik.
- Die häufigsten Fehler sind falsche `proxy_pass`-Pfade, fehlende Module und nicht freigegebene Ports in Firewall oder Backend.
Warum nginx port forwarding in der Praxis meist Reverse Proxy bedeutet
Ich trenne das Thema bewusst in zwei Ebenen: Nginx kann HTTP-Anfragen an eine interne Anwendung weiterreichen oder rohe TCP/UDP-Verbindungen durchreichen. Nginx macht dabei kein NAT; er nimmt Verbindungen auf einem öffentlichen Port an und spricht sie kontrolliert an ein Backend weiter. Genau deshalb schaue ich zuerst auf `listen`, `server_name` und `proxy_pass`, bevor ich überhaupt an Feinjustierung denke.
Für Webserver ist das meist ein Reverse-Proxy-Szenario: außen etwa Port 80 oder 443, innen ein Dienst auf `127.0.0.1:3000` oder auf einer privaten IP. Nginx entscheidet dabei zunächst anhand von IP und Port, welcher `server`-Block zuständig ist, und prüft danach den `Host`-Header. Wenn die Zuordnung nicht passt, landet die Anfrage schnell im falschen virtuellen Host oder im Default-Server. Das ist der Grund, warum eine saubere Host-Konfiguration oft mehr wert ist als jede später gefrickelte Rewrite-Regel.
Wer diese Logik versteht, baut stabiler und spart sich die meisten späteren Überraschungen. Als Nächstes lohnt sich der Blick darauf, wann HTTP genügt und wann ich auf das Stream-Modul wechseln würde.

Wann HTTP reicht und wann das Stream-Modul nötig wird
Die eigentliche Weiche ist einfach: Sprichst du mit einer Webanwendung, reicht meistens der HTTP-Kontext. Geht es um ein Protokoll ohne HTTP-Struktur, brauchst du `stream`. Genau an dieser Stelle werden viele Setups unnötig kompliziert, weil HTTP und TCP in einen Topf geworfen werden.
| Szenario | Passender Nginx-Modus | Warum das sinnvoll ist | Worauf ich achte |
|---|---|---|---|
| Website, API, Admin-Panel | HTTP Reverse Proxy | Nginx kann Header, Pfade, Cookies und TLS sauber behandeln. | `proxy_set_header`, `server_name`, Redirects |
| WebSocket oder SSE | HTTP Reverse Proxy | Bleibt im HTTP-Kontext, braucht aber oft Upgrade-Header. | Verbindungslaufzeit, Upgrade-Weitergabe, Timeouts |
| PostgreSQL, Redis, MQTT, SSH | TCP über `stream` | Es gibt keine HTTP-Header, nur eine rohe Verbindung. | Portzuordnung, `proxy_timeout`, Modulaktivierung |
| DNS oder andere UDP-Dienste | UDP über `stream` | UDP braucht ein anderes Session-Verhalten als HTTP. | `listen ... udp`, Timeout, Backend-Erreichbarkeit |
Praktisch heißt das auch: Das `stream`-Modul ist nicht automatisch in jeder Installation aktiv. Bei eigenen Builds muss es mit `--with-stream` mitkompiliert werden, bei Paketinstallationen ist es oft als Modul vorhanden, aber nicht zwingend geladen. Wenn ich das nicht zuerst prüfe, suche ich mich später unnötig dumm und dämlich. Mit dieser Trennung im Kopf wird die eigentliche Konfiguration deutlich klarer.
Eine saubere HTTP-Weiterleitung mit proxy_pass aufbauen
Für Webanwendungen ist das der Fall, den ich am häufigsten einsetze. Das Backend läuft auf einem internen Port, Nginx nimmt den öffentlichen Traffic an und leitet die Anfragen weiter. Wichtig ist dabei nicht nur das Ziel, sondern auch, welche Informationen die Zielanwendung bekommt.
Minimalbeispiel für eine Webanwendung
server {
listen 80;
server_name app.example.de;
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;
}
}Ich lasse das Backend hier absichtlich nur auf `127.0.0.1` hören. So ist der Dienst nicht direkt aus dem Netz erreichbar, und Nginx bleibt die einzige öffentliche Eintrittsstelle. `proxy_set_header Host $host;` ist dabei kein kosmetisches Detail, sondern verhindert, dass die Anwendung nur den internen Upstream-Namen sieht. `X-Forwarded-For` und `X-Real-IP` helfen bei Logs, Rate-Limits und Fehlersuche, weil die eigentliche Client-Adresse erhalten bleibt.
Der kleine Slash-Unterschied mit großer Wirkung
Ein typischer Stolperstein steckt im Pfad hinter `proxy_pass`. Diese beiden Varianten verhalten sich nicht gleich:
- `location /app/ { proxy_pass http://127.0.0.1:3000/; }` entfernt den Prefix `/app/` und reicht den Rest weiter.
- `location /app/ { proxy_pass http://127.0.0.1:3000; }` reicht die vollständige URI weiter.
Dieser Unterschied ist klein, aber in der Praxis oft die Ursache für 404s, kaputte Assets oder seltsame Redirects. Wenn eine Anwendung WebSockets nutzt, ergänze ich außerdem die Upgrade-Header, damit der Tunnel nicht unterwegs abbricht. Gerade bei mehreren Diensten auf derselben Maschine ist dieses Detail oft der Unterschied zwischen sauberer Entkopplung und einem langen Nachmittag mit Logs.
TCP und UDP über stream weiterreichen
Sobald es nicht mehr um HTTP geht, wechsle ich in den `stream`-Kontext. Dort arbeitet Nginx als generischer TCP- oder UDP-Proxy, also auf der Transportschicht. Es gibt keine Pfadregeln, keine Header und keine `server_name`-Logik wie im Web-Kontext.
Lesen Sie auch: Nginx DNS Resolver - Wann er wirklich nötig ist und wie er funktioniert
Beispiel für einen TCP-Dienst
stream {
upstream postgres_backend {
server 127.0.0.1:5432;
}
server {
listen 15432;
proxy_connect_timeout 1s;
proxy_timeout 10m;
proxy_pass postgres_backend;
}
server {
listen 53 udp;
proxy_timeout 20s;
proxy_pass 127.0.0.1:5353;
}
}Das erste Beispiel leitet einen externen Port auf eine interne PostgreSQL-Instanz weiter. Das zweite zeigt, wie sich auch UDP-Dienste wie DNS proxy'en lassen. Für beide Fälle gilt: Das Zielsystem sollte möglichst nur intern erreichbar sein, sonst baust du dir ungewollt zwei öffentliche Zugänge zum selben Dienst.
Wenn der Backend-Dienst die Client-Adresse kennen muss, ist das PROXY-Protocol der übliche Weg, sofern die Gegenseite es versteht. Ich setze das aber nur ein, wenn ich den kompletten Pfad kontrolliere, weil sonst schnell Kompatibilitätsprobleme entstehen. Damit sind die sauberen Konfigurationsfälle abgedeckt; im Alltag scheitert es dann meist an denselben wenigen Fehlern.
Typische Fehler, die ich in der Praxis immer wieder sehe
| Symptom | Wahrscheinliche Ursache | Sauberer Fix |
|---|---|---|
| 502 Bad Gateway | Backend läuft nicht, falscher Port oder falsche IP | Dienst prüfen, Portbindung prüfen, mit `ss` oder Logs gegenprüfen |
| 404 trotz funktionierendem Proxy | Falscher `location`-Block oder falsches `proxy_pass`-Pfadverhalten | Trailing Slash kontrollieren und URI-Rewrite bewusst setzen |
| Backend sieht den falschen Host | `Host`-Header wird nicht explizit weitergereicht | `proxy_set_header Host $host;` setzen |
| Konfiguration greift nicht | Direktive steht im falschen Kontext oder `stream` fehlt | Modul laden, Blockstruktur prüfen, `nginx -t` laufen lassen |
| Port ist bereits belegt | Nginx und Anwendung binden denselben öffentlichen Port | Backend auf internen Port legen, Nginx bleibt vorn |
| WebSocket trennt sich sofort | Upgrade-Header oder Timeouts fehlen | Upgrade-Header ergänzen und Timeout passend erhöhen |
Vor jedem Reload prüfe ich die Konfiguration mit `nginx -t`. Das klingt banal, spart aber genau die Fehler, die man nachts am wenigsten gebrauchen kann: einen halbfertigen Reload, einen stillen Syntaxfehler oder einen Block, der zwar formal gültig ist, aber logisch nie erreicht wird. Wenn der Syntaxcheck sauber ist, gehe ich erst in den Betrieb über.
So plane ich eine robuste Weiterleitung für den Betrieb
Wenn ich eine Weiterleitung nicht nur zum Testen, sondern für den echten Betrieb aufsetze, denke ich in Schichten. Nginx soll die öffentliche Kante sein, das Backend bleibt intern, und der Verkehr zwischen beiden Seiten wird so einfach wie möglich gehalten. Je weniger Dienste direkt nach außen sprechen, desto kleiner wird die Angriffsfläche.
- Ich binde interne Dienste an `127.0.0.1` oder an ein privates Netz, nicht an `0.0.0.0`.
- Ich terminierte TLS möglichst an Nginx, wenn ich Zertifikate zentral verwalten will.
- Ich trenne Dienste über eigene `server`-Blöcke oder `upstream`-Gruppen, statt alles in einen Block zu quetschen.
- Ich passe Timeouts an, wenn Uploads, lange API-Aufrufe oder WebSockets im Spiel sind.
- Ich prüfe, ob Firewall-Regeln nur die wirklich benötigten Ports freigeben.
Für einfache Web-Apps ist das meist die beste Lösung: ein Port nach außen, ein sauberer Proxy davor, ein oder mehrere Backends dahinter. Für reine Admin-Protokolle oder komplexe Netzgrenzen würde ich dagegen eher mit VPN, Bastion Host oder Firewall-Regeln arbeiten, statt Nginx zur Allzwecklösung zu machen. Genau an dieser Stelle trennt sich eine brauchbare von einer nur halb verstandenen Architektur.
Was ich vor dem Go-live noch einmal prüfe
- Hört Nginx wirklich auf dem gewünschten öffentlichen Port?
- Antwortet das Backend nur intern und nicht direkt im Netz?
- Passen `Host`, `X-Forwarded-For` und `X-Forwarded-Proto` zur Anwendung?
- Funktionieren Redirects, WebSockets und lange Requests ohne Abbrüche?
- Ist `stream` geladen, falls TCP oder UDP weitergeleitet werden soll?
- Sind Firewall, Hosting-Policy und eventuelle Systemregeln an der richtigen Stelle geöffnet?
Wenn ich nur einen Rat mitgeben würde, dann diesen: Richte die Weiterleitung so ein, dass Nginx die einzige öffentliche Eintrittsstelle ist, und teste jede Route einmal mit einem echten Client statt nur mit einer lokalen Shell. Genau dort zeigen sich Probleme mit Headern, Pfaden und Timeouts am schnellsten.