--- name: nginx-vhost-deployment description: "Nginx-Webserver mit Name-based Virtual Hosting auf Ubuntu-Server installieren, konfigurieren und deployen – inkl. Fallstricke, DNS-Checks und Test-Workflow" version: 1.0.0 author: Agent A (Deployer) platforms: [linux, ubuntu] --- # Nginx Virtual Host Deployment Installation und Konfiguration von nginx mit mehreren Virtual Hosts (name-based) auf einem Ubuntu-VPS. Deckt den gesamten Workflow ab: DNS-Prüfung → Installation → VHost-Konfiguration → Berechtigungen → Firewall → Test → SSL-Vorbereitung. ## Auslöser (Trigger) - „Webserver installieren" - „Webseite online stellen" - „Domain aufsetzen" - „nginx einrichten" - „Virtual Host konfigurieren" - „Mehrere Domains auf einem Server" --- ## Schritt 1: DNS-Check VOR der Installation **Immer zuerst prüfen**, ob die Domains auf den Server zeigen – sonst arbeitet man ins Leere. ```bash for domain in domain1.de domain2.de; do echo "=== DNS: $domain ===" dig +short A "$domain" dig +short AAAA "$domain" done ``` **Interpretation:** - Zeigt auf **richtige IP** → ✅ Weiter - Zeigt auf **Cloudflare-IPs** (104.x, 172.x) → ⚠️ Nutzer muss Cloudflare-Proxy deaktivieren (orange→grau) - Zeigt auf **fremde IP** → ❌ Nutzer muss A-Record ändern - **Kein Eintrag** → ❌ Nutzer muss A-Record anlegen **Server-IP herausfinden:** `curl -s ifconfig.me` oder aus Memory/Konfiguration. --- ## Schritt 2: nginx installieren ```bash sudo apt-get update -qq sudo apt-get install -y -qq nginx certbot python3-certbot-nginx sudo systemctl enable --now nginx ``` **NICHT vergessen:** `certbot` und `python3-certbot-nginx` gleich mitinstallieren – spart später Arbeit. --- ## Schritt 3: Verzeichnisstruktur pro Domain ```bash sudo mkdir -p /var/www/DOMAIN.de/html sudo mkdir -p /var/www/DOMAIN.de/logs sudo chown -R www-data:www-data /var/www/DOMAIN.de/ sudo chmod -R 755 /var/www/DOMAIN.de/ ``` ⚠️ **PITFALL – 403 Forbidden:** Wenn nginx 403 zurückgibt, sind die Dateien/Berechtigungen schuld: - **Ursache:** Dateien gehören `root:root`, aber nginx-Worker läuft als `www-data` - **Fix:** `sudo chown -R www-data:www-data /var/www/` + `sudo chmod -R 755 /var/www/` - **Test:** `ps aux | grep nginx` → Worker-User prüfen --- ## Schritt 4: VHost-Konfiguration erstellen ### ⚠️ PITFALL – sites-available vs sites-enabled: SEPARATE Dateien, nicht nur Symlink Auf manchen Setups sind `sites-available/DOMAIN` und `sites-enabled/DOMAIN` **zwei getrennte Dateien** (kein Symlink). nginx lädt NUR `sites-enabled/`. Wer nur die `available`-Datei editiert und reloadet, sieht **keine Änderung** — und `nginx -t` meldet trotzdem OK (er testet die alte, aktive Datei → "grün" aber unverändert). Symptom (2026-08-04 PersoEnt): neuer `location /pb/` Proxy erschien nie über HTTPS, `nginx -T | grep -c "location /pb/"` = 0, obwohl die available-Datei den Block hatte. **Diagnose (Symlink oder separate Datei?):** ```bash ls -la /etc/nginx/sites-enabled/DOMAIN # "-> sites-available/..." = Symlink, sonst separate Datei nginx -T 2>/dev/null | grep -c "location /pb/" # 0 obwohl in available vorhanden => separate Datei aktiv ``` **Fix:** Endversion nach BEIDEN `sites-available/...` UND `sites-enabled/...` schreiben (oder sauber als Symlink erzwingen: `ln -sf .../sites-available/DOMAIN .../sites-enabled/DOMAIN`), dann `nginx -t && systemctl reload nginx` und mit `nginx -T | grep` verifizieren, dass der neue Block wirklich in der aktiven Config steht. ### ⚠️ PITFALL – write_file verweigert Systempfade `write_file` kann **nicht** nach `/etc/nginx/sites-available/` schreiben (Systempfad-Schutz). **Workaround:** `sudo tee` im Terminal nutzen. ```bash sudo tee /etc/nginx/sites-available/DOMAIN.de << 'EOF' server { listen 80; listen [::]:80; server_name DOMAIN.de www.DOMAIN.de; root /var/www/DOMAIN.de/html; index index.html index.htm; access_log /var/www/DOMAIN.de/logs/access.log; error_log /var/www/DOMAIN.de/logs/error.log; location / { try_files $uri $uri/ =404; } } EOF # Symlink aktivieren sudo ln -sf /etc/nginx/sites-available/DOMAIN.de /etc/nginx/sites-enabled/ # Test + Reload sudo nginx -t && sudo systemctl reload nginx ``` **Template** für neue VHosts liegt in `templates/nginx-vhost.conf`. --- ## Schritt 5: Lokal testen (Host-Header-Trick) Auch wenn DNS noch nicht propagiert ist, kann man den VHost lokal testen: ```bash # HTTP-Status prüfen curl -s -o /dev/null -w "HTTP %{http_code}\n" -H "Host: DOMAIN.de" http://localhost/ # Titel auslesen curl -s -H "Host: DOMAIN.de" http://localhost/ | grep -oP '\K[^<]+' ``` **Erwartet:** HTTP 200 + korrekter Seitentitel. --- ## Schritt 6: Firewall (UFW) konfigurieren ```bash sudo ufw allow 80/tcp sudo ufw allow 443/tcp sudo ufw allow 22/tcp # SSH nicht vergessen! sudo ufw --force enable sudo ufw status verbose ``` ⚠️ **PITFALL:** `ufw enable` ohne vorher `allow 22/tcp` sperrt dich aus dem SSH aus! Immer zuerst SSH erlauben. --- ## Schritt 7: Dem Nutzer DNS-Anpassungen mitteilen Dem Nutzer eine klare Tabelle zeigen, welche DNS-Änderungen nötig sind: | Domain | Aktuelles DNS | Aktion | |--------|--------------|--------| | domain.de | 104.21.x.x (Cloudflare) | A → SERVER-IP, Proxy deaktivieren | | domain2.de | kein Eintrag | A-Record → SERVER-IP anlegen | **Server-IP:** Immer explizit nennen (z. B. `92.60.38.175`). --- ## Schritt 8: SSL (nach DNS-Propagation) Sobald DNS propagiert ist (5-60 Min): ```bash sudo certbot --nginx -d DOMAIN.de -d www.DOMAIN.de --non-interactive --agree-tos --email ADMIN-EMAIL ``` Oder alle auf einmal: ```bash sudo certbot --nginx -d domain1.de,www.domain1.de -d domain2.de,www.domain2.de ... ``` Certbot modifiziert automatisch die nginx-Konfiguration und fügt den `listen 443 ssl` Block hinzu. ### ⚠️ PITFALL – Certbot überschreibt SSL-Pfade in BESTEHENDEN vhosts `certbot --nginx` für eine NEUE Domain kann die `ssl_certificate`-Pfade in den Configs **bestehender vhosts überschreiben**. Certbot schreibt nicht nur den neuen vhost, sondern modifiziert ggf. auch andere Dateien — besonders wenn `server_name`-Wildcards oder `*`-Einträge involviert sind. **Symptom:** Nach `certbot --nginx -d neuedomain.de` funktioniert `https://alte-domain.de/` nicht mehr (SSL-Fehler, `ERR_CERT_COMMON_NAME_INVALID`). Die alte Config zeigt jetzt auf `/etc/letsencrypt/live/neuedomain.de/...` statt `.../alte-domain.de/...`. **Fix (nach JEDEM certbot-Lauf prüfen):** ```bash # Prüfen ob alle vhosts noch auf IHRE Zertifikate zeigen for f in /etc/nginx/sites-enabled/*; do cert=$(grep ssl_certificate "$f" | head -1 | grep -oP '/live/\K[^/]+') domain=$(grep server_name "$f" | head -1 | awk '{print $2}') echo "$domain → $cert" # Prüfen ob das Zertifikat den Domain-Namen als SAN hat openssl x509 -in /etc/letsencrypt/live/$cert/fullchain.pem -noout -text 2>/dev/null \ | grep -q "$domain" || echo " ⚠️ MISMATCH: $domain nicht in $cert!" done ``` **Reparatur:** `ssl_certificate` und `ssl_certificate_key` im betroffenen vhost manuell zurück auf das richtige Zertifikat setzen → `nginx -t && systemctl reload nginx`. **Prävention:** Nach jedem `certbot --nginx` für eine neue Domain: Zuerst `curl -sk https://alte-domain.de/` prüfen, bevor die neue URL getestet wird. Wenn alte Domains brechen, sofort reparieren (siehe Fix oben). ### ⚠️ PITFALL – Certbot-Henne-Ei bei NEUEM vhost (Zertifikat existiert noch nicht) Wenn du den SSL-vhost **vorab** manuell anlegst (Referenz auf `/etc/letsencrypt/live/DOMAIN/fullchain.pem`), aber das Zertifikat noch nicht existiert, schlägt **`nginx -t` fehl** (`[emerg] cannot load certificate ... No such file or directory`). Dadurch schlägt **auch `certbot --nginx` fehl** (es läuft intern `nginx -t` → MisconfigurationError) — ein klassisches Henne-Ei. **Fix – nicht einen SSL-vhost vorab anlegen, erst Zertifikat mit HTTP-only-vhost holen:** 1. Temporär einen **rein-HTTP-80-vhost** anlegen (nur `listen 80;`, kein SSL-Referenz): ```bash # in sites-available: nur listen 80, server_name, root sudo ln -sf .../sites-available/DOMAIN .../sites-enabled/ # oder in beide Dateien sudo nginx -t && sudo systemctl reload nginx # JETZT grün ``` 2. Zertifikat per **webroot** holen (port-80-challenge, kein nginx-Plugin nötig): ```bash sudo mkdir -p /var/www/DOMAIN.de/.well-known sudo certbot certonly --webroot -w /var/www/DOMAIN.de -d DOMAIN.de --agree-tos --register-unsafely-without-email ``` 3. Erst **danach** den vollständigen SSL-vhost (mit `listen 443 ssl` + cert-Pfaden) ablegen + `nginx -t && systemctl reload`. 4. Verifizieren: `curl -sk https://DOMAIN.de` → HTTP 200 oder zumindest der SSL-Handshake ok (`curl -sv ... | grep "SSL certificate verify ok"`). **Reihenfolge ist entscheidend:** Erst Port-80 + Zertifikat existiert, DANN SSL-vhost. Vorab-SSL-vhost = Henne-Ei-Blocker. ### ⚠️ PITFALL – write_file verweigert /etc/systemd oder andere geschützte Pfade Für systemd-Unit-Services (nicht nginx): `write_file` verweigert ebenso `/etc/systemd/system/`. Muster: In `/tmp` schreiben, dann `sudo cp /tmp/foo.service /etc/systemd/system/` + `systemctl daemon-reload`. --- ## Troubleshooting | Symptom | Ursache | Fix | |---------|---------|-----| | HTTP 403 | Falsche Datei-Rechte | `chown -R www-data:www-data /var/www/` | | HTTP 404 | index.html fehlt | Pfad in `root`-Direktive prüfen | | „server_names_hash_bucket_size" | Zu viele/zu lange Domainnamen | `sudo tee -a /etc/nginx/nginx.conf` → `server_names_hash_bucket_size 64;` im http-Block | | nginx startet nicht | Syntax-Fehler | `sudo nginx -t` zeigt die genaue Zeile | | write_file „sensitive system path" | Systempfad-Schutz | `sudo tee` im Terminal nutzen | | ufw inactive | Firewall nie aktiviert | `sudo ufw --force enable` |