Nginx Reverse Proxy einrichten: Der komplette Guide
Auf meinem VPS laufen vier Domains parallel. Dahinter ein Mix aus Next.js auf 3003, einer Flask/Gunicorn-App auf 8099, einer weiteren Node-App auf 3004 und ein paar statischen Sites. Das Ganze über eine einzige IP, eine einzige Maschine. Ohne nginx als Reverse Proxy davor müsste ich entweder vier IPs haben oder jede App direkt auf 80/443 binden — weder noch ist praktikabel.
Reverse Proxies sind eines der Konzepte, die simpel klingen und dann in der Praxis viele Detail-Entscheidungen mitbringen, die man am besten einmal ordentlich macht.
Was ein Reverse Proxy konkret tut
Vereinfacht: nginx nimmt die HTTP-Verbindung des Clients an und reicht sie an einen internen Service weiter. Aus Client-Sicht ist nginx der Server. Aus Service-Sicht ist nginx der Client.
Daraus folgt:
- SSL-Terminierung passiert einmal an nginx, das Backend spricht plain HTTP
- Mehrere Domains auf einer IP, weil nginx anhand des
Host-Headers entscheidet - Backends sind nicht direkt aus dem Internet erreichbar — sie binden auf 127.0.0.1
- Caching, Rate Limiting, Header-Manipulation passieren zentral
Ein Setup, das ich tatsächlich nutze
server {
listen 80;
server_name matti.services;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
server_name matti.services;
ssl_certificate /etc/letsencrypt/live/matti.services/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/matti.services/privkey.pem;
location / {
proxy_pass http://127.0.0.1:3003;
proxy_http_version 1.1;
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;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}
Nichts Spektakuläres, aber jede Zeile hat einen Grund.
proxy_http_version 1.1 ist Pflicht, sonst kein Keep-Alive zum Backend. Default ist 1.0, was für moderne Backends quasi nie passt.
Host $host weil das Backend wissen will, unter welchem Hostnamen die Anfrage reinkam — Next.js Server-Components nutzen das.
X-Forwarded-For und X-Real-IP damit das Backend die echte Client-IP sieht. Ohne das logged jeder Request 127.0.0.1.
Upgrade und Connection: upgrade für WebSockets. Selbst wenn ich aktuell keine nutze, lasse ich die drin — kostet nichts und spart Debugging später.
Was die meisten Tutorials weglassen
Buffer-Größen. Default ist proxy_buffer_size 4k. Bei modernen Apps mit großen Cookie-Headern (Auth-Tokens, Session-Daten) reicht das oft nicht. Symptom: 502er, die unregelmäßig auftreten.
proxy_buffer_size 16k;
proxy_buffers 8 16k;
Timeouts. Default ist 60 Sekunden, was für die meisten Web-Apps OK ist. Aber bei Long-Polling oder Streaming-Responses verstecken sich hier Bugs:
proxy_read_timeout 300s;
proxy_send_timeout 300s;
Real-IP bei mehrfachem Proxy. Wenn Cloudflare vor nginx sitzt, ist $remote_addr die Cloudflare-IP. Erst nach set_real_ip_from mit den Cloudflare-Ranges und real_ip_header CF-Connecting-IP wird die echte Client-IP korrekt geloggt.
Trailing Slash — der Klassiker
proxy_pass http://127.0.0.1:3003; (kein Slash am Ende) leitet die volle URL weiter. proxy_pass http://127.0.0.1:3003/; (mit Slash) strippt den Location-Prefix. Das ist subtil und verursacht regelmäßig 404er, wenn man mehrere Apps unter /api/, /admin/ etc. mountet. Im Zweifel: ohne Slash und der App den vollen Pfad geben.
Logging, das brauchbar ist
Default-nginx-Log ist nutzbar, aber unstrukturiert. Ich ersetze das mit JSON:
log_format json escape=json '{"time":"$time_iso8601",'
'"remote":"$remote_addr",'
'"host":"$host",'
'"method":"$request_method",'
'"uri":"$request_uri",'
'"status":$status,'
'"size":$body_bytes_sent,'
'"ua":"$http_user_agent",'
'"upstream_time":"$upstream_response_time"}';
access_log /var/log/nginx/access.json json;
Damit lässt sich das Log ohne Grep-Akrobatik per jq filtern.
Was ich gelernt habe
Reverse Proxies sind keine Stelle, an der man "schnell mal was probiert". Eine kaputte nginx-Config blockt alle Domains, nicht nur die mit der Änderung. nginx -t vor jedem reload ist Pflicht, kein Vorschlag.