---
id: 2026-05-15-ubuntu-26-04-lemp-stack-installieren
slug: ubuntu-26-04-lemp-stack-installieren
title: "LEMP-Stack auf Ubuntu 26.04 LTS einrichten: Nginx, MariaDB, PHP 8.5 & Let’s Encrypt"
excerpt: "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."
date: "2026-05-15T09:00:00+02:00"
updated: "2026-05-15T09:00:00+02:00"
author:
  name: "Sebastian Palencsár"
  handle: "spalencsar"
category: "serverumgebungen"
tags: ["lempstack", "ubuntu2604", "nginx", "http3", "mariadb", "php85", "linuxadmin", "security"]
reading_time: 25
toc: true
---

<blockquote class="infobox infobox--info">
💡 **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](/de/serverumgebungen/ubuntu-24-04-lemp-stack-installieren){.badge-link-text} mit Nginx 1.24, MariaDB 10.11 und PHP 8.3.
</blockquote>

Unter [Ubuntu 26.04 LTS (Resolute Raccoon)](/de/tag/ubuntu2604){.badge-link-text} setzt sich der LEMP-Stack aus [Nginx](https://www.nginx.com/){.badge-link-text} 1.28.x, [MariaDB](https://mariadb.org/){.badge-link-text} 11.8 LTS und [PHP](https://www.php.net/){.badge-link-text} 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`.

<blockquote class="infobox infobox--warn">
⚠️ **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.
</blockquote>

## 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:

```bash
# 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:

```bash
# 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
```

<blockquote class="infobox infobox--practice">
❗ **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.
</blockquote>

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

Installiere Nginx aus den offiziellen Paketquellen von Ubuntu 26.04:

```bash
# 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:

```bash
# 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:

```markdown
┌─────────────────────────────────────────────────────────────┐
│                 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:

```bash
# 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:

```bash
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

```markdown
┌─────────────────────────────────────────────────────────────┐
│              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.

<blockquote class="infobox infobox--warn">
⚠️ **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.
</blockquote>

```ini
[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:

```bash
# 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:

```bash
# 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
```

```markdown
┌─────────────────────────────────────────────────────────────┐
│                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:

```bash
# 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
```

<blockquote class="infobox infobox--info">
💡 **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.
</blockquote>

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

```ini
[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:

```bash
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:

```markdown
┌─────────────────────────────────────────────────────────────┐
│            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`:

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

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

```nginx
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`:

```bash
# 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:

```markdown
┌─────────────────────────────────────────────────────────────┐
│         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

```bash
# Datenbank-Konsole öffnen
sudo mariadb -u root
```

**SQL-Statements in der MariaDB-Konsole:**

```sql
-- 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;
```

<blockquote class="infobox infobox--practice">
❗ **Sicherheitshinweis zum Passwort:** Das oben gezeigte Kennwort ist ein exemplarisches Platzhalterbeispiel. Verwende für produktive Datenbankbenutzer immer ein individuelles, zufallsgeneriertes Passwort.
</blockquote>

### PHP-Verifikationstestskript erstellen

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

```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:

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

<blockquote class="infobox infobox--warn">
⚠️ **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.
</blockquote>

## 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:

```markdown
┌─────────────────────────────────────────────────────────────┐
│             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.

```markdown
┌─────────────────────────────────────────────────────────────┐
│             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:

```bash
# 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:

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

<blockquote class="infobox infobox--warn">
⚠️ **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.
</blockquote>

Prüfe die Konfiguration und lade Nginx neu:

```bash
# 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:

```bash
# 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:

<span class="nb-accent">1. UDP-Socket-Bindung auf dem Server prüfen</span>

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

```bash
# 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.

<span class="nb-accent">2. Alt-Svc Header über TCP verifizieren</span>

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

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

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

<span class="nb-accent">3. Echte HTTP/3-Verbindung erzwingen</span>

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

```bash
# 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.

<blockquote class="infobox infobox--info">
💡 **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`**.
</blockquote>

## 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](https://ubuntu.com/server/docs){.badge-link-text} | Offizielle Systemdokumentation für Ubuntu 26.04 LTS (Resolute Raccoon) |
| **Nginx HTTP/3 Dokumentation** | [Nginx QUIC & HTTP/3](https://nginx.org/en/docs/quic.html){.badge-link-text} | Offizieller Konfigurationsleitfaden für native QUIC-Listener und Alt-Svc |
| **MariaDB Knowledge Base** | [MariaDB 11.8 Dokumentation](https://mariadb.com/kb/en/documentation/){.badge-link-text} | Referenzhandbuch für InnoDB-Optimierung, Systemvariablen und Replikation |
| **PHP 8.5 Dokumentation** | [PHP Manual](https://www.php.net/manual/de/){.badge-link-text} | Vollständige Dokumentation aller Sprachfeatures, FPM-Optionen und OPcache |
| **Certbot Dokumentation** | [Certbot EFF Guide](https://certbot.eff.org/){.badge-link-text} | Dokumentation für automatisierte Let’s-Encrypt-Zertifikate unter Nginx |
| **LEMP Stack (Ubuntu 24.04)** | [Ubuntu 24.04 LEMP Leitfaden](/de/serverumgebungen/ubuntu-24-04-lemp-stack-installieren){.badge-link-text} | 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.
