Files
EKDOS/docs/DEPLOYMENT.md
T
Kyle Müller 7d53bd6856
EK-DOS-WEB bauen und ausrollen / backend-pruefen (push) Failing after 29s
EK-DOS-WEB bauen und ausrollen / bauen-und-ausrollen (push) Skipped
Add node and git installation step for proper checkout in build workflow
2026-09-06 22:45:15 +02:00

298 lines
13 KiB
Markdown

# EK-DOS-WEB betreiben
Zielsystem: Debian/Ubuntu mit nginx und php8.5-fpm. Postgres und Redis laufen
bereits für n8n und werden mitbenutzt (eigene Datenbank, eigene Redis-Nummer).
## Verzeichnisse auf dem Webserver
```
/var/www/ekdos/
├── current -> releases/<sha> was nginx liest
├── previous Notiz für rollback.sh
├── releases/
│ └── <sha>/
│ ├── public/ das Vite-Bündel -> nginx root
│ └── backend/ PHP + vendor/ -> fastcgi
│ ├── .env -> ../../../shared/.env
│ └── var/cache/ kompilierter DI-Container, je Veröffentlichung
├── shared/
│ └── .env überlebt jedes Ausrollen
└── deploy/
├── release.sh
└── rollback.sh
```
Oberfläche und Backend liegen **in derselben Veröffentlichung** und werden mit
einem einzigen Symlink-Tausch gemeinsam sichtbar. Eine neue Oberfläche kann
daher nie gegen ein altes Backend laufen.
## 1. Pakete
```bash
sudo apt update
sudo apt install -y nginx php8.5-fpm php8.5-cli php8.5-pgsql php8.5-redis \
php8.5-mbstring php8.5-curl php8.5-intl php8.5-xml \
composer rsync curl
```
`composer` wird auf dem Webserver gebraucht: die PHP-Abhängigkeiten werden dort
aufgelöst, nicht im Gitea-Runner. Nur dort stimmen PHP-Version und Erweiterungen,
sodass die Plattformprüfung von Composer etwas Echtes prüft.
`php8.5-intl` ist optional, verbessert aber die Erkennung der Kundenrücksprache
in Ticketberichten (Unicode-Normalisierung). Ohne die Erweiterung greift ein
Rückfall ohne Normalisierung.
## 2. Datenbank und Redis
```bash
sudo -u postgres createuser ekdos --pwprompt
sudo -u postgres createdb ekdos --owner=ekdos
```
Redis braucht nichts weiter -- EK-DOS legt seine Schlüssel unter dem Präfix
`ekdos:` ab und nutzt eine eigene Datenbanknummer (`REDIS_DB`).
> Redis hält Sitzungen **und** den n8n-Cache. Ist Redis weg, ist niemand mehr
> angemeldet und jede Ansicht fragt wieder direkt bei n8n an. Die Anwendung
> läuft weiter, aber langsamer und mit erneuter Anmeldung.
## 3. Benutzer und Rechte
Zwei Konten: `ekdos` führt PHP aus, `deploy` nimmt die Dateien entgegen. Über
die gemeinsame Gruppe kommt php-fpm an das, was `deploy` hochlädt.
```bash
sudo useradd --system --home /var/www/ekdos --shell /usr/sbin/nologin ekdos
sudo useradd --create-home --shell /bin/bash deploy
sudo usermod -aG ekdos deploy
sudo mkdir -p /var/www/ekdos/{releases,shared,deploy}
sudo chown -R deploy:ekdos /var/www/ekdos
sudo chmod -R g+rX /var/www/ekdos
# setgid: alles neu Angelegte erbt die Gruppe ekdos, sonst käme php-fpm
# nach dem nächsten Ausrollen nicht mehr an die Dateien.
sudo find /var/www/ekdos -type d -exec chmod g+s {} +
sudo mkdir -p /var/log/php && sudo chown ekdos:ekdos /var/log/php
```
`release.sh` lädt php-fpm neu. Dafür genau dieses eine Kommando ohne Passwort:
```bash
echo 'deploy ALL=(root) NOPASSWD: /bin/systemctl reload php8.5-fpm' \
| sudo tee /etc/sudoers.d/ekdos-deploy
sudo chmod 440 /etc/sudoers.d/ekdos-deploy
sudo visudo -c
```
## 4. Konfiguration anlegen
```bash
sudo -u deploy cp backend/.env.example /var/www/ekdos/shared/.env
sudo -u deploy editor /var/www/ekdos/shared/.env
sudo chown deploy:ekdos /var/www/ekdos/shared/.env
sudo chmod 640 /var/www/ekdos/shared/.env
```
`640` mit Gruppe `ekdos`: php-fpm liest sie, sonst niemand. Sie wird beim
Ausrollen ausdrücklich nie überschrieben.
### n8n-Adressen
| Einstellung | Wert | warum |
| ------------------- | -------------------------------- | ---------------------------------------------------------- |
| `N8N_PUBLIC_BASE` | `https://n8n.elektro-krueger.eu` | Der Dienst auf Port 5678 ist nur über den CNAME erreichbar |
| `N8N_INTERNAL_HOST` | `10.0.11.131` | Jeder weitere n8n-Dienst auf einem anderen Port |
## 5. php-fpm
```bash
sudo cp deploy/php-fpm/ekdos.pool.conf /etc/php/8.5/fpm/pool.d/ekdos.conf
sudo php-fpm8.5 -t
sudo systemctl restart php8.5-fpm
ls -l /run/php/ekdos.sock # muss www-data:www-data 0660 gehören
```
Der Pool setzt `opcache.validate_timestamps = 0`. Neuer Quelltext wird deshalb
**erst nach einem `systemctl reload php8.5-fpm`** wirksam -- `release.sh` macht
das selbst.
## 6. nginx
Die nginx-Konfiguration wird auf dem Server gepflegt und gehört bewusst nicht
ins Repository. Was sie leisten muss:
| Pfad | Ziel |
| ------------- | ------------------------------------------------------------------------ |
| `/api/` | `fastcgi_pass unix:/run/php/ekdos.sock`, `SCRIPT_FILENAME` auf `/var/www/ekdos/current/backend/public/index.php` |
| `/assets/` | `try_files $uri =404` -- kein SPA-Rückfall, sonst wird eine fehlende JS-Datei zu HTML mit Status 200 |
| alles andere | `root /var/www/ekdos/current/public`, `try_files $uri $uri/ /index.html` |
Drei Punkte, die erfahrungsgemäss Ärger machen:
- **`HTTP_X_FORWARDED_FOR` auf `$remote_addr` setzen**, nicht auf
`$proxy_add_x_forwarded_for`. Letzteres hängt die Adresse nur an einen vom
Client mitgeschickten Header an -- damit liesse sich die Anmeldebremse mit
einem selbst gesetzten `X-Forwarded-For` umgehen. Steht ein Proxy davor,
dessen Netze über `set_real_ip_from` eintragen.
- **`add_header` wird nicht vererbt**, sobald eine `location` eine eigene
Kopfzeile setzt. Ein Block mit eigenem `Cache-Control` verliert damit alle
Sicherheitskopfzeilen des Serverblocks.
- **`/api/health` von einer HTTPS-Umleitung ausnehmen.** `release.sh` prüft
lokal über HTTP; eine 301 wäre für `curl -f` ein Erfolg, ohne dass PHP je
erreicht würde. Alternativ `EKDOS_HEALTH_URL` auf die HTTPS-Adresse setzen.
Läuft der Zugriff nur intern über HTTP, muss `SESSION_COOKIE_SECURE=false`
gesetzt sein -- ein Secure-Cookie wird über reines HTTP nicht gesendet.
## 7. Erstes Ausrollen von Hand
Die Strecke übernimmt das später, aber einmal von Hand zeigt, ob alles steht:
```bash
# auf dem Arbeitsplatz
cd frontend && pnpm install && pnpm build && cd ..
SHA=erste
ssh deploy@10.0.11.131 "mkdir -p /var/www/ekdos/releases/$SHA"
rsync -az --delete frontend/dist/ deploy@10.0.11.131:/var/www/ekdos/releases/$SHA/public/
rsync -az --delete --exclude vendor/ --exclude .env --exclude var/ \
backend/ deploy@10.0.11.131:/var/www/ekdos/releases/$SHA/backend/
rsync -az --chmod=F755 deploy/release.sh deploy/rollback.sh \
deploy@10.0.11.131:/var/www/ekdos/deploy/
ssh deploy@10.0.11.131 /var/www/ekdos/deploy/release.sh $SHA
```
`release.sh` installiert die Composer-Pakete, schreibt das Schema fort, schaltet
den Symlink um, lädt php-fpm neu, verwirft den n8n-Cache und prüft
`/api/health`. Bricht etwas vorher ab, bleibt die laufende Fassung unberührt.
Danach den ersten Administrator anlegen:
```bash
ssh deploy@10.0.11.131 'php /var/www/ekdos/current/backend/bin/ekdos user:create'
ssh deploy@10.0.11.131 'php /var/www/ekdos/current/backend/bin/ekdos check'
```
## 8. Gitea-Strecke
`.gitea/workflows/build.yml` läuft bei jedem Push auf `main`:
| Auftrag | Container | tut |
| ---------------------- | ------------------ | ------------------------------------------------------------- |
| `backend-pruefen` | `php:8.5-cli` | `php -l` über alle Dateien, `composer validate` |
| `bauen-und-ausrollen` | `node:22-bookworm` | `pnpm typecheck` / `build`, dann rsync über SSH und `release.sh` |
Bei einem Pull Request wird nur geprüft und gebaut; ausgerollt wird
ausschliesslich aus `main`.
Bauen und Ausrollen liegen bewusst in **einem** Auftrag: das Bündel bleibt im
Arbeitsverzeichnis und geht von dort per rsync raus. `upload-artifact` /
`download-artifact` würden nur eine Fassungsabhängigkeit einführen (v3 oder v4,
je nach Gitea-Version) und damit einen verlässlichen Fehlschlag beim ersten Lauf.
**Es gibt keinen automatischen Rückschritt.** `release.sh` tauscht den Symlink
erst nach Composer und Migration. Scheitert es davor, läuft die alte Fassung
unverändert weiter -- ein automatischer Rückschritt würde dann auf die
*vorletzte* schalten und aus einem folgenlosen Fehlschlag einen Ausfall machen.
Der Lauf berichtet stattdessen, was gerade aktiv ist und ob `/api/health`
antwortet; zurückgeschaltet wird bei Bedarf von Hand.
### Warum der PHP-Auftrag erst node installiert
`actions/checkout` ist eine JavaScript-Aktion. act_runner führt solche Aktionen
mit dem `node` **aus dem Job-Container** aus -- anders als die gehosteten Runner
von GitHub, die ihr eigenes node mitbringen. `php:8.5-cli` enthält weder node
noch git, deshalb bricht der Checkout dort mit `exit code 127` ab, ohne eine
Zeile Ausgabe zu erzeugen.
Der erste Schritt des Auftrags installiert daher `git` und `nodejs` per apt. Er
ist ein reines Shell-Kommando und läuft deshalb auch im nackten Container. Der
Bau-Auftrag braucht das nicht: `node:22-bookworm` bringt beides mit.
Bookworm liefert node 18, `actions/checkout@v4` deklariert node 20. In der
Praxis läuft das Bündel der Aktion auf node 18 -- sollte es doch einmal klemmen,
node 20 aus dem NodeSource-Paketstand installieren.
### Schlüssel einrichten
Auf dem Arbeitsplatz:
```bash
ssh-keygen -t ed25519 -f gitea-deploy -C 'gitea-runner -> ekdos' -N ''
ssh-copy-id -i gitea-deploy.pub deploy@10.0.11.131
ssh-keyscan -t ed25519 10.0.11.131
```
In Gitea unter *Settings -> Secrets* zwei Einträge:
| Secret | Inhalt |
| -------------------- | ----------------------------------------------------------- |
| `DEPLOY_SSH_KEY` | der Inhalt von `gitea-deploy` (der **private** Schlüssel) |
| `DEPLOY_KNOWN_HOSTS` | die Ausgabe von `ssh-keyscan -t ed25519 10.0.11.131` |
`DEPLOY_KNOWN_HOSTS` ist optional -- fehlt es, übernimmt der Lauf den
Fingerabdruck ungeprüft und schreibt einen Hinweis ins Protokoll. Für den
Dauerbetrieb setzen.
Den privaten Schlüssel danach vom Arbeitsplatz löschen; er liegt in Gitea.
### Lockfiles
Beide Aufträge bestehen auf Sperrdateien, sonst löst jeder Lauf neu auf und
bekommt womöglich andere Fassungen als geprüft:
```bash
cd backend && composer update # erzeugt composer.lock
cd frontend && pnpm install # erzeugt pnpm-lock.yaml
```
Beide einchecken.
## Zurückschalten
```bash
ssh deploy@10.0.11.131 /var/www/ekdos/deploy/rollback.sh --list # was liegt da
ssh deploy@10.0.11.131 /var/www/ekdos/deploy/rollback.sh # eine zurück
ssh deploy@10.0.11.131 /var/www/ekdos/deploy/rollback.sh 2f9c1ab # auf eine bestimmte
```
Es wird nur der Symlink getauscht und php-fpm neu geladen -- Sekunden. Die
letzten fünf Veröffentlichungen bleiben liegen.
> **Migrationen werden nicht zurückgenommen.** Hat eine Fassung das Schema
> geändert, muss die alte damit umgehen können. Bei den bisherigen Migrationen
> (nur Tabellen anlegen) ist das der Fall.
## Prüfen, wenn etwas klemmt
```bash
php /var/www/ekdos/current/backend/bin/ekdos check # Postgres, Redis, n8n
curl -fsS http://127.0.0.1/api/health # ohne Anmeldung erreichbar
tail -f /var/log/php/ekdos-error.log # Ausnahmen aus dem Backend
tail -f /var/log/nginx/ekdos.error.log
```
| Symptom | meistens |
| ----------------------------------------------- | ------------------------------------------------------------------ |
| 502 auf `/api/` | Socket fehlt oder falsche Rechte; `systemctl status php8.5-fpm` |
| 403 auf `/api/`, im Fehlerlog „Permission denied" | setgid fehlt, php-fpm kommt nicht an die neue Veröffentlichung |
| Anmeldung wirkt, danach sofort abgemeldet | Redis nicht erreichbar, oder `SESSION_COOKIE_SECURE=true` ohne TLS |
| Listen leer, „n8n nicht erreichbar" | `N8N_PUBLIC_BASE` falsch, oder der Webhook ist in n8n nicht aktiv |
| Angezeigte Daten sind veraltet | Rückfall-Cache greift, weil n8n gerade nicht antwortet |
| 503 „noch nicht eingerichtet" | `N8N_INVOICE_SYNC_SECRET` bzw. `N8N_CUSTOMER_KEY` fehlt in `.env` |
| Quelltextänderung wirkt nicht | Opcache; `sudo systemctl reload php8.5-fpm` |
| Ausrollen bricht bei „composer install" ab | Erweiterung fehlt; `composer check-platform-reqs` auf dem Server |
## Alle Benutzer aussperren
```bash
redis-cli --scan --pattern 'ekdos:sess:*' | xargs -r redis-cli del
```
Danach muss sich jeder neu anmelden. Einzelne Konten trifft es gezielt über die
Benutzerverwaltung (Deaktivieren beendet die offenen Sitzungen sofort).