Refactor deployment process and update documentation
EK-DOS-WEB bauen und ausrollen / backend-pruefen (push) Failing after 2s
EK-DOS-WEB bauen und ausrollen / bauen-und-ausrollen (push) Skipped

- Removed nginx configuration file as it is no longer needed.
- Updated README.md to reflect changes in deployment structure.
- Enhanced DEPLOYMENT.md with detailed directory structure and user permissions.
- Added release.sh and rollback.sh scripts for managing deployments.
- Improved N8N.md to clarify file access and document handling.
- Adjusted health check and cache flushing procedures in deployment scripts.
This commit is contained in:
Kyle Müller
2026-09-06 22:41:04 +02:00
parent 7d82e807fc
commit 940b2d4767
7 changed files with 589 additions and 259 deletions
+199 -64
View File
@@ -3,14 +3,42 @@
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
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.
@@ -29,41 +57,54 @@ Redis braucht nichts weiter -- EK-DOS legt seine Schlüssel unter dem Präfix
> angemeldet und jede Ansicht fragt wieder direkt bei n8n an. Die Anwendung
> läuft weiter, aber langsamer und mit erneuter Anmeldung.
## 3. Verzeichnisse
## 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 mkdir -p /var/www/ekdos/{releases,backend}
sudo useradd --system --home /var/www/ekdos --shell /usr/sbin/nologin ekdos
sudo chown -R ekdos:ekdos /var/www/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
```
## 4. Backend einrichten
`release.sh` lädt php-fpm neu. Dafür genau dieses eine Kommando ohne Passwort:
```bash
cd /var/www/ekdos/backend
sudo -u ekdos cp .env.example .env
sudo -u ekdos editor .env
sudo -u ekdos composer install --no-dev --optimize-autoloader
sudo -u ekdos php bin/ekdos migrate
sudo -u ekdos php bin/ekdos user:create # erster Administrator
sudo -u ekdos php bin/ekdos check
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
```
`.env` gehört dem Benutzer `ekdos` und sollte `chmod 600` sein. Sie wird beim
Ausrollen ausdrücklich nicht überschrieben.
## 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 |
Alle heutigen `ek-dos-web`-Webhooks laufen über den 5678-Dienst und damit über
`N8N_PUBLIC_BASE`. Kommt später ein zweiter n8n-Dienst auf eigenem Port dazu,
wird er mit `Endpoints::internal(<port>, '<pfad>')` angesprochen und geht über
die RFC1918-Adresse -- er ist nach aussen nicht veröffentlicht.
| 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
@@ -75,66 +116,160 @@ 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 -- die Ausrollstrecke
macht das selbst.
**erst nach einem `systemctl reload php8.5-fpm`** wirksam -- `release.sh` macht
das selbst.
## 6. nginx
```bash
sudo cp deploy/nginx/ekdos.conf /etc/nginx/sites-available/ekdos.conf
sudo ln -sf /etc/nginx/sites-available/ekdos.conf /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
```
Die nginx-Konfiguration wird auf dem Server gepflegt und gehört bewusst nicht
ins Repository. Was sie leisten muss:
Kurzfassung: `fastcgi_pass unix:/run/php/ekdos.sock` für `/api/`, alles andere
`try_files $uri $uri/ /index.html`. Kein Anwendungsserver, kein Reverse Proxy
auf einen Node-Prozess.
| 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` |
Läuft der Zugriff nur intern über HTTP, kann `SESSION_COOKIE_SECURE=false`
gesetzt und der 443-Block entfernt werden. Sobald TLS anliegt: wieder auf `true`.
Drei Punkte, die erfahrungsgemäss Ärger machen:
## 7. Ausrollen
- **`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.
Die Gitea-Strecke (`.gitea/workflows/build.yml`) macht das bei jedem Push auf
`main`. Nötig sind:
Läuft der Zugriff nur intern über HTTP, muss `SESSION_COOKIE_SECURE=false`
gesetzt sein -- ein Secure-Cookie wird über reines HTTP nicht gesendet.
- ein SSH-Schlüssel als Gitea-Secret `DEPLOY_SSH_KEY`
- ein Konto `deploy` auf dem Server, das nach `/var/www/ekdos` schreiben darf
- `sudo systemctl reload php8.5-fpm` ohne Passwort für dieses Konto
## 7. Erstes Ausrollen von Hand
```
deploy ALL=(root) NOPASSWD: /bin/systemctl reload php8.5-fpm
```
Jede Veröffentlichung landet unter `releases/<sha>`; sichtbar wird sie erst
durch den Symlink-Tausch von `current`. Die letzten fünf bleiben liegen, ein
Rückschritt ist damit ein `ln -sfn`.
### Von Hand
Die Strecke übernimmt das später, aber einmal von Hand zeigt, ob alles steht:
```bash
cd frontend && pnpm install --frozen-lockfile && pnpm build
rsync -az --delete frontend/dist/ server:/var/www/ekdos/releases/manuell/
ssh server 'cd /var/www/ekdos && ln -sfn releases/manuell current && sudo systemctl reload php8.5-fpm'
# 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.
### 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 backend/bin/ekdos check # Postgres, Redis, n8n
curl -sS https://<host>/api/health # ohne Anmeldung erreichbar
tail -f /var/log/php/ekdos-error.log # Ausnahmen aus dem Backend
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` |
| Anmeldung wirkt, danach sofort abgemeldet | Redis nicht erreichbar, oder `SESSION_COOKIE_SECURE=true` ohne TLS |
| Listen bleiben leer, Meldung „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; `systemctl reload php8.5-fpm` |
| 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
+38
View File
@@ -107,3 +107,41 @@ Interne Aufgaben kennen in n8n genau zwei Empfänger: `sascha` und `svenja`. Ein
drittes EK-DOS-Konto kann sich anmelden und alle Ansichten nutzen, aber noch
keine Aufgaben zugewiesen bekommen. Dafür müsste der n8n-Workflow *Interne
Aufgaben* einen freien Empfängerschlüssel annehmen.
## Wo die Dateien tatsächlich liegen
EK-DOS-WEB liest **keine einzige Datei**. Weder die Next.js-Fassung noch das
PHP-Backend enthalten einen Dateisystemzugriff; der alte Container hatte auch
keinen Mount. Es gibt auch keinen PDF-Renderer: nichts wird erzeugt, nur
weitergereicht.
Alle Dokumente kommen als HTTP-Antwort aus n8n:
| Route | n8n liefert | EK-DOS-WEB tut |
| --------------------------------- | ---------------------- | ---------------------------- |
| `/api/tickets/digitale-akte` | PDF-Bytes | durchreichen |
| `/api/tickets/servicebericht` | PDF-Bytes | durchreichen |
| `/api/customers/digitale-akte` | PDF-Bytes | durchreichen |
| `/api/customer-invoices/pdf` | PDF-Bytes | Pfad prüfen, durchreichen |
| `/api/invoices-create/digitale-akte` | base64 im JSON | dekodieren, durchreichen |
| `/api/hours` | JSON-Report | Felder abbilden |
Der Mount `/mnt/n8n-nas` gehört zum **n8n-Container**, nicht zu dieser
Anwendung. Dort wird die Datei gelesen; EK-DOS-WEB fragt nur über HTTP danach.
Für den Webserver heisst das: er braucht keinen NAS-Zugang, keine SMB-Zugangs-
daten und keinen Mount. Ein Umzug des Webservers auf eine andere Maschine
berührt die Dateiablage nicht.
Zwei Dinge, die häufig hierher vermutet werden, aber nicht hier liegen:
- **Stundennachweise** sind kein Dokument. `/api/hours` liefert einen JSON-Report,
den n8n aus mehreren Quellen zusammensetzt. Es wird keine Datei gelesen.
- **Tageszusammenfassung und Tagesübersicht** kommen in der Anwendung nicht vor.
„Tageszusammenfassung" ist die Beschriftung von Workflow 9/13 im Schulungs-
diagramm; „TAGESÜBERSICHT" ist die Überschrift über der Begrüssung auf der
Startseite. Keines von beidem lädt Daten.
**Zeitgesteuerte Abläufe (Druckaufträge, morgendliche Berichte) gibt es in
EK-DOS-WEB nicht.** Es existiert kein Zeitplaner, kein Cron-Eintrag und kein
Druckcode -- weder alt noch neu. Solche Abläufe sind Schedule-Trigger in n8n und
laufen unabhängig von dieser Anwendung weiter. Der Umbau auf PHP berührt sie nicht.