Die Konfiguration von nginx ist übersichtlich aufgebaut, hat aber einige Eigenheiten, die zu schwer auffindbaren Fehlern führen. Dieser Artikel erklärt den Aufbau, die wichtigsten Blöcke und die Fallen.

Der Aufbau

nginx-Konfiguration ist hierarchisch. Einstellungen werden nach unten vererbt und können überschrieben werden:

# /etc/nginx/nginx.conf
user  www-data;
worker_processes  auto;          # ein Prozess je CPU-Kern
error_log  /var/log/nginx/error.log warn;
pid        /run/nginx.pid;

events {
    worker_connections 1024;     # Verbindungen je Prozess
    use epoll;                   # effizienteste Methode unter Linux
}

http {
    # Basis
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;
    sendfile      on;            # effizientes Senden aus dem Dateizwischenspeicher
    tcp_nopush    on;
    tcp_nodelay   on;
    server_tokens off;           # Versionsnummer nicht verraten

    # Zeitlimits
    client_body_timeout   12s;
    client_header_timeout 12s;
    keepalive_timeout     65s;
    send_timeout          10s;

    # Groessen
    client_max_body_size  10m;   # Upload-Grenze! Standard 1m
    client_body_buffer_size 128k;
    large_client_header_buffers 4 16k;

    # Kompression
    gzip              on;
    gzip_vary         on;
    gzip_min_length   1000;
    gzip_comp_level   6;
    gzip_types        text/plain text/css application/json
                      application/javascript text/xml application/xml;

    # Protokolle
    log_format main '$remote_addr - $remote_user [$time_local] "$request" '
                    '$status $body_bytes_sent "$http_referer" '
                    '"$http_user_agent" rt=$request_time';
    access_log /var/log/nginx/access.log main;

    # Konfigurationen einbinden - getrennt je Seite ist uebersichtlicher
    include /etc/nginx/conf.d/*.conf;
    include /etc/nginx/sites-enabled/*;
}

Ein Server-Block

# /etc/nginx/sites-available/app.example.org
server {
    listen 80;
    listen [::]:80;
    server_name app.example.org www.app.example.org;

    # Alles auf HTTPS umleiten - als erster und eigener Server-Block
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl;
    listen [::]:443 ssl;
    http2 on;
    server_name app.example.org www.app.example.org;

    # TLS
    ssl_certificate     /etc/letsencrypt/live/app.example.org/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/app.example.org/privkey.pem;
    ssl_protocols       TLSv1.2 TLSv1.3;
    ssl_prefer_server_ciphers off;    # bei TLS 1.3 ueberfluessig
    ssl_session_cache   shared:SSL:10m;
    ssl_session_timeout 1d;
    ssl_stapling        on;
    ssl_stapling_verify on;

    # Sicherheits-Kopfzeilen
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
    add_header X-Content-Type-Options    "nosniff" always;
    add_header X-Frame-Options           "SAMEORIGIN" always;
    add_header Referrer-Policy           "strict-origin-when-cross-origin" always;
    add_header Content-Security-Policy   "default-src 'self'" always;

    root /var/www/app;
    index index.html;

    # Statische Dateien direkt ausliefern - schnell und ohne Anwendung
    location /static/ {
        expires 30d;
        add_header Cache-Control "public, immutable";
        access_log off;
    }

    # Anwendung
    location / {
        proxy_pass http://127.0.0.1:8080;
        proxy_http_version 1.1;
        proxy_set_header Connection        "";
        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;

        # Zeitlimits bewusst setzen
        proxy_connect_timeout 5s;
        proxy_send_timeout    60s;
        proxy_read_timeout    60s;

        # Puffer
        proxy_buffering on;
        proxy_buffers   8 16k;
    }

    # Interner Zustandspunkt - nur lokal erreichbar
    location = /health {
        allow 127.0.0.1;
        deny all;
        proxy_pass http://127.0.0.1:8080/health;
    }

    # Versteckte Dateien sperren
    location ~ /\. {
        deny all;
        access_log off;
    }
}

Regeln für location: die Rangfolge

Diese Reihenfolge ist die häufigste Fehlerquelle. nginx prüft in fester Ordnung:

1. location = /pfad        (exakt)         - hoechste Prioritaet
2. location ^~ /pfad       (Praefix, stoppt Regex-Pruefung)
3. location ~  regex       (Regex, in Reihenfolge des Auftretens)
4. location ~* regex       (Regex ohne Gross-/Kleinschreibung)
5. location /pfad          (Praefix)       - laengster Treffer gewinnt
6. location /              (Standard)

# Beispiele
location = /login          nur exakt /login
location ^~ /static/       alles unter /static/, keine Regex danach
location ~ \.php$          jede .php-Anfrage
location /api/             alles unter /api/

# Typischer Fehler: ^~ vergessen
location /static/   { }    # eine spaetere Regex greift vielleicht
location ~ \.(css|js)$ { } # ... und ueberschreibt die Einstellung

Umleitung und Umschreibung

# 301 dauerhaft, 302 voruebergehend - IMMER eine Weiterleitung bei Regeln
# angeben, sonst wird intern umgeschrieben.

# Praefix ersetzen
location /alt/ {
    return 301 /neu/;
}

# Regex-Umschreibung mit Umleitung
rewrite ^/blog/(\d{4})/(\d{2})/(.*)$ /artikel/$1-$2-$3 permanent;

# Intern umschreiben (keine Umleitung)
rewrite ^/konto$ /konto.php last;

# Trailing Slash erzwingen
location /ordner {
    return 301 /ordner/;
}

# www auf ohne-www umleiten
server {
    listen 443 ssl;
    server_name www.example.org;
    return 301 https://example.org$request_uri;
}

# Fuer Weiterleitungen immer $request_uri verwenden, nicht $uri:
#   $request_uri enthaelt die Abfrageparameter, $uri nicht.

Häufige Fallstricke

  1. add_header wird vererbt – aber nur, wenn kein eigener add_header im inneren Block steht. Setzt man in einem location einen eigenen add_header, verliert man alle Kopfzeilen der äußeren Blöcke. Entweder konsequent always verwenden und in jedem betroffenen Block wiederholen – oder mit einer Include-Datei arbeiten.
  2. client_max_body_size Standard ist 1 MB. Wer größere Uploads braucht, muss den Wert erhöhen. Die Meldung 413 Request Entity Too Large ist die Folge.
  3. Fehlermeldungen der Anwendung landen als 502. 502 Bad Gateway heißt meist: Die Anwendung lauscht nicht auf dem angegebenen Port oder läuft nicht. Zuerst ss -tlnp | grep 8080 prüfen.
  4. root ohne try_files. Bei einer Anwendung mit eigenen Routen (Single Page Application) muss die Anfrage auf index.html fallen, sonst liefert ein direktes Aufrufen einer Unterseite 404:
    location / {
        try_files $uri $uri/ /index.html;
    }
  5. Protokollpuffer bei proxy_buffering off. Ohne Puffer sammelt nginx alles im Speicher – bei großen Antworten problematisch.
  6. Doppelte server_name. Zwei Blöcke mit demselben Namen: Der zuerst gelesene gewinnt, der zweite wird ignoriert. nginx warnt beim Test, aber die Warnung wird übersehen.
  7. Standard-Server nicht gesetzt. Ohne default_server beantwortet der erste Server-Block alle Anfragen mit unbekanntem Hostnamen – oft mit einer Fehlerseite einer anderen Seite.

Prüfen und neu laden

# Konfiguration pruefen - IMMER vor dem Neuladen
nginx -t
nginx -T                       # die vollstaendige wirksame Konfiguration ausgeben
nginx -T | grep -A5 'server_name app'

# Neu laden ohne Verbindungsabbruch
systemctl reload nginx
#   reload ist immer besser als restart: bestehende Verbindungen
#   werden noch bedient, neue Prozesse uebernehmen.

# Bei Syntaxfehler bleibt die alte Konfiguration aktiv - gut so.
# Erzwingen: nginx -s reload

# Logs
tail -f /var/log/nginx/access.log
tail -f /var/log/nginx/error.log
journalctl -u nginx --since '10 min ago'

# Zustand
nginx -V                       # Version und Kompilieroptionen
curl -I http://localhost/      # Kopfzeilen pruefen
systemctl status nginx

Debuggen: was nginx wirklich tut

# Kopfzeilen einer Anfrage ansehen (auch die eigenen add_header)
curl -sSI https://app.example.org | grep -iE '^(http|server|x-|strict|content-security)'

# Welche Adresse/Geschwindigkeit?
curl -sS -o /dev/null -w 'HTTP %{http_code}  %{time_total}s  %{size_download} Bytes\n' \
  https://app.example.org/

# nginx-Protokoll erweitern: Variablen zeigen, welcher Block greift
#   In jedem server-Block:  add_header X-Server-Block "app" always;
#   In jedem location:      add_header X-Location "api" always;
# Danach einmal abrufen und die Kopfzeilen ansehen - damit ist klar,
# welche Regel tatsaechlich gegriffen hat.

# TLS pruefen
openssl s_client -connect app.example.org:443 -servername app.example.org </dev/null 2>/dev/null | \
  openssl x509 -noout -subject -issuer -dates

Verwandte Grundlagen: nginx, nginx-Befehle, Reverse-Proxy, Load Balancing.