Initial: Mesh-Configs vom 03.09.2026 01:23
This commit is contained in:
@@ -0,0 +1,255 @@
|
||||
---
|
||||
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 '<title>\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.
|
||||
|
||||
**Zusätzlich:** Nach JEDEM certbot-Lauf für eine NEUE Domain müssen ALLE bestehenden Domains auf SSL-Fehler geprüft werden (certbot kann SSL-Pfade in bestehenden vhosts überschreiben). Siehe Pitfall unten.
|
||||
|
||||
### ⚠️ 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** — besonders wenn `server_name`-Wildcards (`*`) oder
|
||||
Shared-SSL-Zertifikate im Spiel sind. Dieses Problem trat auf dem Mehlzoerwer-Mesh auf:
|
||||
Nach `certbot --nginx -d git.mehlzoerwer.de` zeigte `mehlzoerwer.de` plötzlich auf das
|
||||
`git.mehlzoerwer.de`-Zertifikat (ohne `mehlzoerwer.de` als SAN → `ERR_CERT_COMMON_NAME_INVALID`).
|
||||
|
||||
**Prävention — Pflicht nach jedem certbot-Lauf:**
|
||||
```bash
|
||||
# 1. ALLE bestehenden Domains auf SSL prüfen
|
||||
for d in $(nginx -T 2>/dev/null | grep 'server_name ' | awk '{print $2}' | tr -d ';' | sort -u); do
|
||||
code=$(curl -sk -o /dev/null -w "%{http_code}" "https://$d/" --max-time 5)
|
||||
echo "$d → HTTP $code"
|
||||
done
|
||||
|
||||
# 2. Wenn eine alte Domain 000 oder SSL-Fehler liefert → vhost-Checker
|
||||
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}' | tr -d ';')
|
||||
echo "$domain → Zertifikat: $cert"
|
||||
done
|
||||
```
|
||||
|
||||
**Reparatur:** `ssl_certificate` und `ssl_certificate_key` im betroffenen vhost zurück
|
||||
auf das richtige Zertifikat setzen (z.B. `/etc/letsencrypt/live/mehlzoerwer.de/...`)
|
||||
→ `nginx -t && systemctl reload nginx` → erneuter SSL-Test.
|
||||
|
||||
### ⚠️ 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` |
|
||||
@@ -0,0 +1,21 @@
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
server_name {{DOMAIN}} www.{{DOMAIN}};
|
||||
|
||||
root /var/www/{{DOMAIN}}/html;
|
||||
index index.html index.htm;
|
||||
|
||||
access_log /var/www/{{DOMAIN}}/logs/access.log;
|
||||
error_log /var/www/{{DOMAIN}}/logs/error.log;
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ =404;
|
||||
}
|
||||
|
||||
# location /api/ {
|
||||
# proxy_pass http://localhost:3000;
|
||||
# proxy_set_header Host $host;
|
||||
# proxy_set_header X-Real-IP $remote_addr;
|
||||
# }
|
||||
}
|
||||
Reference in New Issue
Block a user