b2in/dev/git-deploy-monitoring/01-git-deploy.md
Kevin Adametz b5ed756d67 Git-Deploy per Tag, Betriebsüberwachung und Release-Seeder für den Livegang
- dev/betrieb/deploy.sh: Deploy nur per Tag, mit --seed (Seeder im Wartungsmodus)
  und --initial (Vorschau/Live einmalig an Ort und Stelle auf Git umstellen)
- dev/betrieb/vorschau-auffrischen.sh: Live-DB und Live-Medien in die Vorschau (Generalprobe)
- InteriorReleaseSeeder: alle Inhaltsschritte des Relaunchs in einem Lauf, idempotent;
  Displays, Playlists und Mediathek bleiben unberührt
- DisplayInteriorBrandingSeeder überschreibt keinen inzwischen gepflegten Claim mehr
- Monitoring: GlitchTip (Server + Browser), /health mit Token, Scheduler-Herzschlag,
  Alarme per Mail und ntfy, Version in about und /health
- public/build versioniert; .gitignore: macOS-Muster Icon traf resources/views/flux/icon/
- Runbook: dev/betrieb/README.md

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 08:56:23 +00:00

9.9 KiB

01 — Git-Deploy per Tag

Ausgeliefert wird ein Git-Tag, nie ein Branch-Stand. Das Live-Verzeichnis ist ein Git-Checkout dieses Tags. Ein Skript (vorlagen/deploy.sh) führt den Deploy von Hand per SSH aus und bricht ab, bevor es etwas verändert, wenn etwas nicht stimmt.

Erprobt mit: Ploi-verwaltetem Ubuntu-Server, PHP-FPM 8.4, Nginx, Supervisor, MySQL, Forgejo als Git-Server. Läuft genauso mit Forge, plain VPS, GitHub/GitLab.


Grundsätze

  • Nur Tags (vX.Y.Z, SemVer): Feature-Release erhöht die mittlere Stelle, Hotfix die letzte. Die Tag-Nachricht ist die Release-Notiz (Inhalt, Migrationen, neue .env-Werte, genauer Deploy-Befehl).
  • Deploy nur von Hand per SSH. Push-Webhooks und „Quick Deploy“ im Server-Panel bleiben aus — ein Push ist noch kein Release.
  • Assets lokal bauen und committen (public/build versioniert). Auf dem Server gibt es kein Node. Vite benennt Dateien nach Inhalt — ohne Frontend-Änderung kein Diff.
  • Auf dem Server wird nichts von Hand geändert. Das Skript bricht ab, sobald der Checkout vom Tag abweicht. Auch Hotfixes laufen über Commit → Tag → Deploy.
  • Kein git clean auf Live. .env, storage/, vendor/, RELEASE, public/storage sind ignoriert und bleiben unberührt.
  • Deploy im selben Verzeichnis mit php artisan down (Sekunden), keine Release-Verzeichnisse — storage/app bleibt an seinem Ort.
  • Migrationen sind additiv. Der alte Code muss mit dem neuen Schema laufen; dann ist der Rollback einfach der vorherige Tag. Kein migrate:rollback auf Live.

Voraussetzungen im Repository

.gitignore muss enthalten bzw. darf nicht enthalten:

/RELEASE           # schreibt deploy.sh beim Deploy
/.env
/vendor
/node_modules
/public/hot
/public/storage
/storage/*.key
# NICHT ignorieren: /public/build  (wird versioniert)

Prüfen, ob ein Muster versehentlich echte Dateien trifft (macOS-Muster wie Icon treffen bei core.ignorecase=true auch …/icon/):

git ls-files --others --ignored --exclude-standard | grep -v -E '^(vendor|node_modules|storage)/' | head -50

Version in der App: vorlagen/laravel/app/Support/Monitoring/Release.php plus

// config/app.php
'version' => App\Support\Monitoring\Release::current(env('APP_VERSION')),

deploy.sh nach dev/betrieb/deploy.sh (oder anders, dann DEPLOY_SCRIPT im Skript anpassen) und die Einstellungen oben setzen:

DEPLOY_SCRIPT="dev/betrieb/deploy.sh"   # Pfad im Repository
PHP_FPM_SERVICE="php8.4-fpm"            # systemd-Name
ABOUT_SECTION=""                        # Abschnitt aus AboutCommand::add, leer = RELEASE ausgeben
QUEUE_PROBE=1                           # 0, wenn das Projekt keine Queue hat

QUEUE_PROBE=1 setzt monitoring:queue-probe voraus (03).


A — Einmalig: Server-Zugang zum Git-Server

Auf dem Server als App-Benutzer (bei Ploi ploi):

  1. git --version

  2. Deploy-Key erzeugen und im Git-Server nur lesend am Repository eintragen:

    ssh-keygen -t ed25519 -f ~/.ssh/deploy_b2inapp -N "" -C "b2inapp-live"
    cat ~/.ssh/deploy_b2inapp.pub
    
  3. ~/.ssh/config:

    Host forge.adametz.media 
        Port 222
        User git
        IdentityFile ~/.ssh/deploy_b2inapp
        IdentitiesOnly yes
    
  4. Host-Key aufnehmen, Fingerprint mit dem im Git-Server angezeigten vergleichen:

    ssh-keyscan -p 222 forge.adametz.media  >> ~/.ssh/known_hosts
    ssh-keygen -lf <(ssh-keyscan -p 222 forge.adametz.media  2>/dev/null)
    
  5. Test: git ls-remote ssh://git@forge.adametz.media :222/kadmin/b2in.git zeigt die Tags.

  6. FPM-Reload ohne Passwort testen (Ploi erlaubt das dem Benutzer ploi):

    ps -o pid,lstart,args -C php-fpm8.4 | head -3
    echo "" | sudo -S service php8.4-fpm reload; echo "Exit: $?"
    sleep 2; ps -o pid,lstart,args -C php-fpm8.4 | head -3
    

    Exit 0, master process mit gleicher PID, pool www neu → das Skript lädt FPM selbst. Fragt sudo nach einem Passwort: beim Deploy DEPLOY_FPM_RELOAD= setzen, das Skript wartet dann auf Enter, bis FPM im Panel neu geladen ist. Immer Reload, nie Restart — Restart entfernt kurz den Socket, Nginx liefert 502.

  7. Supervisor und Worker prüfen: systemctl is-enabled supervisor → enabled; im Panel ein Worker mit php artisan queue:work <connection> --queue=<queues> --tries=3 --max-time=3600.

B — Einmalig: Live-Verzeichnis zum Checkout machen

B1 — Neues Projekt

cd ~ && git clone ssh://git@git.example.com:222/team/meineapp.git meineapp.de
cd meineapp.de && git checkout --detach v1.0.0
cp /pfad/zur/.env .env && composer install --no-dev --optimize-autoloader
echo v1.0.0 > RELEASE && php artisan migrate --force && php artisan config:cache

B2 — Bestehendes Verzeichnis (bisher SFTP o. Ä.) an Ort und Stelle umwandeln

Erst ein Tag vA erstellen, der genau dem Live-Stand entspricht.

cd ~ && tar czf backup-vor-git.tgz --exclude=./storage --exclude=./node_modules -C ~/meineapp.de .
cd ~/meineapp.de
git init -q
git remote add origin ssh://git@git.example.com:222/team/meineapp.git
git fetch --tags origin
git update-ref --no-deref HEAD "$(git rev-parse 'vA^{commit}')"
git read-tree HEAD            # Index = Tag, Dateien bleiben unangetastet
git status --porcelain        # Abweichungen sichten
Zeile Bedeutung Vorgehen
M datei Live weicht vom Tag ab git diff -- datei. Fehlt die Änderung im Repository: erst lokal nachziehen, neuer Tag. Sonst Altstand, wird überschrieben
D datei fehlt auf Live meist Entwicklerdatei; der Checkout legt sie an
?? datei nur auf Live nicht löschen — nach ~/vor-git/ verschieben oder in .gitignore aufnehmen (neuer Tag)

Bekannte Funde: fehlende .gitignore auf Live (dann erscheinen .env, vendor/ als ?? — verschwinden nach dem Checkout), per SFTP gelöschte Altdateien, Pfade mit Umlauten (Git setzt sie in Anführungszeichen — beim Filtern mit grep beachten).

Wenn nichts Unerklärtes übrig ist:

git checkout -q -f --detach vA
git status --porcelain        # muss leer sein

Beim ersten Deploy existiert deploy.sh evtl. noch nicht im Live-Stand:

git show vB:dev/betrieb/deploy.sh > /tmp/deploy.sh && bash /tmp/deploy.sh vB

C — Release lokal (jedes Mal)

# 1. sauber und grün
git status                                   # nichts offen
php artisan test --compact --parallel && vendor/bin/phpstan analyse

# 2. Assets bauen und committen
npm ci && npm run build
git add public/build
git diff --cached --quiet public/build || git commit -m "build: Assets für vX.Y.Z"

# 3. Tag mit Release-Notiz, pushen (Tags mitnehmen!)
git tag -a vX.Y.Z -m "vX.Y.Z — <Inhalt>

Deploy: <keine Migration | N Migrationen>, <neue .env-Werte>
  bash dev/betrieb/deploy.sh vX.Y.Z [--migrations]"
git push origin main --follow-tags

D — Deploy (jedes Mal, auf dem Server)

  1. Bringt der Tag Migrationen mit: DB-Backup.

  2. Neue .env-Werte laut Release-Notiz vorher eintragen.

  3. Deploy:

    cd ~/meineapp.de
    bash dev/betrieb/deploy.sh vX.Y.Z                 # ohne Migrationen
    bash dev/betrieb/deploy.sh vX.Y.Z --migrations    # mit Migrationen
    

Ablauf im Skript:

Schritt Bei Fehler
Arbeitsverzeichnis sauber? Abbruch, nichts verändert
git fetch --tags, Tag vorhanden? Abbruch, nichts verändert
Neue Migrationen? Ohne --migrations Abbruch, nichts verändert
down → Checkout → RELEASE → composer install → migrate → Livewire-Assets → Caches → queue:restart → FPM-Reload → up Seite bleibt im Wartungsmodus, Skript nennt den Rollback-Befehl
Version ausgeben —
monitoring:queue-probe --wait=60 Exit 3: live, aber kein Worker — Supervisor/Worker prüfen

Optionen: --clear-schedule (setzt withoutOverlapping-Sperren zurück, nur wenn sich Sperrdauern geändert haben). Lange Migrationen: DB_LOCK_WAIT_TIMEOUT=300 bash dev/betrieb/deploy.sh vX.Y.Z --migrations.

Exit-Codes: 0 fertig · 1 abgebrochen · 2 falscher Aufruf · 3 kein Worker.

Hinweis: Das Skript läuft aus einer Kopie des vorher ausgecheckten Stands (Bash liest Skripte stückweise, Git ersetzt die Datei beim Checkout). Neue Schritte im Skript greifen also erst beim übernächsten Deploy — beim einführenden Deploy von Hand nachholen.

  1. Prüfen: /health mit Token zeigt "version":"vX.Y.Z", Uptime Kuma grün, GlitchTip ohne neue Einträge, Logs ohne neue Fehler.

E — Rollback

bash dev/betrieb/deploy.sh <vorheriger Tag>

Baut vendor/ und Caches für den alten Tag neu auf. Nur bei Datenproblemen das DB-Backup einspielen.


Fallstricke

  • FPM-Restart statt Reload → 502 für Sekunden (Nginx: connect() to unix:…fpm.sock failed). Einzelne recv() failed (104: Connection reset by peer) genau zum Deploy-Zeitpunkt sind normal (laufende Anfragen beim Reload). Abhilfe wäre process_control_timeout = 10s im Abschnitt [global] von /etc/php/8.4/fpm/php-fpm.conf (root) — nie in die Pool-Datei (www.conf), dort ist der Schlüssel unbekannt und FPM startet nicht mehr.
  • Supervisor gestoppt → queue:restart beendet den Worker, niemand startet ihn neu. Deshalb die Worker-Prüfung am Ende. Notlösung ohne sudo: nohup php artisan queue:work --tries=3 --max-time=3600 >> storage/logs/worker-manual.log 2>&1 &
  • Nie queue:retry all nach Ausfällen: alte Jobs tragen den Stand ihrer Klasse von damals und scheitern oder verschicken veraltete Mails. Erst queue:failed ansehen, gezielt queue:retry <uuid>.
  • Push ohne Tags → deploy.sh findet den Tag nicht. Immer --follow-tags.
  • RELEASE nicht von Hand neu erzeugen auf einem anderen Stand als dem Tag (ergibt vX.Y.Z-3-gabc-dirty). Das Skript schreibt sie.
  • Server-Panel-Logs: Ploi führt für Sites kein Nginx-Zugriffsprotokoll; das Fehlerlog /var/log/nginx/<site>-error.log ist für ploi lesbar.