Refactor deployment process and update documentation
- 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:
+165
-97
@@ -3,24 +3,69 @@ name: EK-DOS-WEB bauen und ausrollen
|
|||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [main]
|
branches: [main]
|
||||||
|
pull_request:
|
||||||
|
branches: [main]
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
|
# Zwei gleichzeitige Ausrollvorgänge kämen sich beim Symlink-Tausch in die Quere.
|
||||||
|
# Ein neuer Lauf wartet, statt den laufenden abzubrechen.
|
||||||
|
concurrency:
|
||||||
|
group: ekdos-deploy
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
env:
|
env:
|
||||||
DEPLOY_HOST: 10.0.11.131
|
DEPLOY_HOST: 10.0.11.131
|
||||||
DEPLOY_USER: deploy
|
DEPLOY_USER: deploy
|
||||||
DEPLOY_ROOT: /var/www/ekdos
|
DEPLOY_PATH: /var/www/ekdos
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
# ──────────────────────────────────────────────────────────────────────────
|
# ──────────────────────────────────────────────────────────────────────────
|
||||||
# Die Oberfläche wird hier zu statischen Dateien kompiliert. Node existiert
|
# Backend: nichts zu kompilieren, aber alles zu prüfen. Composer läuft hier
|
||||||
# ausschliesslich in diesem Container; auf dem Zielserver läuft keines.
|
# NICHT -- die Abhängigkeiten werden auf dem Webserver aufgelöst, wo PHP-
|
||||||
|
# Version und Erweiterungen tatsächlich stimmen. Eine Plattformprüfung im
|
||||||
|
# Runner würde nur sich selbst prüfen.
|
||||||
# ──────────────────────────────────────────────────────────────────────────
|
# ──────────────────────────────────────────────────────────────────────────
|
||||||
frontend:
|
backend-pruefen:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
container: node:22-bookworm-slim
|
container: php:8.5-cli
|
||||||
defaults:
|
steps:
|
||||||
run:
|
- uses: actions/checkout@v4
|
||||||
working-directory: frontend
|
|
||||||
|
- name: Syntax aller PHP-Dateien prüfen
|
||||||
|
working-directory: backend
|
||||||
|
run: |
|
||||||
|
set -eu
|
||||||
|
find src public bin -type f \( -name '*.php' -o -name 'ekdos' \) -print0 \
|
||||||
|
| xargs -0 -n1 -P4 php -l
|
||||||
|
|
||||||
|
- name: composer.json und composer.lock prüfen
|
||||||
|
working-directory: backend
|
||||||
|
run: |
|
||||||
|
set -eu
|
||||||
|
curl -sS https://getcomposer.org/installer \
|
||||||
|
| php -- --install-dir=/usr/local/bin --filename=composer
|
||||||
|
composer validate --no-check-publish
|
||||||
|
# Ohne Sperrdatei löst der Webserver bei jedem Ausrollen neu auf und
|
||||||
|
# bekommt womöglich andere Fassungen als hier geprüft.
|
||||||
|
test -f composer.lock || {
|
||||||
|
echo "composer.lock fehlt. Einmal 'composer update' laufen lassen und einchecken."
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# ──────────────────────────────────────────────────────────────────────────
|
||||||
|
# Bauen und ausrollen in einem Auftrag.
|
||||||
|
#
|
||||||
|
# Bewusst ohne upload-/download-artifact: welche Fassung dieser Aktionen eine
|
||||||
|
# Gitea-Instanz beherrscht (v3 oder v4), hängt an deren Version und ist eine
|
||||||
|
# verlässliche Quelle für Fehlschläge beim ersten Lauf. Das Bündel bleibt
|
||||||
|
# einfach im Arbeitsverzeichnis liegen und geht von dort per rsync raus.
|
||||||
|
#
|
||||||
|
# Node existiert nur in diesem Container -- auf dem Webserver läuft keines.
|
||||||
|
# ──────────────────────────────────────────────────────────────────────────
|
||||||
|
bauen-und-ausrollen:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
container: node:22-bookworm
|
||||||
|
needs: [backend-pruefen]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
@@ -28,121 +73,144 @@ jobs:
|
|||||||
run: corepack enable && corepack prepare pnpm@10.16.1 --activate
|
run: corepack enable && corepack prepare pnpm@10.16.1 --activate
|
||||||
|
|
||||||
- name: Abhängigkeiten installieren
|
- name: Abhängigkeiten installieren
|
||||||
|
working-directory: frontend
|
||||||
run: pnpm install --frozen-lockfile
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
# Typprüfung getrennt, damit ein Typfehler nicht als Bündelfehler erscheint.
|
# Getrennt von `build`, damit ein Typfehler als solcher im Protokoll steht
|
||||||
|
# und nicht als Bündelfehler.
|
||||||
- name: Typen prüfen
|
- name: Typen prüfen
|
||||||
|
working-directory: frontend
|
||||||
run: pnpm typecheck
|
run: pnpm typecheck
|
||||||
|
|
||||||
- name: Bündel bauen
|
- name: Bündel bauen
|
||||||
|
working-directory: frontend
|
||||||
run: pnpm build
|
run: pnpm build
|
||||||
|
|
||||||
- uses: actions/upload-artifact@v3
|
- name: Bündel prüfen
|
||||||
with:
|
working-directory: frontend
|
||||||
name: frontend-dist
|
|
||||||
path: frontend/dist
|
|
||||||
retention-days: 7
|
|
||||||
|
|
||||||
# ──────────────────────────────────────────────────────────────────────────
|
|
||||||
# Das Backend wird nicht kompiliert, aber geprüft: Syntax aller Dateien und
|
|
||||||
# ein Abhängigkeitsbaum ohne Entwicklungspakete.
|
|
||||||
# ──────────────────────────────────────────────────────────────────────────
|
|
||||||
backend:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
container: php:8.5-cli
|
|
||||||
defaults:
|
|
||||||
run:
|
|
||||||
working-directory: backend
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Composer und Erweiterungen bereitstellen
|
|
||||||
run: |
|
run: |
|
||||||
apt-get update && apt-get install -y --no-install-recommends git unzip libpq-dev
|
set -eu
|
||||||
docker-php-ext-install pdo_pgsql
|
test -f dist/index.html || { echo "dist/index.html fehlt -- der Bau hat nichts erzeugt."; exit 1; }
|
||||||
curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
|
du -sh dist
|
||||||
|
|
||||||
- name: Abhängigkeiten installieren
|
# ── Ab hier nur noch aus main ──────────────────────────────────────────
|
||||||
run: composer install --no-dev --no-interaction --prefer-dist --optimize-autoloader --ignore-platform-req=ext-redis
|
|
||||||
|
|
||||||
- name: Syntax prüfen
|
- name: Werkzeuge zum Ausrollen installieren
|
||||||
run: find src public bin -type f \( -name '*.php' -o -name 'ekdos' \) -print0 | xargs -0 -n1 php -l
|
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||||
|
run: |
|
||||||
- uses: actions/upload-artifact@v3
|
set -eu
|
||||||
with:
|
apt-get update
|
||||||
name: backend-build
|
apt-get install -y --no-install-recommends rsync openssh-client
|
||||||
path: |
|
|
||||||
backend
|
|
||||||
!backend/.env
|
|
||||||
retention-days: 7
|
|
||||||
|
|
||||||
# ──────────────────────────────────────────────────────────────────────────
|
|
||||||
# Ausrollen. Jede Veröffentlichung landet in einem eigenen Verzeichnis; erst
|
|
||||||
# der Symlink-Tausch macht sie sichtbar. Ein Fehlschlag lässt die laufende
|
|
||||||
# Fassung unberührt.
|
|
||||||
# ──────────────────────────────────────────────────────────────────────────
|
|
||||||
deploy:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
needs: [frontend, backend]
|
|
||||||
if: gitea.ref == 'refs/heads/main'
|
|
||||||
steps:
|
|
||||||
- uses: actions/download-artifact@v3
|
|
||||||
with:
|
|
||||||
name: frontend-dist
|
|
||||||
path: dist
|
|
||||||
|
|
||||||
- uses: actions/download-artifact@v3
|
|
||||||
with:
|
|
||||||
name: backend-build
|
|
||||||
path: backend
|
|
||||||
|
|
||||||
- name: SSH einrichten
|
- name: SSH einrichten
|
||||||
|
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||||
|
env:
|
||||||
|
DEPLOY_SSH_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
|
||||||
|
DEPLOY_KNOWN_HOSTS: ${{ secrets.DEPLOY_KNOWN_HOSTS }}
|
||||||
run: |
|
run: |
|
||||||
|
set -eu
|
||||||
|
test -n "${DEPLOY_SSH_KEY:-}" || { echo "Secret DEPLOY_SSH_KEY fehlt."; exit 1; }
|
||||||
|
|
||||||
install -m 700 -d ~/.ssh
|
install -m 700 -d ~/.ssh
|
||||||
echo "${{ secrets.DEPLOY_SSH_KEY }}" > ~/.ssh/id_ed25519
|
printf '%s\n' "$DEPLOY_SSH_KEY" > ~/.ssh/id_ed25519
|
||||||
chmod 600 ~/.ssh/id_ed25519
|
chmod 600 ~/.ssh/id_ed25519
|
||||||
ssh-keyscan -H "$DEPLOY_HOST" >> ~/.ssh/known_hosts
|
|
||||||
|
# Bevorzugt der hinterlegte Fingerabdruck. Fehlt er, wird er erfragt --
|
||||||
|
# bequem, aber ohne Schutz vor einem untergeschobenen Ziel. Für den
|
||||||
|
# Dauerbetrieb setzen: ssh-keyscan -t ed25519 10.0.11.131
|
||||||
|
if [ -n "${DEPLOY_KNOWN_HOSTS:-}" ]; then
|
||||||
|
printf '%s\n' "$DEPLOY_KNOWN_HOSTS" > ~/.ssh/known_hosts
|
||||||
|
else
|
||||||
|
echo "Hinweis: DEPLOY_KNOWN_HOSTS ist nicht gesetzt, Fingerabdruck wird ungeprüft übernommen."
|
||||||
|
ssh-keyscan -H "$DEPLOY_HOST" >> ~/.ssh/known_hosts 2>/dev/null
|
||||||
|
fi
|
||||||
|
chmod 600 ~/.ssh/known_hosts
|
||||||
|
|
||||||
|
# Lieber hier scheitern als nach dem halben Hochladen.
|
||||||
|
- name: Ziel prüfen
|
||||||
|
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||||
|
run: |
|
||||||
|
set -eu
|
||||||
|
ssh "$DEPLOY_USER@$DEPLOY_HOST" "
|
||||||
|
set -eu
|
||||||
|
test -d '$DEPLOY_PATH/releases' || { echo '$DEPLOY_PATH/releases fehlt -- siehe docs/DEPLOYMENT.md'; exit 1; }
|
||||||
|
test -f '$DEPLOY_PATH/shared/.env' || { echo '$DEPLOY_PATH/shared/.env fehlt -- einmalig anlegen'; exit 1; }
|
||||||
|
command -v composer >/dev/null || { echo 'composer fehlt auf dem Webserver'; exit 1; }
|
||||||
|
command -v php >/dev/null || { echo 'php fehlt auf dem Webserver'; exit 1; }
|
||||||
|
# Prüft die sudo-Regel, ohne etwas auszuführen.
|
||||||
|
sudo -n -l /bin/systemctl reload php8.5-fpm >/dev/null 2>&1 \
|
||||||
|
|| echo 'Hinweis: sudo-Regel für den fpm-Neustart fehlt (siehe docs/DEPLOYMENT.md) -- release.sh wird daran scheitern.'
|
||||||
|
mkdir -p '$DEPLOY_PATH/releases/${{ github.sha }}' '$DEPLOY_PATH/deploy'
|
||||||
|
"
|
||||||
|
|
||||||
- name: Oberfläche übertragen
|
- name: Oberfläche übertragen
|
||||||
|
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||||
run: |
|
run: |
|
||||||
RELEASE="$DEPLOY_ROOT/releases/${{ gitea.sha }}"
|
set -eu
|
||||||
ssh "$DEPLOY_USER@$DEPLOY_HOST" "mkdir -p '$RELEASE'"
|
rsync -az --delete \
|
||||||
rsync -az --delete dist/ "$DEPLOY_USER@$DEPLOY_HOST:$RELEASE/"
|
frontend/dist/ \
|
||||||
|
"$DEPLOY_USER@$DEPLOY_HOST:$DEPLOY_PATH/releases/${{ github.sha }}/public/"
|
||||||
|
|
||||||
- name: Backend übertragen
|
- name: Backend übertragen
|
||||||
|
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||||
run: |
|
run: |
|
||||||
# .env bleibt auf dem Server und wird nie überschrieben.
|
set -eu
|
||||||
|
# vendor/ entsteht auf dem Server; .env liegt in shared/ und wird dort
|
||||||
|
# verknüpft; var/ ist der Zwischenspeicher dieser Veröffentlichung.
|
||||||
rsync -az --delete \
|
rsync -az --delete \
|
||||||
|
--exclude 'vendor/' \
|
||||||
--exclude '.env' \
|
--exclude '.env' \
|
||||||
--exclude 'var/cache' \
|
--exclude 'var/' \
|
||||||
backend/ "$DEPLOY_USER@$DEPLOY_HOST:$DEPLOY_ROOT/backend/"
|
backend/ \
|
||||||
|
"$DEPLOY_USER@$DEPLOY_HOST:$DEPLOY_PATH/releases/${{ github.sha }}/backend/"
|
||||||
|
|
||||||
- name: Umschalten und neu laden
|
- name: Ausrollskripte übertragen
|
||||||
|
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||||
run: |
|
run: |
|
||||||
ssh "$DEPLOY_USER@$DEPLOY_HOST" bash -euo pipefail <<'REMOTE'
|
set -eu
|
||||||
cd /var/www/ekdos
|
rsync -az --chmod=F755 \
|
||||||
|
deploy/release.sh deploy/rollback.sh \
|
||||||
|
"$DEPLOY_USER@$DEPLOY_HOST:$DEPLOY_PATH/deploy/"
|
||||||
|
|
||||||
# Schema fortschreiben, bevor die neue Oberfläche sichtbar wird.
|
# Hier passiert das Eigentliche: composer install, Migration, atomarer
|
||||||
php backend/bin/ekdos migrate
|
# Symlink-Tausch, php-fpm neu laden, n8n-Cache verwerfen,
|
||||||
|
# Gesundheitsprüfung. Das Skript liegt unter deploy/release.sh und lässt
|
||||||
# Atomarer Tausch: ln -sfn auf ein temporäres Ziel, dann umbenennen.
|
# sich genauso von Hand aufrufen.
|
||||||
ln -sfn "releases/${GITEA_SHA:-$(ls -1t releases | head -1)}" current.new
|
- name: Veröffentlichung aktivieren
|
||||||
mv -Tf current.new current
|
id: aktivieren
|
||||||
|
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||||
# Opcache hält den alten Quelltext, bis fpm neu lädt.
|
|
||||||
sudo systemctl reload php8.5-fpm
|
|
||||||
|
|
||||||
# Zwischengespeicherte n8n-Antworten passen womöglich nicht mehr
|
|
||||||
# zum neuen Abbildungscode.
|
|
||||||
php backend/bin/ekdos cache:flush
|
|
||||||
|
|
||||||
# Die letzten fünf Veröffentlichungen behalten.
|
|
||||||
ls -1t releases | tail -n +6 | xargs -r -I{} rm -rf "releases/{}"
|
|
||||||
REMOTE
|
|
||||||
env:
|
|
||||||
GITEA_SHA: ${{ gitea.sha }}
|
|
||||||
|
|
||||||
- name: Gesundheitsprüfung
|
|
||||||
run: |
|
run: |
|
||||||
|
set -eu
|
||||||
ssh "$DEPLOY_USER@$DEPLOY_HOST" \
|
ssh "$DEPLOY_USER@$DEPLOY_HOST" \
|
||||||
'curl -fsS -o /dev/null -w "%{http_code}\n" http://127.0.0.1/api/health'
|
"$DEPLOY_PATH/deploy/release.sh '${{ github.sha }}'"
|
||||||
|
|
||||||
|
# KEIN automatischer Rückschritt.
|
||||||
|
#
|
||||||
|
# release.sh tauscht den Symlink erst nach composer und Migration. Scheitert
|
||||||
|
# es davor, läuft die alte Fassung unverändert weiter -- ein Rückschritt
|
||||||
|
# würde dann auf die *vorletzte* schalten und aus einem folgenlosen
|
||||||
|
# Fehlschlag einen Ausfall machen. Deshalb hier nur berichten, was live ist.
|
||||||
|
- name: Bei Fehlschlag Lage berichten
|
||||||
|
if: failure() && steps.aktivieren.outcome == 'failure'
|
||||||
|
run: |
|
||||||
|
echo "Das Aktivieren ist fehlgeschlagen. Aktueller Stand auf dem Server:"
|
||||||
|
ssh "$DEPLOY_USER@$DEPLOY_HOST" "
|
||||||
|
echo -n ' aktiv: '; readlink '$DEPLOY_PATH/current' || echo '(keiner)'
|
||||||
|
echo -n ' vorher: '; cat '$DEPLOY_PATH/previous' 2>/dev/null || echo '(nichts vermerkt)'
|
||||||
|
curl -fsS --max-time 5 http://127.0.0.1/api/health && echo || echo ' /api/health antwortet NICHT'
|
||||||
|
" || true
|
||||||
|
echo
|
||||||
|
echo "Läuft die alte Fassung noch, ist nichts weiter zu tun -- Ursache beheben und erneut pushen."
|
||||||
|
echo "Sonst von Hand zurück: ssh $DEPLOY_USER@$DEPLOY_HOST $DEPLOY_PATH/deploy/rollback.sh"
|
||||||
|
exit 1
|
||||||
|
|
||||||
|
# /api/health ist im Port-80-Block von der HTTPS-Umleitung ausgenommen,
|
||||||
|
# sonst wäre die 301 für `curl -f` ein Erfolg und PHP nie erreicht.
|
||||||
|
- name: Ergebnis
|
||||||
|
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||||
|
run: |
|
||||||
|
set -eu
|
||||||
|
echo "Ausgerollt: ${{ github.sha }}"
|
||||||
|
ssh "$DEPLOY_USER@$DEPLOY_HOST" \
|
||||||
|
"curl -fsS --max-time 5 -H 'Accept: application/json' http://127.0.0.1/api/health"
|
||||||
|
echo
|
||||||
|
|||||||
@@ -25,8 +25,8 @@ statischen Dateien kompiliert; ausgeliefert wird sie von nginx.
|
|||||||
| ------------------- | ----------------------------------------------------------------- |
|
| ------------------- | ----------------------------------------------------------------- |
|
||||||
| `backend/` | PHP 8.5, Slim 4. BFF-Sitzung, Benutzerverwaltung, n8n-Weiterleitung |
|
| `backend/` | PHP 8.5, Slim 4. BFF-Sitzung, Benutzerverwaltung, n8n-Weiterleitung |
|
||||||
| `frontend/` | React 19 + Vite. Baut nach `frontend/dist/` |
|
| `frontend/` | React 19 + Vite. Baut nach `frontend/dist/` |
|
||||||
| `deploy/nginx/` | nginx-Site |
|
|
||||||
| `deploy/php-fpm/` | php-fpm-Pool |
|
| `deploy/php-fpm/` | php-fpm-Pool |
|
||||||
|
| `deploy/*.sh` | Veröffentlichung aktivieren / zurückschalten |
|
||||||
| `.gitea/workflows/` | Bau- und Ausrollstrecke |
|
| `.gitea/workflows/` | Bau- und Ausrollstrecke |
|
||||||
| `docs/` | Betrieb und n8n-Schnittstelle |
|
| `docs/` | Betrieb und n8n-Schnittstelle |
|
||||||
| `legacy-nextjs/` | Die abgelöste Next.js-Fassung. Kann nach dem Umstieg weg |
|
| `legacy-nextjs/` | Die abgelöste Next.js-Fassung. Kann nach dem Umstieg weg |
|
||||||
|
|||||||
@@ -1,97 +0,0 @@
|
|||||||
# EK-DOS-WEB
|
|
||||||
#
|
|
||||||
# nginx macht beides: es liefert das statisch gebaute React-Bündel aus und
|
|
||||||
# reicht ausschliesslich /api/ an php-fpm weiter. Auf dem Server läuft kein
|
|
||||||
# Node und kein Anwendungsserver -- nur nginx und php8.5-fpm.
|
|
||||||
#
|
|
||||||
# Ablage: /etc/nginx/sites-available/ekdos.conf -> sites-enabled/
|
|
||||||
# Prüfen: nginx -t && systemctl reload nginx
|
|
||||||
|
|
||||||
upstream ekdos_php {
|
|
||||||
# Unix-Socket statt TCP: kein Netzwerkstack, keine offene Portnummer.
|
|
||||||
server unix:/run/php/ekdos.sock;
|
|
||||||
}
|
|
||||||
|
|
||||||
server {
|
|
||||||
listen 80;
|
|
||||||
listen [::]:80;
|
|
||||||
server_name schulung.elektro-krueger.local;
|
|
||||||
|
|
||||||
# Hinter einem TLS-Terminator (Cloudflare, vorgelagertes nginx) diesen Block
|
|
||||||
# entfernen und stattdessen den 443-Block unten verwenden.
|
|
||||||
return 301 https://$host$request_uri;
|
|
||||||
}
|
|
||||||
|
|
||||||
server {
|
|
||||||
listen 443 ssl;
|
|
||||||
listen [::]:443 ssl;
|
|
||||||
http2 on;
|
|
||||||
server_name schulung.elektro-krueger.local;
|
|
||||||
|
|
||||||
ssl_certificate /etc/ssl/ekdos/fullchain.pem;
|
|
||||||
ssl_certificate_key /etc/ssl/ekdos/privkey.pem;
|
|
||||||
ssl_protocols TLSv1.2 TLSv1.3;
|
|
||||||
|
|
||||||
# Das gebaute Bündel aus dem Gitea-Lauf.
|
|
||||||
root /var/www/ekdos/current;
|
|
||||||
index index.html;
|
|
||||||
|
|
||||||
# Die Anwendung ist intern; sie gehört in keinen Suchindex.
|
|
||||||
add_header X-Robots-Tag "noindex, nofollow" always;
|
|
||||||
add_header X-Content-Type-Options "nosniff" always;
|
|
||||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
|
||||||
add_header Referrer-Policy "same-origin" always;
|
|
||||||
|
|
||||||
# Rechnungen und Serviceberichte sind mehrere Megabyte gross.
|
|
||||||
client_max_body_size 32m;
|
|
||||||
|
|
||||||
# ── Statische Oberfläche ──────────────────────────────────────────────
|
|
||||||
# Vite vergibt Inhaltshashes, deshalb dürfen die Bündel dauerhaft im Cache
|
|
||||||
# liegen. Nur index.html darf das nicht, sonst sieht niemand ein Update.
|
|
||||||
location /assets/ {
|
|
||||||
expires 1y;
|
|
||||||
add_header Cache-Control "public, immutable";
|
|
||||||
try_files $uri =404;
|
|
||||||
}
|
|
||||||
|
|
||||||
location = /index.html {
|
|
||||||
add_header Cache-Control "no-store";
|
|
||||||
}
|
|
||||||
|
|
||||||
# Einseitenanwendung: jede unbekannte Adresse bekommt index.html.
|
|
||||||
location / {
|
|
||||||
try_files $uri $uri/ /index.html;
|
|
||||||
}
|
|
||||||
|
|
||||||
# ── Backend ───────────────────────────────────────────────────────────
|
|
||||||
location /api/ {
|
|
||||||
include fastcgi_params;
|
|
||||||
fastcgi_pass ekdos_php;
|
|
||||||
|
|
||||||
# Einziger Einstiegspunkt. Es gibt keine weiteren .php-Dateien im Web.
|
|
||||||
fastcgi_param SCRIPT_FILENAME /var/www/ekdos/backend/public/index.php;
|
|
||||||
fastcgi_param SCRIPT_NAME /index.php;
|
|
||||||
fastcgi_param DOCUMENT_ROOT /var/www/ekdos/backend/public;
|
|
||||||
|
|
||||||
# Der Rechnungsabgleich und grosse PDFs brauchen Luft.
|
|
||||||
fastcgi_read_timeout 60s;
|
|
||||||
fastcgi_buffering off;
|
|
||||||
|
|
||||||
# Damit die Anmeldebremse die echte Adresse sieht.
|
|
||||||
fastcgi_param HTTP_X_FORWARDED_FOR $proxy_add_x_forwarded_for;
|
|
||||||
fastcgi_param HTTPS on;
|
|
||||||
}
|
|
||||||
|
|
||||||
# PHP wird nirgendwo sonst ausgeführt.
|
|
||||||
location ~ \.php$ {
|
|
||||||
return 404;
|
|
||||||
}
|
|
||||||
|
|
||||||
# Punktdateien bleiben unerreichbar (.env, .git und Ähnliches).
|
|
||||||
location ~ /\. {
|
|
||||||
deny all;
|
|
||||||
}
|
|
||||||
|
|
||||||
access_log /var/log/nginx/ekdos.access.log;
|
|
||||||
error_log /var/log/nginx/ekdos.error.log warn;
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# Eine hochgeladene Veröffentlichung aktivieren.
|
||||||
|
#
|
||||||
|
# Läuft auf dem Webserver als Benutzer `deploy`. Die Gitea-Strecke ruft das Skript
|
||||||
|
# nach dem Hochladen auf; von Hand geht es genauso:
|
||||||
|
#
|
||||||
|
# /var/www/ekdos/deploy/release.sh 2f9c1ab
|
||||||
|
#
|
||||||
|
# Erwartete Verzeichnisse:
|
||||||
|
#
|
||||||
|
# $ROOT/releases/<name>/public das Vite-Bündel
|
||||||
|
# $ROOT/releases/<name>/backend das PHP-Backend (noch ohne vendor/)
|
||||||
|
# $ROOT/shared/.env die Konfiguration, überlebt jedes Ausrollen
|
||||||
|
# $ROOT/current -> releases/<name> der Symlink, den nginx liest
|
||||||
|
#
|
||||||
|
# Beide Hälften werden mit einem einzigen Symlink-Tausch gemeinsam sichtbar.
|
||||||
|
# Schlägt vorher etwas fehl, bricht das Skript ab und die laufende Fassung
|
||||||
|
# bleibt unberührt.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT="${EKDOS_ROOT:-/var/www/ekdos}"
|
||||||
|
KEEP="${EKDOS_KEEP:-5}"
|
||||||
|
FPM_SERVICE="${EKDOS_FPM:-php8.5-fpm}"
|
||||||
|
HEALTH_URL="${EKDOS_HEALTH_URL:-http://127.0.0.1/api/health}"
|
||||||
|
|
||||||
|
RELEASE="${1:-}"
|
||||||
|
[ -n "$RELEASE" ] || { echo "Aufruf: release.sh <veroeffentlichung>" >&2; exit 2; }
|
||||||
|
|
||||||
|
RELEASE_DIR="$ROOT/releases/$RELEASE"
|
||||||
|
BACKEND="$RELEASE_DIR/backend"
|
||||||
|
|
||||||
|
step() { printf '\n\033[1m==> %s\033[0m\n' "$1"; }
|
||||||
|
fail() { printf '\033[31mFEHLER: %s\033[0m\n' "$1" >&2; exit 1; }
|
||||||
|
|
||||||
|
# ── 1. Prüfen, was da hochgeladen wurde ──────────────────────────────────────
|
||||||
|
step "Veröffentlichung $RELEASE prüfen"
|
||||||
|
[ -d "$RELEASE_DIR" ] || fail "$RELEASE_DIR gibt es nicht."
|
||||||
|
[ -f "$RELEASE_DIR/public/index.html" ] || fail "Im Bündel fehlt public/index.html."
|
||||||
|
[ -f "$BACKEND/public/index.php" ] || fail "Im Backend fehlt public/index.php."
|
||||||
|
[ -f "$ROOT/shared/.env" ] || fail "$ROOT/shared/.env fehlt. Einmalig aus backend/.env.example anlegen."
|
||||||
|
|
||||||
|
# ── 2. Gemeinsame Dateien einhängen ──────────────────────────────────────────
|
||||||
|
# Die .env gehört dem Server, nicht dem Repository, und wird nie mit ausgerollt.
|
||||||
|
step "Konfiguration verknüpfen"
|
||||||
|
ln -sfn "$ROOT/shared/.env" "$BACKEND/.env"
|
||||||
|
|
||||||
|
# var/cache bleibt bewusst je Veröffentlichung eigen: dort liegt der von PHP-DI
|
||||||
|
# kompilierte Container. Geteilt über mehrere Veröffentlichungen hinweg würde er
|
||||||
|
# auf Klassen der alten Fassung zeigen.
|
||||||
|
mkdir -p "$BACKEND/var/cache"
|
||||||
|
chmod 2775 "$BACKEND/var" "$BACKEND/var/cache"
|
||||||
|
|
||||||
|
# ── 3. Abhängigkeiten auf der Zielmaschine auflösen ──────────────────────────
|
||||||
|
# Bewusst hier und nicht im Gitea-Runner: nur hier stimmen PHP-Version und
|
||||||
|
# Erweiterungen (pdo_pgsql, redis), sodass die Plattformprüfung von Composer
|
||||||
|
# etwas Echtes prüft statt übersprungen zu werden.
|
||||||
|
step "Composer-Abhängigkeiten installieren"
|
||||||
|
( cd "$BACKEND" && composer install --no-dev --no-interaction --no-progress --prefer-dist --optimize-autoloader --classmap-authoritative )
|
||||||
|
|
||||||
|
# ── 4. Schema fortschreiben, bevor die neue Fassung sichtbar wird ────────────
|
||||||
|
step "Datenbankschema fortschreiben"
|
||||||
|
php "$BACKEND/bin/ekdos" migrate
|
||||||
|
|
||||||
|
# ── 5. Umschalten ────────────────────────────────────────────────────────────
|
||||||
|
# ln -sfn legt bei einem vorhandenen Symlink sonst *im* Ziel an, deshalb erst
|
||||||
|
# einen neuen Namen anlegen und dann mit mv -T atomar darüberschieben.
|
||||||
|
step "Auf $RELEASE umschalten"
|
||||||
|
PREVIOUS="$(readlink "$ROOT/current" 2>/dev/null || echo '')"
|
||||||
|
ln -sfn "releases/$RELEASE" "$ROOT/current.new"
|
||||||
|
mv -Tf "$ROOT/current.new" "$ROOT/current"
|
||||||
|
[ -n "$PREVIOUS" ] && printf '%s\n' "$PREVIOUS" > "$ROOT/previous" || true
|
||||||
|
|
||||||
|
# ── 6. php-fpm neu laden ─────────────────────────────────────────────────────
|
||||||
|
# Der Pool fährt mit opcache.validate_timestamps=0. Ohne Neuladen liefe weiter
|
||||||
|
# der alte Quelltext aus dem Opcache.
|
||||||
|
step "$FPM_SERVICE neu laden"
|
||||||
|
sudo systemctl reload "$FPM_SERVICE"
|
||||||
|
|
||||||
|
# ── 7. Zwischenspeicher verwerfen ────────────────────────────────────────────
|
||||||
|
# Zwischengespeicherte n8n-Antworten passen womöglich nicht mehr zur neuen
|
||||||
|
# Feldabbildung. Sitzungen bleiben unberührt -- niemand wird abgemeldet.
|
||||||
|
step "n8n-Cache verwerfen"
|
||||||
|
php "$BACKEND/bin/ekdos" cache:flush
|
||||||
|
|
||||||
|
# ── 8. Nachsehen, ob es läuft ────────────────────────────────────────────────
|
||||||
|
step "Gesundheitsprüfung"
|
||||||
|
for attempt in 1 2 3 4 5; do
|
||||||
|
if curl -fsS --max-time 5 "$HEALTH_URL" > /dev/null; then
|
||||||
|
echo " $HEALTH_URL antwortet."
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
|
||||||
|
[ "$attempt" -eq 5 ] && fail "$HEALTH_URL antwortet nicht. Zurück mit: $ROOT/deploy/rollback.sh"
|
||||||
|
echo " Versuch $attempt fehlgeschlagen, neuer Versuch …"
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
|
||||||
|
# ── 9. Aufräumen ─────────────────────────────────────────────────────────────
|
||||||
|
# Die aktuelle und die vorige Veröffentlichung bleiben in jedem Fall liegen.
|
||||||
|
step "Alte Veröffentlichungen aufräumen (die letzten $KEEP bleiben)"
|
||||||
|
KEEP_LIST="$(printf '%s\n%s\n' "$RELEASE" "$(basename "${PREVIOUS:-none}")")"
|
||||||
|
|
||||||
|
# find statt ls: kein unexpandiertes */-Glob, das unter `pipefail` den ganzen
|
||||||
|
# Lauf nach einem bereits erfolgreichen Ausrollen scheitern liesse.
|
||||||
|
OBSOLETE="$(cd "$ROOT/releases" && find . -mindepth 1 -maxdepth 1 -type d -printf '%T@ %f\n' \
|
||||||
|
| sort -rn | cut -d' ' -f2- | tail -n "+$((KEEP + 1))" || true)"
|
||||||
|
|
||||||
|
if [ -n "$OBSOLETE" ]; then
|
||||||
|
printf '%s\n' "$OBSOLETE" | while read -r old; do
|
||||||
|
[ -n "$old" ] || continue
|
||||||
|
|
||||||
|
# Die aktive und die vorige Veröffentlichung bleiben in jedem Fall liegen.
|
||||||
|
if printf '%s\n' "$KEEP_LIST" | grep -qxF "$old"; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo " entferne $old"
|
||||||
|
rm -rf -- "$ROOT/releases/$old"
|
||||||
|
done
|
||||||
|
else
|
||||||
|
echo " nichts aufzuräumen"
|
||||||
|
fi
|
||||||
|
|
||||||
|
step "$RELEASE ist aktiv."
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# Auf die vorige Veröffentlichung zurückschalten.
|
||||||
|
#
|
||||||
|
# /var/www/ekdos/deploy/rollback.sh # eine zurück
|
||||||
|
# /var/www/ekdos/deploy/rollback.sh 2f9c1ab # auf eine bestimmte
|
||||||
|
# /var/www/ekdos/deploy/rollback.sh --list # zeigt, was vorrätig ist
|
||||||
|
#
|
||||||
|
# Es wird nur der Symlink getauscht und php-fpm neu geladen. Die alten
|
||||||
|
# Veröffentlichungen liegen vollständig da, ein Rückschritt dauert Sekunden.
|
||||||
|
#
|
||||||
|
# ACHTUNG: Datenbankmigrationen werden NICHT zurückgenommen. Hat die neue
|
||||||
|
# Fassung das Schema geändert, muss die alte damit umgehen können. Bei den
|
||||||
|
# bisherigen Migrationen (nur Tabellen anlegen) ist das der Fall.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT="${EKDOS_ROOT:-/var/www/ekdos}"
|
||||||
|
FPM_SERVICE="${EKDOS_FPM:-php8.5-fpm}"
|
||||||
|
HEALTH_URL="${EKDOS_HEALTH_URL:-http://127.0.0.1/api/health}"
|
||||||
|
|
||||||
|
step() { printf '\n\033[1m==> %s\033[0m\n' "$1"; }
|
||||||
|
fail() { printf '\033[31mFEHLER: %s\033[0m\n' "$1" >&2; exit 1; }
|
||||||
|
|
||||||
|
CURRENT="$(basename "$(readlink "$ROOT/current" 2>/dev/null || echo none)")"
|
||||||
|
|
||||||
|
if [ "${1:-}" = "--list" ]; then
|
||||||
|
echo "Vorrätige Veröffentlichungen (neueste zuerst):"
|
||||||
|
( cd "$ROOT/releases" && ls -1td -- */ | sed 's#/$##' ) | while read -r name; do
|
||||||
|
[ "$name" = "$CURRENT" ] && echo " * $name (aktiv)" || echo " $name"
|
||||||
|
done
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
TARGET="${1:-}"
|
||||||
|
|
||||||
|
if [ -z "$TARGET" ]; then
|
||||||
|
# Ohne Argument: der Eintrag, den release.sh beim letzten Umschalten notiert hat.
|
||||||
|
[ -f "$ROOT/previous" ] || fail "Keine vorige Veröffentlichung vermerkt. Auswahl zeigen mit: rollback.sh --list"
|
||||||
|
TARGET="$(basename "$(cat "$ROOT/previous")")"
|
||||||
|
fi
|
||||||
|
|
||||||
|
[ -d "$ROOT/releases/$TARGET" ] || fail "Die Veröffentlichung '$TARGET' gibt es nicht. Auswahl zeigen mit: rollback.sh --list"
|
||||||
|
[ "$TARGET" != "$CURRENT" ] || fail "'$TARGET' ist bereits aktiv."
|
||||||
|
|
||||||
|
step "Zurück von $CURRENT auf $TARGET"
|
||||||
|
ln -sfn "releases/$TARGET" "$ROOT/current.new"
|
||||||
|
mv -Tf "$ROOT/current.new" "$ROOT/current"
|
||||||
|
printf '%s\n' "$CURRENT" > "$ROOT/previous"
|
||||||
|
|
||||||
|
step "$FPM_SERVICE neu laden"
|
||||||
|
sudo systemctl reload "$FPM_SERVICE"
|
||||||
|
|
||||||
|
step "n8n-Cache verwerfen"
|
||||||
|
php "$ROOT/current/backend/bin/ekdos" cache:flush
|
||||||
|
|
||||||
|
step "Gesundheitsprüfung"
|
||||||
|
curl -fsS --max-time 5 "$HEALTH_URL" > /dev/null || fail "$HEALTH_URL antwortet nicht."
|
||||||
|
|
||||||
|
step "$TARGET ist wieder aktiv."
|
||||||
+190
-55
@@ -3,14 +3,42 @@
|
|||||||
Zielsystem: Debian/Ubuntu mit nginx und php8.5-fpm. Postgres und Redis laufen
|
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).
|
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
|
## 1. Pakete
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo apt update
|
sudo apt update
|
||||||
sudo apt install -y nginx php8.5-fpm php8.5-cli php8.5-pgsql php8.5-redis \
|
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
|
`php8.5-intl` ist optional, verbessert aber die Erkennung der Kundenrücksprache
|
||||||
in Ticketberichten (Unicode-Normalisierung). Ohne die Erweiterung greift ein
|
in Ticketberichten (Unicode-Normalisierung). Ohne die Erweiterung greift ein
|
||||||
Rückfall ohne Normalisierung.
|
Rückfall ohne Normalisierung.
|
||||||
@@ -29,42 +57,55 @@ 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
|
> angemeldet und jede Ansicht fragt wieder direkt bei n8n an. Die Anwendung
|
||||||
> läuft weiter, aber langsamer und mit erneuter Anmeldung.
|
> 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
|
```bash
|
||||||
sudo mkdir -p /var/www/ekdos/{releases,backend}
|
|
||||||
sudo useradd --system --home /var/www/ekdos --shell /usr/sbin/nologin ekdos
|
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
|
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
|
```bash
|
||||||
cd /var/www/ekdos/backend
|
echo 'deploy ALL=(root) NOPASSWD: /bin/systemctl reload php8.5-fpm' \
|
||||||
sudo -u ekdos cp .env.example .env
|
| sudo tee /etc/sudoers.d/ekdos-deploy
|
||||||
sudo -u ekdos editor .env
|
sudo chmod 440 /etc/sudoers.d/ekdos-deploy
|
||||||
sudo -u ekdos composer install --no-dev --optimize-autoloader
|
sudo visudo -c
|
||||||
sudo -u ekdos php bin/ekdos migrate
|
|
||||||
sudo -u ekdos php bin/ekdos user:create # erster Administrator
|
|
||||||
sudo -u ekdos php bin/ekdos check
|
|
||||||
```
|
```
|
||||||
|
|
||||||
`.env` gehört dem Benutzer `ekdos` und sollte `chmod 600` sein. Sie wird beim
|
## 4. Konfiguration anlegen
|
||||||
Ausrollen ausdrücklich nicht überschrieben.
|
|
||||||
|
```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
|
### n8n-Adressen
|
||||||
|
|
||||||
| Einstellung | Wert | warum |
|
| Einstellung | Wert | warum |
|
||||||
| ------------------- | --------------------------------- | ----------------------------------------------------------- |
|
| ------------------- | -------------------------------- | ---------------------------------------------------------- |
|
||||||
| `N8N_PUBLIC_BASE` | `https://n8n.elektro-krueger.eu` | Der Dienst auf Port 5678 ist nur über den CNAME erreichbar |
|
| `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 |
|
| `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.
|
|
||||||
|
|
||||||
## 5. php-fpm
|
## 5. php-fpm
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -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
|
Der Pool setzt `opcache.validate_timestamps = 0`. Neuer Quelltext wird deshalb
|
||||||
**erst nach einem `systemctl reload php8.5-fpm`** wirksam -- die Ausrollstrecke
|
**erst nach einem `systemctl reload php8.5-fpm`** wirksam -- `release.sh` macht
|
||||||
macht das selbst.
|
das selbst.
|
||||||
|
|
||||||
## 6. nginx
|
## 6. nginx
|
||||||
|
|
||||||
```bash
|
Die nginx-Konfiguration wird auf dem Server gepflegt und gehört bewusst nicht
|
||||||
sudo cp deploy/nginx/ekdos.conf /etc/nginx/sites-available/ekdos.conf
|
ins Repository. Was sie leisten muss:
|
||||||
sudo ln -sf /etc/nginx/sites-available/ekdos.conf /etc/nginx/sites-enabled/
|
|
||||||
sudo nginx -t && sudo systemctl reload nginx
|
|
||||||
```
|
|
||||||
|
|
||||||
Kurzfassung: `fastcgi_pass unix:/run/php/ekdos.sock` für `/api/`, alles andere
|
| Pfad | Ziel |
|
||||||
`try_files $uri $uri/ /index.html`. Kein Anwendungsserver, kein Reverse Proxy
|
| ------------- | ------------------------------------------------------------------------ |
|
||||||
auf einen Node-Prozess.
|
| `/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`
|
Drei Punkte, die erfahrungsgemäss Ärger machen:
|
||||||
gesetzt und der 443-Block entfernt werden. Sobald TLS anliegt: wieder auf `true`.
|
|
||||||
|
|
||||||
## 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
|
Läuft der Zugriff nur intern über HTTP, muss `SESSION_COOKIE_SECURE=false`
|
||||||
`main`. Nötig sind:
|
gesetzt sein -- ein Secure-Cookie wird über reines HTTP nicht gesendet.
|
||||||
|
|
||||||
- ein SSH-Schlüssel als Gitea-Secret `DEPLOY_SSH_KEY`
|
## 7. Erstes Ausrollen von Hand
|
||||||
- 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
|
|
||||||
|
|
||||||
```
|
Die Strecke übernimmt das später, aber einmal von Hand zeigt, ob alles steht:
|
||||||
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
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd frontend && pnpm install --frozen-lockfile && pnpm build
|
# auf dem Arbeitsplatz
|
||||||
rsync -az --delete frontend/dist/ server:/var/www/ekdos/releases/manuell/
|
cd frontend && pnpm install && pnpm build && cd ..
|
||||||
ssh server 'cd /var/www/ekdos && ln -sfn releases/manuell current && sudo systemctl reload php8.5-fpm'
|
|
||||||
|
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
|
## Prüfen, wenn etwas klemmt
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
php backend/bin/ekdos check # Postgres, Redis, n8n
|
php /var/www/ekdos/current/backend/bin/ekdos check # Postgres, Redis, n8n
|
||||||
curl -sS https://<host>/api/health # ohne Anmeldung erreichbar
|
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/php/ekdos-error.log # Ausnahmen aus dem Backend
|
||||||
tail -f /var/log/nginx/ekdos.error.log
|
tail -f /var/log/nginx/ekdos.error.log
|
||||||
```
|
```
|
||||||
|
|
||||||
| Symptom | meistens |
|
| Symptom | meistens |
|
||||||
| ------------------------------------------- | --------------------------------------------------------------------- |
|
| ----------------------------------------------- | ------------------------------------------------------------------ |
|
||||||
| 502 auf `/api/` | Socket fehlt oder falsche Rechte; `systemctl status php8.5-fpm` |
|
| 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 |
|
| 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 |
|
| 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 |
|
| 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` |
|
| 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` |
|
| 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
|
## Alle Benutzer aussperren
|
||||||
|
|
||||||
|
|||||||
+38
@@ -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
|
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
|
keine Aufgaben zugewiesen bekommen. Dafür müsste der n8n-Workflow *Interne
|
||||||
Aufgaben* einen freien Empfängerschlüssel annehmen.
|
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.
|
||||||
|
|||||||
Reference in New Issue
Block a user