💡 Hinweis: Dieses Tutorial setzt voraus, dass du einen normalen Benutzer mit sudo-Rechten verwendest — wie im Tutorial Debian 13 Server absichern beschrieben.

1. Einleitung — Was ist ein Reverse Proxy?

Stell dir vor, du betreibst auf einem einzigen Server mehrere Webanwendungen: eine Node.js-App auf Port 3000, eine API auf Port 8080 und vielleicht noch ein Dashboard auf Port 9090. Alle sollen über Port 80/443 erreichbar sein — mit eigener Subdomain und SSL-Zertifikat. Genau das macht ein Reverse Proxy.

Ein Reverse Proxy sitzt zwischen dem Internet und deinen Backend-Services. Er nimmt eingehende Anfragen entgegen und leitet sie — basierend auf Domain, Pfad oder anderen Kriterien — an den richtigen internen Service weiter. Der Client merkt davon nichts: Für ihn sieht es so aus, als käme alles von einem einzigen Server.

Warum brauchst du einen Reverse Proxy?

  • Ein einziger Einstiegspunkt: Alle Services laufen hinter Port 443 mit SSL — keine kryptischen Port-Nummern in der URL
  • SSL-Terminierung: Nginx kümmert sich um HTTPS, deine Apps müssen sich nicht darum kümmern
  • Sicherheit: Deine Backend-Ports sind nicht direkt aus dem Internet erreichbar
  • Load Balancing: Verteile Traffic auf mehrere Instanzen einer App
  • Caching: Häufig angefragte Inhalte direkt aus dem Proxy liefern
  • Zentrale Konfiguration: Rate Limiting, Logging und Security-Header an einer Stelle

Nginx ist dafür die populärste Wahl — leichtgewichtig, extrem performant und millionenfach bewährt.

2. Voraussetzungen

Dieses Tutorial baut auf den vorherigen Teilen der Serie auf:

Falls du die vorherigen Tutorials noch nicht durchgearbeitet hast, hole das jetzt nach. Ab hier gehe ich davon aus, dass Nginx läuft und du weißt, wie Server-Blöcke funktionieren.

# Schneller Check: Läuft Nginx?
sudo systemctl status nginx
# Ausgabe sollte "active (running)" zeigen

3. Konzept: Ein Server, mehrere Services auf verschiedenen Ports

Bevor wir konfigurieren, solltest du das Grundkonzept verstehen. Hier ein typisches Setup:

Internet


┌──────────────────────┐
Nginx (Port 80/443) │  ← Reverse Proxy
└──────┬───┬───┬───────┘
       │   │   │
       ▼   ▼   ▼
    :3000 :8080 :9090
    App   API   Dashboard

Der Ablauf bei jeder Anfrage:

  1. Ein Client ruft https://app.example.com auf
  2. Nginx empfängt die Anfrage auf Port 443
  3. Nginx prüft den server_name und findet den passenden Server-Block
  4. Die Direktive proxy_pass leitet die Anfrage intern an http://127.0.0.1:3000 weiter
  5. Die App antwortet, Nginx reicht die Antwort an den Client durch

Wichtig: Die Backend-Services lauschen nur auf 127.0.0.1 (localhost) — sie sind von außen nicht direkt erreichbar. Nur Nginx ist dem Internet ausgesetzt.

4. Grundlegende Reverse-Proxy-Konfiguration

Die zwei wichtigsten Direktiven für einen Reverse Proxy sind proxy_pass und proxy_set_header.

proxy_pass — Wohin wird weitergeleitet?

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

Das ist die Minimalversion. Nginx leitet alle Anfragen an den lokalen Service auf Port 3000 weiter. Aber so simpel solltest du es nicht konfigurieren — denn wichtige Informationen gehen verloren.

proxy_set_header — Kontext weitergeben

Wenn Nginx eine Anfrage weiterleitet, gehen standardmäßig einige Informationen verloren. Dein Backend sieht dann nur 127.0.0.1 als Client-IP und weiß nicht, ob die Original-Anfrage per HTTPS kam. Mit proxy_set_header reichst du diese Informationen durch:

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

    # Original-Host-Header durchreichen
    proxy_set_header Host $host;

    # Echte Client-IP weitergeben
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

    # Dem Backend mitteilen, ob HTTPS verwendet wurde
    proxy_set_header X-Forwarded-Proto $scheme;
}

Was diese Header bedeuten:

HeaderZweck
HostDer Original-Hostname aus der Anfrage (z.B. app.example.com)
X-Real-IPDie echte IP-Adresse des Clients
X-Forwarded-ForKette aller Proxies, durch die die Anfrage ging
X-Forwarded-Protohttp oder https — das Original-Protokoll

Vollständiger Basis-Template

Hier ist ein Template, das du als Ausgangspunkt für jeden Reverse-Proxy-Block verwenden kannst:

server {
    listen 80;
    server_name app.example.com;

    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;

        # Timeouts anpassen (optional, aber empfohlen)
        proxy_connect_timeout 60s;
        proxy_send_timeout 60s;
        proxy_read_timeout 60s;
    }
}

5. Praxis-Beispiel 1: Node.js App hinter Nginx

Lass uns eine echte Node.js-Anwendung hinter Nginx stellen. Zuerst erstellen wir eine minimale App zum Testen.

Node.js installieren (falls noch nicht geschehen)

# Node.js 22 LTS via NodeSource
curl -fsSL https://deb.nodesource.com/setup_22.x | sudo bash -
sudo apt install -y nodejs

# Version prüfen
node --version
npm --version

Test-App erstellen

# Verzeichnis anlegen
sudo mkdir -p /var/www/myapp
cd /var/www/myapp

# Minimale Express-App
sudo tee app.js > /dev/null <<'EOF'
const express = require('express');
const app = express();
const PORT = 3000;

app.get('/', (req, res) => {
    res.json({
        message: 'Hello from Node.js!',
        headers: req.headers,
        ip: req.headers['x-real-ip'] || req.ip
    });
});

app.listen(PORT, '127.0.0.1', () => {
    console.log(`App running on http://127.0.0.1:${PORT}`);
});
EOF

# Dependencies installieren
sudo npm init -y
sudo npm install express

Beachte: Die App bindet sich an 127.0.0.1, nicht an 0.0.0.0. Das ist wichtig — sie soll nur lokal erreichbar sein.

Systemd-Service für die App

Damit die App beim Serverstart automatisch läuft und bei Abstürzen neu startet:

sudo tee /etc/systemd/system/myapp.service > /dev/null <<'EOF'
[Unit]
Description=My Node.js App
After=network.target

[Service]
Type=simple
User=www-data
WorkingDirectory=/var/www/myapp
ExecStart=/usr/bin/node app.js
Restart=on-failure
RestartSec=5
Environment=NODE_ENV=production

[Install]
WantedBy=multi-user.target
EOF

sudo systemctl daemon-reload
sudo systemctl enable --now myapp
sudo systemctl status myapp

Nginx-Konfiguration

sudo tee /etc/nginx/sites-available/myapp > /dev/null <<'EOF'
server {
    listen 80;
    server_name app.example.com;

    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;

        proxy_connect_timeout 60s;
        proxy_send_timeout 60s;
        proxy_read_timeout 60s;
    }
}
EOF

# Aktivieren und testen
sudo ln -s /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx

Testen

# Lokal testen (direkt)
curl http://127.0.0.1:3000

# Über Nginx testen
curl -H "Host: app.example.com" http://127.0.0.1

In der JSON-Antwort solltest du die durchgereichten Header sehen — x-real-ip, x-forwarded-for und x-forwarded-proto.

6. Praxis-Beispiel 2: Docker-Container hinter Nginx

Docker-Container sind ein häufiger Use Case für Reverse Proxies. Jeder Container bekommt seinen eigenen Port, Nginx leitet den Traffic weiter.

Docker installieren (falls nötig)

# Docker-Repository hinzufügen
sudo apt update
sudo apt install -y ca-certificates curl gnupg
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/debian/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/debian trixie stable" | sudo tee /etc/apt/sources.list.d/docker.list
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io

Beispiel: Grafana im Container

# Grafana starten (Dashboard auf Port 3001)
docker run -d \
    --name grafana \
    --restart unless-stopped \
    -p 127.0.0.1:3001:3000 \
    grafana/grafana

Wichtig: Das 127.0.0.1: vor dem Port stellt sicher, dass der Container nur lokal erreichbar ist — nicht direkt aus dem Internet.

Nginx-Konfiguration für den Container

sudo tee /etc/nginx/sites-available/grafana > /dev/null <<'EOF'
server {
    listen 80;
    server_name grafana.example.com;

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

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

sudo ln -s /etc/nginx/sites-available/grafana /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx

Docker Compose mit mehreren Services

In der Praxis nutzt du oft Docker Compose mit mehreren Services:

# docker-compose.yml
services:
  grafana:
    image: grafana/grafana
    ports:
      - "127.0.0.1:3001:3000"
    restart: unless-stopped

  uptime-kuma:
    image: louislam/uptime-kuma
    ports:
      - "127.0.0.1:3002:3001"
    volumes:
      - uptime-kuma-data:/app/data
    restart: unless-stopped

  gitea:
    image: gitea/gitea
    ports:
      - "127.0.0.1:3003:3000"
    volumes:
      - gitea-data:/data
    restart: unless-stopped

volumes:
  uptime-kuma-data:
  gitea-data:

Jeder Service bekommt seinen eigenen Port auf 127.0.0.1 und eine eigene Nginx-Konfiguration mit passender Subdomain.

7. Mehrere Subdomains für verschiedene Services

DNS einrichten

Zuerst brauchst du DNS-Einträge für jede Subdomain. Bei deinem DNS-Provider (Cloudflare, Hetzner DNS, etc.):

# A-Records anlegen (alle auf die gleiche Server-IP)
app.example.com      A    203.0.113.50
api.example.com      A    203.0.113.50
grafana.example.com  A    203.0.113.50
git.example.com      A    203.0.113.50

Alternativ kannst du einen Wildcard-DNS-Eintrag setzen:

*.example.com    A    203.0.113.50

Damit zeigen alle Subdomains auf deinen Server. Nginx entscheidet dann, welcher Service antwortet.

Nginx-Konfiguration für mehrere Subdomains

Jede Subdomain bekommt ihre eigene Konfigurationsdatei:

# /etc/nginx/sites-available/app.example.com
server {
    listen 80;
    server_name app.example.com;

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

# /etc/nginx/sites-available/api.example.com
server {
    listen 80;
    server_name api.example.com;

    location / {
        proxy_pass http://127.0.0.1:8080;
        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;
    }
}

# /etc/nginx/sites-available/grafana.example.com
server {
    listen 80;
    server_name grafana.example.com;

    location / {
        proxy_pass http://127.0.0.1:3001;
        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;
    }
}
# Alle aktivieren
for site in app api grafana; do
    sudo ln -sf /etc/nginx/sites-available/${site}.example.com /etc/nginx/sites-enabled/
done
sudo nginx -t && sudo systemctl reload nginx

Catch-All für unbekannte Subdomains

Was passiert, wenn jemand eine Subdomain aufruft, die nicht konfiguriert ist? Ohne Catch-All antwortet Nginx mit dem ersten Server-Block. Besser ist ein expliziter Default:

server {
    listen 80 default_server;
    server_name _;

    return 444;  # Verbindung ohne Antwort schließen
}

8. SSL für alle Subdomains

Jetzt sichern wir alle Subdomains mit SSL ab. Du hast zwei Optionen: einzelne Zertifikate oder ein Wildcard-Zertifikat.

Option A: Einzelne Zertifikate (einfach)

# Certbot für mehrere Domains auf einmal
sudo certbot --nginx \
    -d app.example.com \
    -d api.example.com \
    -d grafana.example.com \
    -d git.example.com

Certbot modifiziert automatisch deine Nginx-Konfigurationen und fügt die SSL-Direktiven ein. Bei Renewal werden alle Zertifikate automatisch erneuert.

Option B: Wildcard-Zertifikat (eleganter)

Ein Wildcard-Zertifikat gilt für *.example.com — du musst nicht für jede neue Subdomain ein neues Zertifikat anfordern. Voraussetzung: DNS-Challenge (HTTP-Challenge funktioniert nicht für Wildcards).

# Cloudflare DNS Plugin installieren
sudo apt install -y python3-certbot-dns-cloudflare

# Cloudflare API-Token konfigurieren
sudo mkdir -p /etc/letsencrypt
sudo tee /etc/letsencrypt/cloudflare.ini > /dev/null <<EOF
dns_cloudflare_api_token = DEIN_CLOUDFLARE_API_TOKEN
EOF
sudo chmod 600 /etc/letsencrypt/cloudflare.ini

# Wildcard-Zertifikat anfordern
sudo certbot certonly \
    --dns-cloudflare \
    --dns-cloudflare-credentials /etc/letsencrypt/cloudflare.ini \
    -d example.com \
    -d "*.example.com" \
    --preferred-challenges dns-01

Nginx SSL-Konfiguration mit Wildcard

Mit dem Wildcard-Zertifikat sieht ein Server-Block so aus:

server {
    listen 443 ssl;
    http2 on;
    server_name app.example.com;

    ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

    # Moderne SSL-Einstellungen
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
    ssl_prefer_server_ciphers off;

    # HSTS (optional, aber empfohlen)
    add_header Strict-Transport-Security "max-age=63072000" always;

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

# HTTP → HTTPS Redirect
server {
    listen 80;
    server_name app.example.com;
    return 301 https://$host$request_uri;
}

SSL-Snippet für DRY-Konfiguration

Um nicht in jedem Server-Block die gleichen SSL-Einstellungen zu wiederholen, erstelle ein Snippet:

# /etc/nginx/snippets/ssl-wildcard.conf
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;
add_header Strict-Transport-Security "max-age=63072000" always;

Jetzt reicht in jedem Server-Block:

server {
    listen 443 ssl;
    http2 on;
    server_name app.example.com;

    include snippets/ssl-wildcard.conf;

    location / {
        proxy_pass http://127.0.0.1:3000;
        # ... Header wie gehabt
    }
}

9. WebSocket-Support

Viele moderne Anwendungen nutzen WebSockets — Chat-Apps, Live-Dashboards, Echtzeit-Notifications. Standardmäßig blockiert Nginx WebSocket-Verbindungen, weil sie ein Upgrade des HTTP-Protokolls erfordern.

Das Problem

Ein WebSocket-Handshake sieht so aus:

GET /ws HTTP/1.1
Host: app.example.com
Upgrade: websocket
Connection: Upgrade

Nginx leitet standardmäßig weder den Upgrade– noch den Connection-Header weiter. Das Ergebnis: Der WebSocket-Handshake schlägt fehl.

Die Lösung

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;

    # WebSocket-Support
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";

    # Timeout für WebSocket-Verbindungen erhöhen
    # (Standard 60s würde idle Connections trennen)
    proxy_read_timeout 86400s;
    proxy_send_timeout 86400s;
}

Erklärung:

  • proxy_http_version 1.1 — WebSockets erfordern HTTP/1.1 (nicht 1.0)
  • proxy_set_header Upgrade $http_upgrade — Leitet den Upgrade-Header durch
  • proxy_set_header Connection "upgrade" — Signalisiert das Connection-Upgrade
  • proxy_read_timeout 86400s — 24 Stunden Timeout für langlebige WebSocket-Verbindungen

Nur bestimmte Pfade als WebSocket

Wenn nur ein bestimmter Pfad WebSockets nutzt (z.B. /ws oder /socket.io):

server {
    listen 443 ssl;
    http2 on;
    server_name app.example.com;
    include snippets/ssl-wildcard.conf;

    # Normaler HTTP-Traffic
    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;
    }

    # WebSocket-Endpunkt
    location /socket.io/ {
        proxy_pass http://127.0.0.1:3000;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        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_read_timeout 86400s;
    }
}

10. Load Balancing Grundlagen

Was, wenn eine einzelne App-Instanz den Traffic nicht mehr schafft? Mit der upstream-Direktive kannst du den Traffic auf mehrere Backend-Instanzen verteilen.

Einfaches Round-Robin

upstream myapp_backend {
    server 127.0.0.1:3000;
    server 127.0.0.1:3001;
    server 127.0.0.1:3002;
}

server {
    listen 443 ssl;
    http2 on;
    server_name app.example.com;
    include snippets/ssl-wildcard.conf;

    location / {
        proxy_pass http://myapp_backend;
        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;
    }
}

Standardmäßig verwendet Nginx Round-Robin — jede Anfrage geht an den nächsten Server in der Liste.

Weitere Strategien

# Gewichtung: Server 1 bekommt doppelt so viel Traffic
upstream myapp_backend {
    server 127.0.0.1:3000 weight=2;
    server 127.0.0.1:3001 weight=1;
}

# Least Connections: Anfrage geht an den Server mit den wenigsten Verbindungen
upstream myapp_backend {
    least_conn;
    server 127.0.0.1:3000;
    server 127.0.0.1:3001;
}

# IP Hash: Gleicher Client → Gleicher Server (Session-Persistence)
upstream myapp_backend {
    ip_hash;
    server 127.0.0.1:3000;
    server 127.0.0.1:3001;
}

Health Checks und Failover

upstream myapp_backend {
    server 127.0.0.1:3000 max_fails=3 fail_timeout=30s;
    server 127.0.0.1:3001 max_fails=3 fail_timeout=30s;
    server 127.0.0.1:3002 backup;  # Nur wenn alle anderen down sind
}
  • max_fails=3 — Nach 3 fehlgeschlagenen Versuchen wird der Server als „down“ markiert
  • fail_timeout=30s — Server bleibt 30 Sekunden als „down“ markiert, dann wird er erneut getestet
  • backup — Dieser Server wird nur aktiviert, wenn alle primären Server ausfallen

11. Caching & Buffering am Reverse Proxy

Nginx kann Antworten deiner Backend-Services zwischenspeichern. Das entlastet die Backends und beschleunigt die Auslieferung für wiederkehrende Anfragen.

Proxy-Cache einrichten

# In /etc/nginx/nginx.conf (im http-Block)
http {
    # Cache-Zone definieren
    proxy_cache_path /var/cache/nginx/proxy
        levels=1:2
        keys_zone=proxy_cache:10m
        max_size=1g
        inactive=60m
        use_temp_path=off;

    # ...
}

Parameter erklärt:

  • levels=1:2 — Verzeichnisstruktur für Cache-Dateien
  • keys_zone=proxy_cache:10m — 10 MB Shared Memory für Cache-Keys
  • max_size=1g — Maximale Cache-Größe auf der Festplatte
  • inactive=60m — Cache-Einträge werden nach 60 Minuten ohne Zugriff gelöscht

Cache im Server-Block aktivieren

server {
    listen 443 ssl;
    http2 on;
    server_name api.example.com;
    include snippets/ssl-wildcard.conf;

    location / {
        proxy_pass http://127.0.0.1:8080;
        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;

        # Caching aktivieren
        proxy_cache proxy_cache;
        proxy_cache_valid 200 10m;       # 200er-Antworten 10 Minuten cachen
        proxy_cache_valid 404 1m;        # 404 nur 1 Minute
        proxy_cache_use_stale error timeout updating;

        # Cache-Status im Header (zum Debugging)
        add_header X-Cache-Status $upstream_cache_status;
    }

    # Bestimmte Pfade NICHT cachen
    location /api/auth {
        proxy_pass http://127.0.0.1:8080;
        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_no_cache 1;
        proxy_cache_bypass 1;
    }
}

Buffering

Nginx puffert standardmäßig die Antworten des Backends, bevor sie an den Client gesendet werden. Das ist meistens gut — aber für Streaming-Anwendungen (Server-Sent Events, Chunked Responses) willst du es deaktivieren:

# Für SSE/Streaming-Endpunkte
location /events {
    proxy_pass http://127.0.0.1:3000;
    proxy_buffering off;
    proxy_cache off;
    proxy_set_header Connection '';
    proxy_http_version 1.1;
    chunked_transfer_encoding off;
}

12. Sicherheit: Rate Limiting & IP-Whitelisting

Rate Limiting

Schütze deine APIs vor Missbrauch und DDoS mit Rate Limiting:

# In /etc/nginx/nginx.conf (im http-Block)
http {
    # Rate-Limit-Zone definieren: 10 Requests/Sekunde pro IP
    limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s;

    # Für Login-Endpunkte: Strenger (5 Requests/Minute)
    limit_req_zone $binary_remote_addr zone=login_limit:10m rate=5r/m;
}
# Im Server-Block anwenden
server {
    listen 443 ssl;
    http2 on;
    server_name api.example.com;
    include snippets/ssl-wildcard.conf;

    # Allgemeines Rate Limit für die API
    location / {
        limit_req zone=api_limit burst=20 nodelay;
        limit_req_status 429;

        proxy_pass http://127.0.0.1:8080;
        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;
    }

    # Strengeres Limit für Login
    location /api/auth/login {
        limit_req zone=login_limit burst=3 nodelay;
        limit_req_status 429;

        proxy_pass http://127.0.0.1:8080;
        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;
    }
}

Parameter:

  • rate=10r/s — Maximal 10 Requests pro Sekunde
  • burst=20 — Bis zu 20 Anfragen dürfen kurzzeitig „vorauseilen“
  • nodelay — Burst-Anfragen sofort bedienen, nicht verzögern
  • limit_req_status 429 — HTTP 429 (Too Many Requests) statt dem Standard 503

IP-Whitelisting

Für Admin-Panels oder interne Services — nur bestimmte IPs zulassen:

# Nur bestimmte IPs erlauben
location /admin {
    allow 203.0.113.10;       # Dein Büro
    allow 198.51.100.0/24;    # VPN-Netz
    deny all;                  # Alles andere blockieren

    proxy_pass http://127.0.0.1:9090;
    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;
}

Geo-basiertes Blocking (optional)

# Bestimmte IPs oder Netze komplett blocken
geo $blocked {
    default 0;
    # Bekannte böswillige Netze
    192.0.2.0/24 1;
    198.51.100.0/24 1;
}

server {
    if ($blocked) {
        return 403;
    }
    # ...
}

Security-Header für alle Proxied Services

# /etc/nginx/snippets/security-headers.conf
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Content-Security-Policy "default-src 'self'" always;

Einbinden mit include snippets/security-headers.conf; in jedem Server-Block.

13. Zusammenfassung & Checkliste

Hier ist deine Checkliste für jeden neuen Service hinter dem Reverse Proxy:

☐ Service-Checkliste

  1. Backend-Service läuft und bindet an 127.0.0.1:PORT
  2. Systemd-Service oder Docker-Container mit restart-Policy erstellt
  3. DNS-Eintrag (A-Record) zeigt auf deinen Server
  4. Nginx-Konfiguration in /etc/nginx/sites-available/ erstellt
  5. Symlink in /etc/nginx/sites-enabled/ gesetzt
  6. sudo nginx -t erfolgreich ausgeführt
  7. Nginx reload durchgeführt
  8. SSL-Zertifikat mit Certbot eingerichtet
  9. HTTP → HTTPS Redirect konfiguriert
  10. Firewall prüfen: Nur Port 80 und 443 offen
  11. Backend-Port nicht in der Firewall geöffnet

Empfohlene Firewall-Regeln

# UFW: Nur HTTP(S) und SSH erlauben
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow ssh
sudo ufw allow 'Nginx Full'
sudo ufw enable
sudo ufw status

Kurzfassung der wichtigsten Direktiven

DirektiveZweck
proxy_passZiel-URL des Backend-Service
proxy_set_headerHTTP-Header an Backend durchreichen
proxy_http_version 1.1Nötig für WebSockets und Keepalive
upstreamGruppe von Backend-Servern (Load Balancing)
proxy_cacheResponse-Caching aktivieren
limit_reqRate Limiting pro IP
proxy_buffering offFür Streaming/SSE deaktivieren

14. Troubleshooting

502 Bad Gateway

Der häufigste Fehler. Bedeutet: Nginx konnte keine Verbindung zum Backend herstellen.

# 1. Läuft der Backend-Service überhaupt?
sudo systemctl status myapp
# oder
docker ps

# 2. Lauscht er auf dem richtigen Port?
sudo ss -tlnp | grep 3000

# 3. Ist er lokal erreichbar?
curl -v http://127.0.0.1:3000

# 4. Nginx Error-Log prüfen
sudo tail -f /var/log/nginx/error.log

Häufige Ursachen:

  • Service nicht gestartet
  • Service bindet an anderen Port oder andere IP
  • SELinux blockiert die Verbindung (bei aktiviertem SELinux: setsebool -P httpd_can_network_connect 1)
  • Falsche proxy_pass URL (Trailing Slash beachten!)

504 Gateway Timeout

# Backend antwortet zu langsam — Timeouts erhöhen
proxy_connect_timeout 300s;
proxy_send_timeout 300s;
proxy_read_timeout 300s;

Trailing Slash — Die Falle

Aufgepasst beim Trailing Slash in proxy_pass:

# Anfrage: /app/hello

# OHNE Trailing Slash — Pfad wird durchgereicht
location /app/ {
    proxy_pass http://127.0.0.1:3000;
    # Backend erhält: /app/hello
}

# MIT Trailing Slash — Pfad wird umgeschrieben
location /app/ {
    proxy_pass http://127.0.0.1:3000/;
    # Backend erhält: /hello  (ohne /app)
}

Header-Probleme debuggen

# Alle Header anzeigen, die beim Backend ankommen
curl -v https://app.example.com 2>&1 | grep -i "^[<>]"

# Oder im Backend loggen — Node.js Beispiel:
app.use((req, res, next) => {
    console.log('Headers:', JSON.stringify(req.headers, null, 2));
    next();
});

WebSocket-Verbindung bricht ab

  • Prüfe, ob proxy_http_version 1.1 gesetzt ist
  • Prüfe die Upgrade und Connection Header
  • Erhöhe proxy_read_timeout für langlebige Verbindungen
  • Wenn Cloudflare davor liegt: WebSockets müssen im Cloudflare-Dashboard aktiviert sein

Nützliche Debug-Befehle

# Nginx-Konfiguration validieren
sudo nginx -t

# Nginx-Konfiguration im Detail anzeigen
sudo nginx -T

# Aktive Verbindungen anzeigen
sudo nginx -T 2>/dev/null | grep -c "server_name"

# Echtzeit Error-Log
sudo tail -f /var/log/nginx/error.log

# Echtzeit Access-Log
sudo tail -f /var/log/nginx/access.log

# Prüfen, welche Ports belegt sind
sudo ss -tlnp

# DNS-Auflösung testen
dig app.example.com +short

15. Nächste Schritte

Du hast jetzt ein solides Verständnis von Nginx als Reverse Proxy. Hier sind einige Richtungen, in die du weitergehen kannst:

  • Monitoring: Nginx-Metriken mit Prometheus und Grafana überwachen — stub_status-Modul aktivieren
  • Automatisierung: Ansible oder Terraform für Infrastructure-as-Code
  • Container-Orchestrierung: Traefik als Alternative zu Nginx im Docker/Kubernetes-Umfeld
  • Zero-Downtime Deployments: Rolling Updates mit upstream und Health Checks
  • ModSecurity: Web Application Firewall (WAF) für Nginx
  • HTTP/3 (QUIC): Nginx unterstützt experimentell HTTP/3 — die Zukunft des Web-Protokolls

Im nächsten Tutorial der Serie schauen wir uns an, wie du Nginx-Performance tuning betreibst — Worker-Prozesse, Connection-Limits und Kernel-Parameter für maximale Performance unter Last.

Fragen oder Probleme? Schreib einen Kommentar — ich helfe gerne!