presseportale/dev/git-deploy-monitoring/01-git-deploy.md
Kevin Adametz 32e1349517 dev: Baukasten Git-Deploy und Betriebsüberwachung für andere Laravel-Projekte (dev/git-deploy-monitoring) — Anleitungen, Checkliste, Vorlagen für Laravel, deploy.sh und Monitoring-Server
Vorlagen in einem frischen Laravel 12 eingebaut (30 Tests grün), deploy.sh
gegen ein simuliertes Repository durchgespielt (Deploy, Abbruch bei
Migrationen, Drift, Rollback, fehlender Worker). GlitchTip-Aufbewahrung heißt
seit Version 6 GLITCHTIP_RETENTION_DAYS — in der Projekt-Doku korrigiert.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 09:45:16 +00:00

261 lines
9.9 KiB
Markdown

# 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:
```gitignore
/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/`):
```bash
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
```php
// 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:
```bash
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:
```bash
ssh-keygen -t ed25519 -f ~/.ssh/deploy_meineapp -N "" -C "meineapp-live"
cat ~/.ssh/deploy_meineapp.pub
```
3. `~/.ssh/config`:
```
Host git.example.com
Port 222
User git
IdentityFile ~/.ssh/deploy_meineapp
IdentitiesOnly yes
```
4. Host-Key aufnehmen, Fingerprint mit dem im Git-Server angezeigten vergleichen:
```bash
ssh-keyscan -p 222 git.example.com >> ~/.ssh/known_hosts
ssh-keygen -lf <(ssh-keyscan -p 222 git.example.com 2>/dev/null)
```
5. Test: `git ls-remote ssh://git@git.example.com:222/team/meineapp.git` zeigt die Tags.
6. **FPM-Reload ohne Passwort testen** (Ploi erlaubt das dem Benutzer `ploi`):
```bash
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
```bash
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.
```bash
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:
```bash
git checkout -q -f --detach vA
git status --porcelain # muss leer sein
```
Beim ersten Deploy existiert `deploy.sh` evtl. noch nicht im Live-Stand:
```bash
git show vB:dev/betrieb/deploy.sh > /tmp/deploy.sh && bash /tmp/deploy.sh vB
```
---
## C — Release lokal (jedes Mal)
```bash
# 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:
```bash
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.
4. 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
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.