💡 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 insites-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_instanceswurden 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.phpnach 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 Parameterreuseport(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 HeaderAlt-Svc: h3=":443"; ma=86400aktiviert 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-Securityerzwingt verschlüsselte Verbindungen. Erweiterungen wieincludeSubDomainsbinden ausnahmslos alle bestehenden und künftigen Subdomains an HTTPS. Der Zusatzpreloadbeantragt 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
h2an; sobald derAlt-Svc-Header registriert wurde, wechselt das Protokoll beim Neuladen aufh3.
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 | grep http_v3 |
Prüft, ob Nginx mit dem HTTP/3-QUIC-Modul gebaut wurde |
| UDP Socket-Prüfung | sudo ss -ulnp | 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.