LEMP-Stack auf Ubuntu 26.04 LTS einrichten: Nginx, MariaDB, PHP 8.5 & Let’s Encrypt

Produktionsreife Installation des LEMP-Stacks auf Ubuntu 26.04 LTS: Nginx 1.28.x mit HTTP/2 & HTTP/3, MariaDB 11.8, PHP 8.5-FPM Pool-Tuning und Let’s Encrypt TLS.

Lesezeit: 25 min

💡 Hinweis für Bestandsinstallationen (Ubuntu 24.04 LTS): Läuft dein System auf der Vorgängerversion, findest du die passende Dokumentation im Leitfaden LEMP-Stack auf Ubuntu 24.04 LTS einrichten mit Nginx 1.24, MariaDB 10.11 und PHP 8.3.

Unter Ubuntu 26.04 LTS (Resolute Raccoon) setzt sich der LEMP-Stack aus Nginx 1.28.x, MariaDB 11.8 LTS und PHP 8.5-FPM zusammen. Neben HTTP/1.1 und HTTP/2 über TCP unterstützt dieser Aufbau natives HTTP/3 über das QUIC-Transportprotokoll via UDP Port 443.

Dieser Leitfaden beschreibt die Einrichtung und Härtung des Stacks für den Serverbetrieb: von der Firewall-Konfiguration über das InnoDB- und Worker-Pool-Tuning bis hin zur automatischen TLS-Zertifikatsverwaltung mit Certbot und der Signalisierung von HTTP/3 per Alt-Svc.

⚠️ Netzwerkanforderung für HTTP/3: QUIC ist ein Transportprotokoll, das UDP als Grundlage verwendet. Stelle sicher, dass in deiner vorgeschalteten Firewall (z. B. Hetzner, AWS Security Groups oder Proxmox) neben TCP Port 80 und TCP Port 443 auch UDP Port 443 für eingehenden Datenverkehr freigegeben ist.

Systemvoraussetzungen und Komponentenmatrix

Nginx wickelt tausende parallele Verbindungen mit geringem Speicherbedarf ab. Dagegen bestimmen Datenbankabfragen und parallele PHP-Worker direkt den Bedarf an RAM und I/O-Leistung:

Komponente Paketversion in Ubuntu 26.04 Standard-Port / Socket Mindestanforderung Empfohlen für Produktion
Betriebssystem Ubuntu 26.04 LTS - 1 vCPU, 1 GB RAM 2–4 vCPUs, 4–8 GB RAM
Nginx 1.28.x (HTTP/3 & HTTP/2) TCP 80, TCP 443, UDP 443 128 MB RAM 512 MB RAM
MariaDB 11.8.x LTS TCP 3306 (127.0.0.1) 512 MB RAM 2 bis 4 GB RAM (InnoDB Buffer)
PHP-FPM 8.5 /run/php/php8.5-fpm.sock 256 MB RAM 1 bis 2 GB RAM (Worker-Pool)
Storage NVMe / SSD - 15 GB frei 30+ GB mit täglichem Backup

1. Systemvorbereitung und Basisabsicherung

Aktualisiere vor der Paketinstallation das Basissystem und installiere die Werkzeuge für Administration und Absicherung:


# Paketindex aktualisieren und Basissystem auf den neuesten Stand bringen
sudo apt update && sudo apt upgrade -y

# Relevante Administrationswerkzeuge installieren
sudo apt install -y curl wget gnupg2 ca-certificates lsb-release ufw fail2ban

Firewall für Webserver und HTTP/3 QUIC konfigurieren

Gib neben SSH und den Standard-Webports über TCP auch UDP Port 443 für HTTP/3 frei:


# SSH-Port vor Aktivierung der Firewall freigeben
sudo ufw allow 22/tcp comment "SSH Remote Access"

# Standard-Webports freigeben (Nginx Full Profil)
sudo ufw allow "Nginx Full"

# UDP Port 443 für natives HTTP/3 & QUIC freigeben
sudo ufw allow 443/udp comment "HTTP3 QUIC UDP"

# Firewall aktivieren und Status überprüfen
sudo ufw --force enable
sudo ufw status verbose

Aussperrschutz bei SSH: Vergewissere dich vor dem Aktivieren der Firewall mit sudo ufw status, dass Port 22 (oder dein individueller SSH-Port) gelistet ist. Ein vorzeitiges Aktivieren trennt deine aktive Sitzung dauerhaft.

2. Nginx Webserver mit nativem HTTP/3 (QUIC) installieren

Installiere Nginx aus den offiziellen Paketquellen von Ubuntu 26.04:


# Nginx Webserver installieren
sudo apt install -y nginx

# Dienst aktivieren und sofort starten
sudo systemctl enable --now nginx

# Laufzeitstatus verifizieren
sudo systemctl status nginx --no-pager

Build-Konfiguration und HTTP/3-Modul prüfen

Nginx unterstützt HTTP/3 seit Version 1.25.0 über das Modul ngx_http_v3_module. Upstream wird dieses Modul in der offiziellen Dokumentation weiterhin als experimentell geführt, was bei besonders verfügbarkeitskritischen Systemen berücksichtigt werden sollte.

Schließe nicht allein aus der Versionsnummer auf Build-Optionen, sondern prüfe die installierte Binärdatei:


# Prüfen, ob das Nginx-Binary mit HTTP/3-Unterstützung kompiliert wurde
nginx -V 2>&1 | tr ' ' '\n' | grep -E '(http_v3|ssl)'

Interpretation der Ausgabe:

  • --with-http_v3_module: Der Binärcode für QUIC und HTTP/3 ist einkompiliert.
  • built with OpenSSL ...: Zeigt die verknüpfte SSL-Bibliothek an. Nginx empfiehlt für QUIC eine moderne OpenSSL- oder kompatible TLS-Bibliothek. Je nach Bibliotheksversion können bestimmte Funktionen (wie 0-RTT / Early Data) Einschränkungen unterliegen.

Unter Ubuntu folgt Nginx einem modularen Aufbau, der globale Einstellungen von individuellen Domain-Hostings trennt:


┌─────────────────────────────────────────────────────────────┐
│                 Nginx Verzeichnisstruktur                   │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│   /etc/nginx/                                               │
│   ├── nginx.conf          Globale Master-Konfiguration      │
│   ├── conf.d/             Modulare HTTP/3-Snippets          │
│   ├── sites-available/    VHost-Vorlagen (Inaktiv)          │
│   └── sites-enabled/      Aktive VHost-Symlinks             │
│                                                             │
└─────────────────────────────────────────────────────────────┘
  • nginx.conf: Globale Parameter wie Worker-Prozesse, Event-Modelle, Gzip-Kompression und Pufferlimits.
  • conf.d/: Modulare Konfigurationsfragmente für TLS-Parameter, Security-Header oder Upstream-Definitionen.
  • sites-available/: Konfigurationsvorlagen für einzelne Webseiten und Domains.
  • sites-enabled/: Enthält Symlinks auf Dateien in sites-available/. Nur hier verlinkte VHosts lädt Nginx.

3. MariaDB 11.8 LTS absichern und optimieren

Installiere den Datenbank-Server und den Client:


# MariaDB Server und Client installieren
sudo apt install -y mariadb-server mariadb-client

# Dienst aktivieren und starten
sudo systemctl enable --now mariadb

# Status verifizieren
sudo systemctl status mariadb --no-pager

Initialen Sicherheitsassistenten ausführen

Das Skript mariadb-secure-installation schließt Standard-Schwachstellen frischer Installationen:


sudo mariadb-secure-installation

Empfohlene Antworten im interaktiven Assistenten:

  • Enter current password for root (enter for none): [Enter] drücken.
  • Switch to unix_socket authentication: n (unter Ubuntu bereits Standard).
  • Change the root password? n (Root-Login läuft sicher über den lokalen Unix Domain Socket via sudo).
  • Remove anonymous users? Y (entfernt unberechtigte Testbenutzer).
  • Disallow root login remotely? Y (unterbindet externe Root-Verbindungen).
  • Remove test database and access to it? Y (löscht die ungeschützte Testdatenbank).
  • Reload privilege tables now? Y (lädt Rechte unverzüglich neu).

Verzeichnisstruktur und Konfigurations-Tuning


┌─────────────────────────────────────────────────────────────┐
│              MariaDB 11.8 Verzeichnisstruktur               │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│   /etc/mysql/                                               │
│   ├── my.cnf              Zentraler Inkludierungs-Symlink   │
│   └── mariadb.conf.d/     Server-Konfigurationsdateien      │
│       ├── 50-server.cnf   Hauptkonfiguration des Daemon     │
│       └── 60-galera.cnf   Cluster- & Replikationsprofile    │
│                                                             │
└─────────────────────────────────────────────────────────────┘

Lege für betriebliche Anpassungen eine eigene Konfigurationsdatei unter /etc/mysql/mariadb.conf.d/99-performance.cnf an.

⚠️ Veraltete Direktiven: Parameter wie innodb_buffer_pool_instances wurden in modernen MariaDB-Zweigen entfernt bzw. ignoriert. Verwende ausschließlich gültige Variablen für MariaDB 11.8.


[mysqld]
# Netzwerkbindung strikt auf Localhost beschränken
bind-address = 127.0.0.1

# Zeichensatz auf vollständiges UTF-8 mit 4 Bytes setzen
character-set-server = utf8mb4
collation-server     = utf8mb4_unicode_520_ci

# InnoDB Puffer-Optimierung (ca. 50-70 % des zugewiesenen Datenbank-RAMs)
innodb_buffer_pool_size = 1G
innodb_flush_method     = O_DIRECT

# Transaktions-Sicherheit vs. Disk-I/O:
# 1 = Volle ACID-Konformität (Default, kein Datenverlust bei Stromausfall)
# 2 = Höhere I/O-Performance (Risiko: 1-2 Sek. Verlust bei Systemabsturz)
innodb_flush_log_at_trx_commit = 1

# Verbindungslimits und Timeouts
max_connections     = 100
connect_timeout     = 5
wait_timeout        = 600
interactive_timeout = 600
Parameter Standardwert Empfohlener Wert Wirkung & Trade-off
innodb_buffer_pool_size 128 MB 50–70 % des DB-RAM Hält Indizes und Tabellendaten im RAM, reduziert NVMe-Zugriffe
innodb_flush_log_at_trx_commit 1 1 (oder 2 für Bulk-I/O) 1 bietet volle ACID-Konformität; 2 flusht sekündlich und entlastet I/O
innodb_flush_method fsync O_DIRECT Umgeht den Betriebssystem-Cache und verhindert doppelte Pufferung
character-set-server utf8mb4 utf8mb4 Vollständige Unterstützung für Emojis und internationale Zeichensätze

Konfiguration vor dem Neustart validieren

Prüfe die Syntax aller Konfigurationsdateien, bevor du den Dienst neu startest:


# Konfigurationsdateien auf unbekannte Variablen und Syntaxfehler prüfen
sudo mariadbd --help --verbose > /dev/null

# Wenn die Prüfung ohne Fehlercode abschließt, MariaDB neu laden
sudo systemctl restart mariadb

Schlägt die Prüfung fehl, gibt mariadbd eine Fehlermeldung auf stderr aus (z. B. [ERROR] unknown variable), ohne den laufenden Dienst zu unterbrechen.

4. PHP 8.5-FPM und Erweiterungen einrichten

PHP-FPM führt serverseitige Skripte in getrennten Worker-Prozessen aus und kommuniziert über Unix Domain Sockets mit dem Webserver:


# PHP 8.5 FPM und Erweiterungen installieren
sudo apt install -y php8.5-fpm php8.5-mysql php8.5-common php8.5-curl \
                    php8.5-mbstring php8.5-xml php8.5-zip php8.5-intl \
                    php8.5-opcache php8.5-gd php8.5-bcmath

# Dienst aktivieren und Status prüfen
sudo systemctl enable --now php8.5-fpm
sudo systemctl status php8.5-fpm --no-pager

┌─────────────────────────────────────────────────────────────┐
│                PHP 8.5 Verzeichnisstruktur                  │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│   /etc/php/8.5/                                             │
│   ├── fpm/                                                  │
│   │   ├── php-fpm.conf    Globale Master-Settings           │
│   │   ├── php.ini         FPM-Laufzeitkonfiguration         │
│   │   └── pool.d/         Worker-Pools                      │
│   │       └── www.conf    Standard-Pool (www-data Socket)   │
│   └── cli/                                                  │
│       └── php.ini         CLI-Laufzeitkonfiguration         │
│                                                             │
└─────────────────────────────────────────────────────────────┘

PHP-FPM Pool-Tuning nach Arbeitsspeicher-Formel

PHP-FPM startet Worker standardmäßig dynamisch. Um Speicherengpässe (Out of Memory) durch unkontrollierte Prozesse zu verhindern, stimmst du pm.max_children auf den verfügbaren Arbeitsspeicher ab:


# Durchschnittlichen RAM-Bedarf aktiver PHP-FPM-Worker ermitteln (in MB)
ps --no-headers -o rss -C php-fpm8.5 | awk '{total+=$1; count++} END {if (count>0) print int(total/count/1024) " MB"; else print "50 MB (Standard-Richtwert)"}'

# Faustformel zur Berechnung von pm.max_children:
# max_children = (Verfügbarer RAM für PHP in MB) / (Durchschnittlicher RAM pro Worker in MB)

# Rechenbeispiel: 2048 MB reservierter PHP-RAM bei ca. 50 MB pro Worker
echo $(( 2048 / 50 ))
# Ergebnis: 40

💡 RAM-Budgetierung im Gesamtsystem: Diese Formel liefert eine Kapazitätsobergrenze. Vom gesamten physischen Arbeitsspeicher müssen zuvor die Reserven für das Betriebssystem (~500–1.000 MB), den Nginx-Webserver, MariaDB (innodb_buffer_pool_size), den OPcache sowie Puffer für Lastspitzen abgezogen werden, bevor das Restbudget für PHP-Worker vergeben wird.

Passe /etc/php/8.5/fpm/pool.d/www.conf an:


[www]
user = www-data
group = www-data

listen = /run/php/php8.5-fpm.sock
listen.owner = www-data
listen.group = www-data
listen.mode = 0660

pm = dynamic
pm.max_children = 40
pm.start_servers = 10
pm.min_spare_servers = 5
pm.max_spare_servers = 15
pm.max_requests = 1000

pm.status_path = /fpm-status
Direktive Wert Betrieblicher Nutzen
pm = dynamic dynamisch Passt die Anzahl der Worker-Prozesse automatisch an die aktuelle Systemlast an
pm.max_children 40 Obergrenze gleichzeitiger PHP-Anfragen; verhindert Serverabsturz durch RAM-Erschöpfung
pm.max_requests 1000 Recycelt Worker nach 1.000 Requests und begrenzt dadurch die Auswirkungen schleichenden Speicherwachstums oder möglicher Memory Leaks
listen.mode 0660 Gewährt ausschließlich Nginx Zugriff auf den Unix Domain Socket

Lade PHP-FPM neu, um die Konfiguration anzuwenden:


sudo systemctl reload php8.5-fpm

5. Virtual Host mit FastCGI und HTTP/3 konfigurieren

Der Virtual Host liefert statische Assets direkt aus und leitet Anfragen an .php-Dateien über den Unix-Domain-Socket an PHP-FPM weiter:


┌─────────────────────────────────────────────────────────────┐
│            Nginx & PHP 8.5-FPM Request-Flow                 │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│   Client-Anfrage (HTTP/2 TCP 443 oder HTTP/3 UDP 443)       │
│   ▼                                                         │
│   Nginx Webserver                                           │
│   ├── Statische Assets   ──▶ Zero-Copy Auslieferung        │
│   └── location ~ \.php$  ──▶ FastCGI-Proxy-Pass            │
│                                        │                    │
│                                        ▼                    │
│                              Socket: php8.5-fpm.sock        │
│                                        │                    │
│                                        ▼                    │
│                              PHP-FPM Worker-Prozess         │
│                                        │                    │
│                                        ▼                    │
│   Client-Antwort         ◀── HTML / JSON Response          │
│                                                             │
└─────────────────────────────────────────────────────────────┘

Server-Block anlegen

Entferne den Standard-Symlink und erstelle die Konfigurationsdatei /etc/nginx/sites-available/lemp.conf:


# Default-Symlink entfernen
sudo rm -f /etc/nginx/sites-enabled/default

Datei /etc/nginx/sites-available/lemp.conf erstellen:


server {
    listen 80;
    listen [::]:80;
    server_name example.com www.example.com;

    root /var/www/example.com/public;
    index index.php index.html;

    # Ausführliche Logging-Pfade
    access_log /var/log/nginx/example.com.access.log;
    error_log /var/log/nginx/example.com.error.log warn;

    # Sicherheits-Header
    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;

    # Statische Dateien direkt und mit Cache-Headern ausliefern
    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }

    location ~* \.(css|js|jpg|jpeg|png|gif|ico|webp|svg|woff|woff2)$ {
        expires 30d;
        add_header Cache-Control "public, no-transform";
        access_log off;
    }

    # PHP-FPM Weiterleitung via Unix Domain Socket
    location ~ \.php$ {
        include snippets/fastcgi-php.conf;
        fastcgi_pass unix:/run/php/php8.5-fpm.sock;
        fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
        include fastcgi_params;
    }

    # Zugriff auf versteckte Dateien (.git, .env) strikt sperren
    location ~ /\.(?!well-known).* {
        deny all;
        access_log off;
        log_not_found off;
    }
}

Webroot und Berechtigungsmodell einrichten

www-data sollte nicht Eigentümer des gesamten Anwendungscodes sein. Kompromittierte Skripte könnten sonst beliebige PHP-Dateien modifizieren. Der Code gehört dem administrativen Deployment-Benutzer mit Leserechten für www-data. Schreibrechte erhält der Webserver ausschließlich in Verzeichnissen wie storage:


# Webverzeichnis für öffentlichen Code sowie beschreibbaren Speicher anlegen
sudo mkdir -p /var/www/example.com/public
sudo mkdir -p /var/www/example.com/storage

# Eigentümer: Deployment-User (oder root), Gruppe www-data
sudo chown -R $USER:www-data /var/www/example.com

# Standardrechte: Verzeichnisse 755, Dateien 644 (nur Leserecht für www-data)
sudo find /var/www/example.com -type d -exec chmod 755 {} \;
sudo find /var/www/example.com -type f -exec chmod 644 {} \;

# Schreibrechte für www-data NUR auf dedizierte Upload- oder Cache-Verzeichnisse
sudo chown -R www-data:www-data /var/www/example.com/storage
sudo find /var/www/example.com/storage -type d -exec chmod 775 {} \;
sudo find /var/www/example.com/storage -type f -exec chmod 664 {} \;

# Virtual Host aktivieren
sudo ln -sf /etc/nginx/sites-available/lemp.conf /etc/nginx/sites-enabled/

# Syntaxprüfung durchführen und Nginx neu laden
sudo nginx -t && sudo systemctl reload nginx

6. End-to-End Test: Nginx, PHP 8.5 und MariaDB

Prüfe das Zusammenspiel von Webserver, PHP-FPM und Datenbank mit einer Testdatenbank und einem PDO-Skript:


┌─────────────────────────────────────────────────────────────┐
│         End-to-End Test: Nginx, PHP 8.5 & MariaDB           │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│   HTTP GET /db-test.php                                     │
│   ▼                                                         │
│   Nginx Server-Block (HTTP/2 & HTTP/3 QUIC)                 │
│   ▼                                                         │
│   PHP 8.5-FPM (Unix Domain Socket)                          │
│   ▼                                                         │
│   PDO-Verbindung ─────────▶ MariaDB 11.8 (127.0.0.1:3306)   │
│                              ├── UTF8mb4 Verbindung         │
│                              ├── Prepared Statement         │
│                              └── Testdaten verarbeiten      │
│   ▼                                                         │
│   Status-Rückmeldung ──────▶ 200 OK (JSON / HTML)           │
│                                                             │
└─────────────────────────────────────────────────────────────┘

Testdatenbank und Testbenutzer einrichten


# Datenbank-Konsole öffnen
sudo mariadb -u root

SQL-Statements in der MariaDB-Konsole:


-- Testdatenbank und sicheren Benutzer erzeugen
CREATE DATABASE IF NOT EXISTS lemp_test CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_520_ci;
CREATE USER IF NOT EXISTS 'lemp_user'@'localhost' IDENTIFIED BY 'BeispielKennwort2026!#Secure';
GRANT ALL PRIVILEGES ON lemp_test.* TO 'lemp_user'@'localhost';
FLUSH PRIVILEGES;

-- Tabelle anlegen und Beispieldatensatz einfügen
USE lemp_test;
CREATE TABLE IF NOT EXISTS system_status (
    id INT AUTO_INCREMENT PRIMARY KEY,
    component VARCHAR(50) NOT NULL,
    status_msg VARCHAR(100) NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB;

INSERT INTO system_status (component, status_msg) 
VALUES ('LEMP Stack 2026', 'Nginx 1.28, PHP 8.5 und MariaDB 11.8 arbeiten einwandfrei zusammen.');
EXIT;

Sicherheitshinweis zum Passwort: Das oben gezeigte Kennwort ist ein exemplarisches Platzhalterbeispiel. Verwende für produktive Datenbankbenutzer immer ein individuelles, zufallsgeneriertes Passwort.

PHP-Verifikationstestskript erstellen

Erstelle /var/www/example.com/public/db-test.php:


<?php
declare(strict_types=1);

header('Content-Type: application/json; charset=utf-8');

$dsn = 'mysql:host=127.0.0.1;dbname=lemp_test;charset=utf8mb4';
$user = 'lemp_user';
$password = 'BeispielKennwort2026!#Secure';

try {
    $pdo = new PDO($dsn, $user, $password, [
        PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION,
        PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
        PDO::ATTR_EMULATE_PREPARES   => false,
    ]);

    $stmt = $pdo->query('SELECT component, status_msg, created_at FROM system_status LIMIT 1');
    $data = $stmt->fetch();

    echo json_encode([
        'status'    => 'success',
        'php'       => PHP_VERSION,
        'webserver' => $_SERVER['SERVER_SOFTWARE'] ?? 'Nginx',
        'database'  => $data,
    ], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);

} catch (PDOException $e) {
    http_response_code(500);
    echo json_encode([
        'status'  => 'error',
        'message' => $e->getMessage(),
    ]);
}

Rufe das Skript lokal ab:


curl -i http://localhost/db-test.php

⚠️ Sicherheitshinweis: Lösche die Datei db-test.php nach erfolgreicher Verifikation unverzüglich aus dem Webroot (sudo rm /var/www/example.com/public/db-test.php), um keine Datenbankpasswörter oder Tabellenstrukturen öffentlich zu exponieren.

7. TLS-Verschlüsselung mit Let’s Encrypt und HTTP/3-Aktivierung

Für den Produktivbetrieb bezieht Certbot ein kostenloses TLS-Zertifikat von Let’s Encrypt und richtet die automatische Verlängerung ein:

Technische Funktionsweise: Dual-Stack aus TCP und UDP

HTTP/1.1 und HTTP/2 setzen auf TCP auf. HTTP/3 nutzt stattdessen das Transportprotokoll QUIC über UDP:


┌─────────────────────────────────────────────────────────────┐
│             Nginx Dual-Stack: TCP/TLS & UDP/QUIC            │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│   HTTP/1.1 ──┐                                              │
│              ├──▶ TCP:443 ──▶ TLS 1.2/1.3 ──┐               │
│   HTTP/2   ──┘                              │               │
│                                             ├──▶ Nginx      │
│   HTTP/3   ─────▶ UDP:443 ──▶ QUIC/TLS 1.3 ─┘               │
│                                                             │
│   Signal: Alt-Svc kündigt HTTP/3 über UDP an                │
│                                                             │
└─────────────────────────────────────────────────────────────┘
  • QUIC über UDP Port 443: QUIC setzt auf UDP auf und integriert Multiplexing sowie Paketwiederherstellung direkt in das Transportprotokoll. Dadurch entfällt das TCP-Head-of-Line-Blocking: Geht ein Paket verloren, pausieren bei HTTP/2 alle parallelen Streams der Verbindung, während bei HTTP/3 nur der jeweils betroffene Stream wartet.
  • Integrierter TLS-1.3-Handshake: QUIC nutzt als Verschlüsselungsschicht TLS 1.3. Der kryptografische Schlüsselaustausch ist direkt in den Verbindungsaufbau integriert (1-RTT bzw. 0-RTT bei Wiederverbindungen).
  • Die Direktive listen 443 quic reuseport;: Aktiviert den QUIC-Listener für eingehende UDP-Datagramme auf Port 443. Der Parameter reuseport (SO_REUSEPORT) ist insbesondere bei mehreren Nginx-Workern sinnvoll und empfohlen, da der Linux-Kernel eingehende UDP-Datagramme dadurch effizient auf die Worker verteilen kann.
  • Die Rolle des Alt-Svc-Headers: Der Header Alt-Svc: h3=":443"; ma=86400 aktiviert HTTP/3 auf dem Server nicht. Er dient als Ankündigungsmechanismus (Alternative Services): Verbindet sich ein Client zunächst über herkömmliches TCP (HTTP/1.1 oder HTTP/2), signalisiert der Server: „Dieser Dienst ist unter Port 443 zusätzlich per HTTP/3 (h3) via UDP erreichbar; diese Information ist 86.400 Sekunden (24 Stunden) gültig.“ Nachfolgende Anfragen kann der Browser dann eigenständig über UDP anfordern.

┌─────────────────────────────────────────────────────────────┐
│             Produktionsreifer Request-Flow                  │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│   Client-Anfrage                                            │
│   ├── TCP:443 ──▶ TLS 1.2/1.3 (HTTP/1.1 & HTTP/2)           │
│   └── UDP:443 ──▶ QUIC & TLS 1.3 (HTTP/3)                   │
│                   │                                         │
│                   ▼                                         │
│   Nginx Webserver (mit HSTS & Alt-Svc Header)               │
│   ├── Statische Dateien ──▶ Direkt ausgeliefert             │
│   └── Dynamische Pfade  ──▶ FastCGI Unix Domain Socket      │
│                             │                               │
│                             ▼                               │
│   PHP 8.5 Worker ─────────▶ MariaDB 11.8 (Port 3306)        │
│                                                             │
└─────────────────────────────────────────────────────────────┘

Installiere Certbot über das offizielle Snap-Paket und beziehe das Zertifikat:


# Certbot über das offizielle Snap-Paket installieren
sudo snap install core && sudo snap refresh core
sudo snap install --classic certbot
sudo ln -sf /snap/bin/certbot /usr/bin/certbot

# TLS-Zertifikat für die Domain abrufen und Nginx automatisch konfigurieren
sudo certbot --nginx -d example.com -d www.example.com

HTTP/2, HTTP/3 und QUIC im Nginx-Serverblock aktivieren

Ergänze nach Ausstellung des Zertifikats /etc/nginx/sites-available/lemp.conf im SSL-Server-Block um http2 on;, den QUIC-Listener und den Alt-Svc-Header:


server {
    # Standard HTTPS über TCP (HTTP/1.1 und HTTP/2)
    listen 443 ssl;
    listen [::]:443 ssl;
    http2 on;

    # Natives HTTP/3 über UDP
    listen 443 quic reuseport;
    listen [::]:443 quic reuseport;

    server_name example.com www.example.com;

    ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
    include /etc/letsencrypt/options-ssl-nginx.conf;
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;

    # Ausgewogene Protokollunterstützung für maximale Client-Kompatibilität
    ssl_protocols TLSv1.2 TLSv1.3;

    # HTTP/3 Advertisement an Browser signalisieren
    add_header Alt-Svc 'h3=":443"; ma=86400';

    # Konservativer HSTS-Header (1 Jahr)
    add_header Strict-Transport-Security "max-age=31536000" always;

    root /var/www/example.com/public;
    index index.php index.html;

    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }

    location ~ \.php$ {
        include snippets/fastcgi-php.conf;
        fastcgi_pass unix:/run/php/php8.5-fpm.sock;
        fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
        include fastcgi_params;
    }
}

⚠️ Wichtiger Hinweis zu HSTS: Der Header Strict-Transport-Security erzwingt verschlüsselte Verbindungen. Erweiterungen wie includeSubDomains binden ausnahmslos alle bestehenden und künftigen Subdomains an HTTPS. Der Zusatz preload beantragt die Aufnahme in die fest in Browsern verankerte Preload-Liste und ist nur schwer widerrufbar. Setze diese Zusätze erst ein, wenn deine gesamte Domain-Infrastruktur nachweislich durchgehend auf HTTPS umgestellt ist.

Prüfe die Konfiguration und lade Nginx neu:


# Syntax überprüfen und Nginx neu laden
sudo nginx -t && sudo systemctl reload nginx

# Automatischen Certbot Renewal-Timer prüfen
sudo systemctl list-timers | grep certbot

Echten HTTP/3- und QUIC-Funktionstest durchführen

Ein erfolgreicher Aufruf von sudo nginx -t ist kein Funktionstest für HTTP/3. Er bestätigt lediglich, dass Nginx die Konfigurationssyntax akzeptiert und laden kann. Ob Clients tatsächlich per QUIC kommunizieren können, hängt von der Erreichbarkeit des UDP-Ports und dem Client-Support ab.

Vor dem Funktionstest prüfen wir daher zunächst, ob die installierte curl-Version überhaupt mit HTTP/3-Unterstützung gebaut wurde:


# Prüfen, ob das lokale curl-Binary HTTP/3 unterstützt
curl -V | grep -i http3

In der Zeile Features: muss das Schlagwort HTTP3 enthalten sein. Fehlt dieser Eintrag, wurde das Paket ohne QUIC-Bibliothek (wie ngtcp2 oder quiche) gebaut; in diesem Fall schlägt --http3-only bereits clientseitig fehl.

Ist HTTP3 in curl -V vorhanden, führen wir die Verifikation in drei Schritten durch:

1. UDP-Socket-Bindung auf dem Server prüfen

Zunächst kontrollieren wir, ob Nginx tatsächlich auf dem UDP-Port 443 lauscht:


# Kontrollieren, ob Nginx den UDP-Socket gebunden hat
sudo ss -ulnp | grep :443

In der Ausgabe muss ein Datagramm-Socket für den Prozess nginx (UNCONN ... 0.0.0.0:443) gelistet sein.

2. Alt-Svc Header über TCP verifizieren

Wir prüfen, ob Nginx beim regulären TCP-Verbindungsaufbau den Ankündigungs-Header an den Client mitsendet:


# Alt-Svc-Signalisierung abfragen
curl -I https://example.com/ | grep -i alt-svc

Die Antwort muss alt-svc: h3=":443"; ma=86400 enthalten.

3. Echte HTTP/3-Verbindung erzwingen

Nun fordern wir die Ressource gezielt und ausschließlich über das QUIC-Protokoll an:


# Verbindung strikt über HTTP/3 (UDP) erzwingen
curl -Iv --http3-only https://example.com/

Erfolgsindikatoren in der Ausgabe:

  • * Connected to example.com (...) port 443 (UDP): Bestätigt die Kommunikation über UDP statt TCP.
  • * Using HTTP/3: Zeigt die Aushandlung des QUIC-Transports an.
  • HTTP/3 200: Die Statuszeile bestätigt den erfolgreichen Empfang über HTTP/3.

Sollte eine zwischengeschaltete Firewall (z. B. beim Cloud-Hoster) UDP Port 443 blockieren, schlägt dieser Befehl mit einem Timeout oder Verbindungsfehler fehl, während gewöhnliche Aufrufe ohne --http3-only transparent auf HTTP/2 über TCP zurückfallen würden.

💡 Browser-Prüfung: Öffne in Google Chrome, Firefox oder Microsoft Edge die Entwicklertools (F12) und wechsle in den Reiter Netzwerk. Klicke mit der rechten Maustaste auf die Tabellenüberschrift und aktiviere die Spalte Protokoll. Beim ersten Laden der Seite zeigt der Browser noch h2 an; sobald der Alt-Svc-Header registriert wurde, wechselt das Protokoll beim Neuladen auf h3.

Befehlsreferenz (Cheatsheet)

Die folgende Tabelle fasst die wichtigsten Administrationsbefehle für den laufenden Betrieb des LEMP-Stacks zusammen:

Aufgabe Auszuführender Befehl Beschreibung
Nginx Syntaxprüfung sudo nginx -t Prüft alle geladenen Konfigurationsdateien auf Syntaxfehler
Nginx Reload sudo systemctl reload nginx Übernimmt Konfigurationsänderungen ohne Unterbrechung aktiver Verbindungen
HTTP/3 Modulprüfung nginx -V 2>&1 &#124; grep http_v3 Prüft, ob Nginx mit dem HTTP/3-QUIC-Modul gebaut wurde
UDP Socket-Prüfung sudo ss -ulnp &#124; grep :443 Verifiziert, dass Nginx auf UDP Port 443 (QUIC) lauscht
HTTP/3 Funktionstest curl -Iv --http3-only https://example.com/ Erzwingt eine reine HTTP/3-Verbindung über das QUIC-Protokoll
MariaDB Config-Test sudo mariadbd --help --verbose > /dev/null Prüft MySQL/MariaDB-Konfigurationsdateien vor dem Neustart
PHP-FPM Status sudo systemctl status php8.5-fpm Zeigt aktive Worker, Laufzeit und PID des PHP-FPM-Masterprozesses
PHP-FPM Neustart sudo systemctl restart php8.5-fpm Startet den PHP-Master-Prozess und alle konfigurierten Worker-Pools neu
MariaDB Konsole sudo mariadb -u root Öffnet die interaktive SQL-Shell mit lokaler Root-Authentifizierung
MariaDB Neustart sudo systemctl restart mariadb Startet den Datenbank-Dienst neu (z. B. nach Pufferanpassungen)
UFW Firewall-Status sudo ufw status verbose Listet alle aktiven Firewall-Regeln für TCP und UDP auf
Certbot Erneuerungstest sudo certbot renew --dry-run Simuliert den automatischen Zertifikatsabruf ohne Schreibzugriff
FastCGI Socket-Prüfung ls -la /run/php/php8.5-fpm.sock Verifiziert Existenz und Berechtigungen (www-data:www-data 0660)

Weiterführende Ressourcen

Ressource Link Zweck und Inhalt
Ubuntu 26.04 Server Ubuntu Server Guide Offizielle Systemdokumentation für Ubuntu 26.04 LTS (Resolute Raccoon)
Nginx HTTP/3 Dokumentation Nginx QUIC & HTTP/3 Offizieller Konfigurationsleitfaden für native QUIC-Listener und Alt-Svc
MariaDB Knowledge Base MariaDB 11.8 Dokumentation Referenzhandbuch für InnoDB-Optimierung, Systemvariablen und Replikation
PHP 8.5 Dokumentation PHP Manual Vollständige Dokumentation aller Sprachfeatures, FPM-Optionen und OPcache
Certbot Dokumentation Certbot EFF Guide Dokumentation für automatisierte Let’s-Encrypt-Zertifikate unter Nginx
LEMP Stack (Ubuntu 24.04) Ubuntu 24.04 LEMP Leitfaden Referenzartikel für Bestandsinstallationen auf Basis von Ubuntu 24.04 LTS

Fazit

Der LEMP-Stack auf Ubuntu 26.04 LTS trennt statische Asset-Auslieferung über Nginx von der PHP-Ausführung im FastCGI Process Manager. Die Datenbank MariaDB 11.8 ist lokal gebunden und über Unix-Domain-Sockets abgesichert.

Durch die parallele Konfiguration von HTTP/2 über TCP und HTTP/3 über UDP Port 443 nutzen Clients das jeweils passende Transportprotokoll, ohne dass ältere Browser den Zugriff verlieren.

Im laufenden Betrieb sollten vor allem zwei Bereiche überwacht werden: das Log /var/log/nginx/example.com.error.log auf eventuelle FastCGI-Worker-Engpässe (server reached max_children) sowie der Speicherverbrauch von MariaDB bei wachsendem Datenbestand.

Teilen & Export

Als Markdown exportieren

Ähnliche Beiträge