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

13 KiB

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

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

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.

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:

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

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

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:

# 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:

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:

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:

cd backend  && composer update      # erzeugt composer.lock
cd frontend && pnpm install         # erzeugt pnpm-lock.yaml

Beide einchecken.

Zurückschalten

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

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

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).