- 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>
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/buildversioniert). 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 cleanauf Live..env,storage/,vendor/,RELEASE,public/storagesind ignoriert und bleiben unberührt. - Deploy im selben Verzeichnis mit
php artisan down(Sekunden), keine Release-Verzeichnisse —storage/appbleibt 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:rollbackauf 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):
-
git --version -
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 -
~/.ssh/config:Host forge.adametz.media Port 222 User git IdentityFile ~/.ssh/deploy_b2inapp IdentitiesOnly yes -
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) -
Test:
git ls-remote ssh://git@forge.adametz.media :222/kadmin/b2in.gitzeigt die Tags. -
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 -3Exit 0,
master processmit gleicher PID,pool wwwneu → das Skript lädt FPM selbst. Fragtsudonach einem Passwort: beim DeployDEPLOY_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. -
Supervisor und Worker prüfen:
systemctl is-enabled supervisor→enabled; im Panel ein Worker mitphp 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)
-
Bringt der Tag Migrationen mit: DB-Backup.
-
Neue
.env-Werte laut Release-Notiz vorher eintragen. -
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.
- Prüfen:
/healthmit 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). Einzelnerecv() failed (104: Connection reset by peer)genau zum Deploy-Zeitpunkt sind normal (laufende Anfragen beim Reload). Abhilfe wäreprocess_control_timeout = 10sim 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:restartbeendet 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 allnach Ausfällen: alte Jobs tragen den Stand ihrer Klasse von damals und scheitern oder verschicken veraltete Mails. Erstqueue:failedansehen, gezieltqueue:retry <uuid>. - Push ohne Tags →
deploy.shfindet den Tag nicht. Immer--follow-tags. RELEASEnicht von Hand neu erzeugen auf einem anderen Stand als dem Tag (ergibtvX.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.logist fürploilesbar.