Auf einem nginx-Server laufen praktisch nie nur eine, sondern mehrere Sites parallel — Hauptdomain, Subdomains, ein paar Tools, vielleicht eine Staging-Variante. Damit jeder Request beim richtigen Vhost landet, ist die genaue Funktion von listen und server_name zentral. Dieser Artikel geht in die Tiefe der Server-Blöcke: alle Flags der listen-Direktive, die exakten Match-Regeln für server_name (Wildcards, Regex, Spezial-Namen), die etablierten Patterns für HTTP-zu-HTTPS-Redirect und www-Kanonisierung, und liefert drei vollständige, praxisnahe Beispiel-Vhosts.

Was ein Server-Block ist und warum mehrere Sites ein einem Server

Ein Server-Block (server { ... }) ist die nginx-Repräsentation eines einzelnen virtuellen Hosts. Innerhalb der nginx-Hauptkonfiguration können beliebig viele Server-Blöcke existieren — jeder davon entspricht einer Site, einer Domain, einem Dienst. Mehrere Blöcke teilen sich die gleichen physischen Listen-Ports (80, 443) und werden über den Host-Header des HTTP-Requests auseinandergehalten.

Das nennt sich name-based virtual hosting: Der Client schickt im Request einen Host: mydomain.com-Header, nginx vergleicht den mit der server_name-Direktive jedes Blocks und wählt den passenden aus. Die Alternative — IP-based virtual hosting — gibt jeder Site eine eigene IP-Adresse und wählt anhand der Ziel-IP. In Zeiten knapper IPv4-Adressen ist letzteres unüblich; name-based ist der Standard.

Praktische Konsequenz: Auf einem einzigen 4-€-VPS lassen sich dutzende kleine Sites betreiben — alle teilen sich CPU, RAM und IP, jede bekommt aber ihre eigene Domain mit eigenem Konfigurations-Block.

Die listen-Direktive im Detail

listen legt fest, wo ein Server-Block erreichbar ist — Adresse, Port und einige zusätzliche Flags. Vollständige Form:

Nginx
listen [<address>:]<port> [default_server] [ssl] [http2] [http3] [proxy_protocol] [reuseport] [...];

Die wichtigsten Varianten in der Praxis:

DirektiveBedeutung
listen 80;Port 80 auf allen IPv4-Interfaces
listen [::]:80;Port 80 auf allen IPv6-Interfaces (notwendig für IPv6-Erreichbarkeit)
listen 192.0.2.10:80;Nur auf einer bestimmten IP — bei Multi-IP-Setups
listen 127.0.0.1:8080;Nur Loopback — typisch für Backends hinter Reverse-Proxy
listen 80 default_server;Markiert diesen Block als Default für Port 80
listen 443 ssl;Port 443 mit TLS aktiv — Zertifikat im Block nötig
listen 443 ssl default_server;Default-Server für 443

Wichtig zu verstehen: Pro Listen-Port kann nur ein Server-Block das default_server-Flag tragen. Welcher das ist, entscheidet die Reihenfolge des Konfig-Ladens — wenn du default_server nicht explizit setzt, nimmt nginx den ersten Block, der den Port öffnet, automatisch als Default.

Die kombinierte Listen-Zeile ist auf modernen nginx-Versionen die Norm:

Nginx Mehrere Flags pro listen
listen 443 ssl default_server;
listen [::]:443 ssl default_server;
http2 on;

Beachte: Auf nginx-Versionen vor 1.25.1 wurde http2 als Listen-Flag geschrieben (listen 443 ssl http2;). Seit 1.25.1 ist das eine eigene Direktive auf Server-Block-Ebene (http2 on;), die alte Form ist deprecated, aber noch unterstützt. Bei Distro-Paketen vor Ubuntu 24.04 / Debian 13 / EL 9 ist die alte Form noch gültig — beide tun dasselbe.

reuseport für hochlast-Server: Mit listen 80 reuseport; öffnet jeder Worker-Prozess seinen eigenen Listen-Socket statt eines geteilten. Das verbessert die Skalierung auf vielen Cores deutlich, kostet aber etwas RAM. Für Server mit weniger als ~5000 Requests/Sekunde unnötig.

server_name im Detail

server_name definiert, welche Hostnamen dieser Server-Block bedient. nginx vergleicht den Host-Header des eingehenden Requests mit dem Wert dieser Direktive. Vier Match-Varianten — laut offizieller nginx-Doku in dieser exakten Priorität:

PrioritätFormBeispielMatch
1Exakter Namemydomain.comnur exakt diese Domain
2Wildcard mit führendem **.mydomain.comalle Subdomains von mydomain.com
3Wildcard mit folgendem *mail.*mail.com, mail.org, mail.de ...
4Regulärer Ausdruck`~^(www.)?mydomain.(comde)$`

nginx prüft in dieser Reihenfolge: erst exakte Treffer, dann Wildcards in beide Richtungen, zuletzt Regex. Beim ersten Treffer stoppt die Suche.

Mehrere Namen in einem Block werden mit Leerzeichen getrennt:

Nginx
server_name mydomain.com www.mydomain.com api.mydomain.com;

Wildcards mit führendem * sind die häufigste Variante:

Nginx
server_name *.mydomain.com;
# matcht: www.mydomain.com, api.mydomain.com, foo.bar.mydomain.com
# matcht NICHT: mydomain.com (ohne Subdomain)

Wenn die Hauptdomain mit abgedeckt sein soll, gibt es eine Kurzform mit führendem Punkt:

Nginx
server_name .mydomain.com;
# matcht: mydomain.com UND www.mydomain.com UND alle weiteren Subdomains

Regex beginnt mit ~ und sollte mit ^ und $ verankert sein:

Nginx
server_name ~^(www\.)?mydomain\.(com|de|org)$;

Regex ist die langsamste Match-Form (sequentiell durchlaufen) und sollte nur eingesetzt werden, wenn Wildcards nicht reichen — z. B. bei mehreren TLDs oder dynamischen Mustern wie ~^kunde\d+\.mydomain\.com$.

Spezial-Namen, die häufig vorkommen:

WertBedeutung
""Matcht Requests ohne Host-Header (selten, aber möglich bei alten Bots)
_Hat keine spezielle Bedeutung — ist nur ein ungültiger Domain-Name. Konventionell als Platzhalter in default_server-Blöcken verwendet, weil er garantiert nichts trifft
$hostnameSetzt den lokalen Hostname des Servers ein

Wichtige Klarstellung zu _: Der Underscore ist kein Catch-all-Mechanismus. Er matcht nichts. Was Server-Blöcke zu Catch-alls macht, ist ausschließlich das default_server-Flag in der listen-Direktive. server_name _; ist Konvention dafür, dass der server_name bewusst nichts matchen soll, weil das Routing über default_server läuft.

Beispiel: zwei Vhosts auf einem Server

Ein Mini-Setup mit zwei Sites — Hauptseite und Blog auf einer Subdomain:

Nginx /etc/nginx/sites-available/mydomain.com
server {
    listen 80;
    listen [::]:80;
    server_name mydomain.com www.mydomain.com;

    root /var/www/mydomain.com;
    index index.html;

    location / {
        try_files $uri $uri/ =404;
    }
}
Nginx /etc/nginx/sites-available/blog.mydomain.com
server {
    listen 80;
    listen [::]:80;
    server_name blog.mydomain.com;

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

    location / {
        try_files $uri $uri/ =404;
    }
}

Beide aktivieren und testen:

Bash
sudo ln -s /etc/nginx/sites-available/mydomain.com /etc/nginx/sites-enabled/
sudo ln -s /etc/nginx/sites-available/blog.mydomain.com /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx

Wenn DNS für beide Domains auf den Server zeigt, lieferst du jetzt zwei verschiedene Sites aus einer einzigen IP-Adresse aus. Test ohne DNS möglich:

Bash Mit explizitem Host-Header
curl -H "Host: mydomain.com"      http://203.0.113.42/
curl -H "Host: blog.mydomain.com" http://203.0.113.42/

Pattern: HTTP zu HTTPS umleiten

Sobald TLS eingerichtet ist (siehe nächste Artikel zu Let's Encrypt), gehört es zum guten Ton, Port-80-Requests sofort auf HTTPS umzulenken — sonst wird die Site potentiell unverschlüsselt angesprochen, was Mitlesen erlaubt und für Suchmaschinen ein Negativsignal ist.

Saubere Lösung mit zwei separaten Server-Blöcken:

Nginx /etc/nginx/sites-available/mydomain.com
# HTTP → HTTPS Umleitung
server {
    listen 80;
    listen [::]:80;
    server_name mydomain.com www.mydomain.com;

    # ACME-Challenge für Let's-Encrypt-Renewals weiterhin per HTTP zulassen
    location /.well-known/acme-challenge/ {
        root /var/www/html;
    }

    # Alles andere auf HTTPS
    location / {
        return 301 https://$host$request_uri;
    }
}

# HTTPS — die eigentliche Site
server {
    listen 443 ssl;
    listen [::]:443 ssl;
    http2 on;
    server_name mydomain.com www.mydomain.com;

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

    root /var/www/mydomain.com;
    index index.html;

    location / {
        try_files $uri $uri/ =404;
    }
}

Erläuterungen:

  • return 301 ist ein permanenter Redirect — Browser und Suchmaschinen merken sich die Umleitung dauerhaft, bei späteren Requests gehen sie direkt auf HTTPS.
  • $host$request_uri rekonstruiert den Original-URI: $host ist die angefragte Domain (aus dem Host-Header), $request_uri ist der angeforderte Pfad inklusive Query-String.
  • /.well-known/acme-challenge/ wird vor der Catch-all-Umleitung erlaubt. Das ist nötig, damit Let's-Encrypt-Renewals weiterhin per HTTP funktionieren — der ACME-Authenticator legt eine Challenge in dieses Verzeichnis und holt sie über Port 80 ab, bevor das neue Zertifikat ausgestellt wird.
  • return ist effizienter als rewrite — das ist ein häufiger Fehler. rewrite ... permanent; funktioniert auch, ist aber komplexer und langsamer; return 301 ... ist die empfohlene Form.

Pattern: www zu non-www (oder umgekehrt) kanonisieren

SEO-Tools mahnen, dass https://mydomain.com und https://www.mydomain.com als zwei verschiedene Sites zählen, wenn beide ohne Redirect zu erreichen sind. Die saubere Lösung: eine Variante als kanonisch festlegen, die andere umleiten.

Variante A — non-www ist kanonisch, www wird umgeleitet:

Nginx
server {
    listen 443 ssl;
    listen [::]:443 ssl;
    server_name www.mydomain.com;

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

    return 301 https://mydomain.com$request_uri;
}

server {
    listen 443 ssl;
    listen [::]:443 ssl;
    http2 on;
    server_name mydomain.com;

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

    root /var/www/mydomain.com;
    # ... weitere Direktiven
}

Variante B — www ist kanonisch: dieselbe Struktur, nur Roles vertauschen.

Welche Variante wählen? Es gibt keinen technischen Grund, eine zu bevorzugen — beide sind gleich valide. Für moderne, mobile-first-Sites ist die kürzere non-www-Form üblicher; für klassische Marken-Websites ist www verbreiteter. Wichtig ist nur die Konsistenz — eine Variante festlegen und überall (Sitemap, internal Links, Mail-Footer, Visitenkarte) die kanonische Form verwenden.

Wichtig: Das Zertifikat muss beide Domains abdecken. Bei Let's Encrypt läuft das über certbot ... -d mydomain.com -d www.mydomain.com — das Zertifikat enthält beide Subject Alternative Names.

Vollständiges Beispiel 1: Static-Site

Ein typischer Static-Site-Server (z. B. für eine mit Astro, Hugo, Jekyll generierte Website):

Nginx /etc/nginx/sites-available/mydomain.com
# HTTP → HTTPS
server {
    listen 80;
    listen [::]:80;
    server_name mydomain.com www.mydomain.com;

    location /.well-known/acme-challenge/ {
        root /var/www/html;
    }

    location / {
        return 301 https://mydomain.com$request_uri;
    }
}

# HTTPS für www → kanonisches non-www
server {
    listen 443 ssl;
    listen [::]:443 ssl;
    server_name www.mydomain.com;

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

    return 301 https://mydomain.com$request_uri;
}

# HTTPS — die eigentliche Site
server {
    listen 443 ssl;
    listen [::]:443 ssl;
    http2 on;
    server_name mydomain.com;

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

    root /var/www/mydomain.com;
    index index.html;

    # Sicherheits-Header
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;

    # Static Assets aggressiv cachen
    location ~* \.(jpg|jpeg|png|gif|webp|svg|css|js|woff2?)$ {
        expires 1y;
        access_log off;
        add_header Cache-Control "public, immutable";
    }

    # Default-Routing
    location / {
        try_files $uri $uri/ $uri.html =404;
    }

    access_log /var/log/nginx/mydomain.com.access.log;
    error_log  /var/log/nginx/mydomain.com.error.log warn;
}

Vollständiges Beispiel 2: Reverse-Proxy auf eine App

Eine Node.js-, Python- oder Go-App, die auf 127.0.0.1:3000 lauscht:

Nginx /etc/nginx/sites-available/app.mydomain.com
server {
    listen 80;
    listen [::]:80;
    server_name app.mydomain.com;

    location /.well-known/acme-challenge/ {
        root /var/www/html;
    }

    location / {
        return 301 https://$host$request_uri;
    }
}

server {
    listen 443 ssl;
    listen [::]:443 ssl;
    http2 on;
    server_name app.mydomain.com;

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

    # Größere Uploads erlauben
    client_max_body_size 25M;

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

        # Header an die Backend-App weitergeben
        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_set_header   Upgrade           $http_upgrade;
        proxy_set_header   Connection        "upgrade";

        # Timeouts großzügig
        proxy_read_timeout 300s;
        proxy_connect_timeout 60s;
    }

    access_log /var/log/nginx/app.mydomain.com.access.log;
    error_log  /var/log/nginx/app.mydomain.com.error.log warn;
}

Wichtige Punkte für Reverse-Proxy:

  • proxy_set_header X-Forwarded-Proto $scheme; ist wichtig, damit die Backend-App weiß, dass der Original-Request HTTPS war — sonst baut sie ggf. Redirects mit http:// auf, was Mixed-Content-Probleme erzeugt.
  • Upgrade/Connection-Header sind für WebSocket-fähige Apps Pflicht (z. B. moderne Live-Chat-, Collaboration- oder Streaming-Apps). Bei reinen REST-APIs ohne WebSockets können sie weggelassen werden, schaden aber nicht.
  • client_max_body_size im Default 1 MB — bei Datei-Uploads über das Limit hebt nginx die Verbindung mit 413 Request Entity Too Large ab. Bewusst auf den Bedarf der App setzen.

Vollständiges Beispiel 3: PHP-FPM (z. B. WordPress)

Klassische LEMP-Stack-Konfiguration für eine PHP-Site:

Nginx /etc/nginx/sites-available/cms.mydomain.com
server {
    listen 443 ssl;
    listen [::]:443 ssl;
    http2 on;
    server_name cms.mydomain.com;

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

    root /var/www/cms.mydomain.com;
    index index.php index.html;

    client_max_body_size 64M;

    # Default — try_files mit PHP als Fallback
    location / {
        try_files $uri $uri/ /index.php?$args;
    }

    # PHP-Dateien an PHP-FPM weiterreichen
    location ~ \.php$ {
        include snippets/fastcgi-php.conf;
        fastcgi_pass unix:/run/php/php8.3-fpm.sock;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    }

    # Static Assets aggressiv cachen
    location ~* \.(jpg|jpeg|png|gif|webp|svg|css|js|woff2?)$ {
        expires 30d;
        access_log off;
        add_header Cache-Control "public";
    }

    # Sensible Pfade explizit blockieren (typische CMS-Lecks)
    location ~ /\.(?!well-known).* {
        deny all;
    }

    access_log /var/log/nginx/cms.mydomain.com.access.log;
    error_log  /var/log/nginx/cms.mydomain.com.error.log warn;
}

Erläuterungen zu den PHP-spezifischen Direktiven:

  • location ~ \.php$ matcht alle URIs, die auf .php enden — diese werden nicht direkt ausgeliefert, sondern an PHP-FPM weitergeleitet.
  • fastcgi_pass unix:/run/php/php8.3-fpm.sock — Unix-Socket der PHP-FPM-Pool-Konfiguration. Auf Debian/Ubuntu ist der Pfad php8.3-fpm.sock (Version anpassen!). Auf RHEL-Familie meistens /run/php-fpm/www.sock.
  • include snippets/fastcgi-php.conf — Distro-Snippet, das die Standard-FastCGI-Parameter setzt. Wer diese Datei nicht hat, muss fastcgi_split_path_info, fastcgi_index und ähnliches manuell pflegen.
  • location ~ /\.(?!well-known).* — verhindert, dass Dotfiles (.git, .env, .htaccess) ausgeliefert werden. Die Negative-Lookahead-Klausel erlaubt /.well-known/ weiter (für ACME-Challenges, Webfinger usw.).

Sicherheits-Checkliste

  • Pro Vhost ein eigener Server-Block — keine Mehrfachnutzung mit if-Konstrukten
  • Sowohl listen 80; als auch listen [::]:80; (und gleiches für 443) — IPv6 nicht vergessen
  • HTTP-zu-HTTPS-Umleitung sauber als separater Server-Block, nicht als if im HTTPS-Block
  • ACME-Challenge-Pfad vor der Catch-all-Umleitung freigegeben
  • default_server explizit definiert — nicht implizit dem ersten Block überlassen
  • Bei mehreren Domains pro Block: alle Namen im Zertifikat (Subject Alternative Names)
  • Kanonische Variante (www oder non-www) festgelegt, andere wird umgeleitet
  • Bei Reverse-Proxy: X-Forwarded-Proto an Backend weitergeben, sonst Mixed-Content-Risiken
  • client_max_body_size an realistische Bedarfe angepasst (Default 1 MB ist oft zu wenig)

Häufige Fehler bei Server-Blöcken

if im server-Block für Redirect-Logik

Es gibt eine bekannte nginx-Konvention: „if is evil" innerhalb von server-Blöcken. if-Statements in nginx haben überraschende Semantik (sie öffnen einen impliziten Sub-Location-Block) und führen oft zu schwer debuggbaren Bugs. Für Redirects ist die saubere Lösung immer ein separater Server-Block mit return 301, nicht ein if ($host = www.mydomain.com)-Konstrukt.

Underscore _ als Catch-all missverstanden

server_name _; allein macht nichts. Nur die Kombination listen ... default_server; plus server_name _; macht den Block zum Catch-all. Wer nur _ setzt, ohne default_server zu nutzen, hat einen Block, der niemals matcht.

HTTPS-Block ohne IPv6-Listen

listen 443 ssl; ohne listen [::]:443 ssl; — IPv6-Verkehr fällt in den IPv6-Default-Server, was meistens nicht der gewünschte Vhost ist. Beide listen-Zeilen sind Pflicht auf Dual-Stack-Servern.

Zertifikat fehlt für www-Variante

Das Zertifikat wurde nur für mydomain.com ausgestellt, der www.mydomain.com-Vhost mit gleichem Zertifikat-Pfad lädt es zwar — Browser melden aber Zertifikatsfehler, weil der Hostname nicht im SAN-Feld steht. Bei Let's-Encrypt-Cert-Erstellung beide Domains angeben: certbot ... -d mydomain.com -d www.mydomain.com.

rewrite statt return für Redirects

rewrite ^/(.*)$ https://$host/$1 permanent; funktioniert, ist aber langsamer und semantisch anders als return 301 https://$host$request_uri;. Für reine Redirects immer return nutzen — rewrite ist für komplexere URI-Manipulationen reserviert.

proxy_pass ohne X-Forwarded-Proto

Das Backend weiß ohne diesen Header nicht, ob der Original-Request HTTPS war. Folge: in der App generierte Links und Redirects nutzen http:// statt https://, der Browser meldet Mixed-Content-Warnungen oder bricht den Request ab. proxy_set_header X-Forwarded-Proto $scheme; ist Pflicht bei jeder Reverse-Proxy-Konfiguration.

Vhost in sites-available, aber Symlink in sites-enabled vergessen

Auf Debian/Ubuntu ist die Datei in sites-available/ allein wirkungslos — sie muss in sites-enabled/ verlinkt werden, damit nginx sie lädt. Klassische Verwirrung: Datei wird editiert, nginx -t läuft sauber, aber die Site reagiert nicht. Lösung: ln -s ../sites-available/<name> /etc/nginx/sites-enabled/.

Verwandte Artikel

Externe Quellen

/ Weiter

Zurück zu Web-Server

Zur Übersicht