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_FORauf$remote_addrsetzen, 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 gesetztenX-Forwarded-Forumgehen. Steht ein Proxy davor, dessen Netze überset_real_ip_fromeintragen.add_headerwird nicht vererbt, sobald einelocationeine eigene Kopfzeile setzt. Ein Block mit eigenemCache-Controlverliert damit alle Sicherheitskopfzeilen des Serverblocks./api/healthvon einer HTTPS-Umleitung ausnehmen.release.shprüft lokal über HTTP; eine 301 wäre fürcurl -fein Erfolg, ohne dass PHP je erreicht würde. AlternativEKDOS_HEALTH_URLauf 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).