# 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/ was nginx liest ├── previous Notiz für rollback.sh ├── releases/ │ └── / │ ├── 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).