presseportale/dev/betrieb/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

262 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Git-Deploy
> Allgemeine, projektunabhängige Fassung mit Vorlagen: [`../git-deploy-monitoring/`](../git-deploy-monitoring/README.md).
**Stand:** 06.10.2026 · **Status:** umgestellt am 06.10.2026, erster Git-Deploy `v1.1.3` (Protokoll unten)
Bis `v1.1.0` gingen Deploys als Paket per SFTP auf den Server
([`deploy-v1.1.0.md`](deploy-v1.1.0.md)). Künftig ist `~/pressekonto.com` ein
Git-Checkout, und ausgeliefert wird ein Tag aus `origin`.
## Grundsätze
- **Remotes:** `origin` = `ssh://git@forge.adametz.media:222/kadmin/pressekonto.git`
(Forgejo auf Hetzner, privat). Von dort holt der Live-Server.
`NAS` = `git.adametz.media` im lokalen Netz, nur als Backup.
- **Ausgeliefert wird nur ein Tag** (`vX.Y.Z`, SemVer wie bisher), nie ein Branch-Stand.
- **Deploy nur von Hand** per SSH mit [`deploy.sh`](deploy.sh). Ploi-„Quick Deploy"
und Push-Webhooks bleiben aus.
- **Assets werden lokal gebaut und committet.** `public/build` ist versioniert
(2,4 MB, 18 Dateien). Auf dem Server gibt es kein Node.
- **Auf dem Server wird nichts von Hand geändert.** Das Skript bricht ab, sobald
der Checkout vom Tag abweicht. Hotfixes laufen ebenfalls über Commit, Tag und Deploy.
- **Kein `git clean` auf Live.** `.env`, `storage/`, `vendor/`, `auth.json`,
`RELEASE` und `public/storage` sind ignoriert und bleiben unberührt.
- **Kein Zero-Downtime-Modus mit Release-Verzeichnissen.** `storage/app` (Archiv,
Quarantäne, Exporte) bleibt an seinem Ort. Deploy im selben Verzeichnis mit
`php artisan down`, wie bisher.
- `dev/`, `tests/` und die Doku liegen damit auch auf dem Server, aber außerhalb
von `public/`. Ausgeliefert werden sie nicht.
## A — Einmalig: Zugang vom Server zu Forgejo
1. Auf dem Server (Benutzer der App) prüfen: `git --version`.
2. Deploy-Key erzeugen und in Forgejo **nur lesend** am Repository eintragen
(Repository ▸ Einstellungen ▸ Deploy-Schlüssel):
```bash
ssh-keygen -t ed25519 -f ~/.ssh/forge_pressekonto -N "" -C "pressekonto-live"
cat ~/.ssh/forge_pressekonto.pub
```
3. `~/.ssh/config` anlegen bzw. ergänzen (`chmod 600 ~/.ssh/config`). Ploi verwaltet nur
`authorized_keys`, die Datei kommt ihm nicht in die Quere:
```
Host forge.adametz.media
Port 222
User git
IdentityFile ~/.ssh/forge_pressekonto
IdentitiesOnly yes
```
4. Host-Key aufnehmen und den Fingerprint mit dem in Forgejo angezeigten vergleichen:
```bash
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/pressekonto.git`
zeigt `refs/tags/v1.1.0`.
6. FPM-Neustart ohne Ploi testen: `echo "" | sudo -S service php8.4-fpm reload`.
Geht das nicht, beim Deploy `PK_FPM_RELOAD=` setzen. Das Skript wartet dann,
bis FPM in Ploi neu geladen ist.
**Getestet 06.10.2026: funktioniert.** Kein Passwort nötig (Ploi erlaubt dem
Benutzer `ploi` genau diesen Befehl), Exit-Code 0, keine Ausgabe. Prüfen lässt
es sich mit `ps -o pid,lstart,args -C php-fpm8.4` vorher und nachher: der
`master process` behält PID und Startzeit, die `pool www`-Prozesse sind neu.
**`PK_FPM_RELOAD` deshalb nicht setzen** und FPM nicht zusätzlich in Ploi
über „Restart“ neu starten — ein Restart entfernt kurz den Socket, Nginx
meldet dann `connect() to unix:/run/php/php8.4-fpm.sock failed` und liefert
502 (so beim Deploy von `v1.3.0`, 16:51).
## B — Einmalig: Live-Verzeichnis in einen Checkout umwandeln
Live läuft auf `v1.1.0`. Das Verzeichnis wird an Ort und Stelle zum Checkout
dieses Tags. Dabei ändert sich zunächst keine Datei. Git zeigt nur, wo der
Server vom Tag abweicht.
7. Dateibackup wie bei v1.1.0:
```bash
cd ~ && tar czf backup-vor-git.tgz --exclude=./storage --exclude=./node_modules -C ~/pressekonto.com .
```
8. Repository anlegen und auf `v1.1.0` setzen, **ohne** Dateien anzufassen:
```bash
cd ~/pressekonto.com
git init -q
git remote add origin ssh://git@forge.adametz.media:222/kadmin/pressekonto.git
git fetch --tags origin
git update-ref --no-deref HEAD "$(git rev-parse 'v1.1.0^{commit}')"
git read-tree HEAD
```
9. Abweichungen sichten:
```bash
git status --porcelain | grep -v -E '^.. (dev/|docs/|_docs/|tests/|\.agents/|\.codex/|\.cursorrules|\.devcontainer/|\.github/|\.mcp\.json|AGENTS\.md|CLAUDE\.md|boost\.json|docker-compose\.yml|phpstan|phpunit\.xml|pint\.json)'
```
Herausgefiltert sind die Entwicklerdateien, die nie im Paket waren. Was danach
übrig bleibt:
| Zeile | Bedeutung | Vorgehen |
|---|---|---|
| ` M datei` | Datei auf Live weicht vom Tag ab | `git diff -- datei` ansehen. Fehlt die Änderung im Repository, erst lokal nachziehen, neuer Tag, dann weiter. Sonst ist es ein Paketrest und wird überschrieben. |
| ` D datei` | Datei fehlt auf Live | Meist Entwicklerdatei oder ein nie hochgeladener Teil. Einordnen, der Checkout legt sie an. |
| `?? datei` | nur auf Live, nicht ignoriert | **Nicht löschen.** Nach `~/vor-git/` verschieben, wenn sie nicht gebraucht wird. Wird sie gebraucht, gehört sie in die `.gitignore` (lokal, neuer Tag). |
10. Wenn nichts Unerklärtes übrig ist, auf den Tag auschecken:
```bash
git checkout -q -f --detach v1.1.0
git status --porcelain # muss leer sein
```
Der Code ist danach inhaltlich derselbe wie vorher. Ein Neustart ist nicht nötig.
## C — Release lokal (jedes Mal)
11. `main` sauber, volle Suite und Larastan grün (wie bisher).
12. Assets bauen und mitcommitten:
```bash
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"
```
Vite benennt Dateien nach ihrem Inhalt. Ohne Frontend-Änderung entsteht kein Diff.
13. Tag setzen und auf beide Remotes pushen:
```bash
git tag -a vX.Y.Z -m "vX.Y.Z — <Inhalt>"
git push origin main --follow-tags
git push NAS main --follow-tags
```
## D — Deploy auf Live (jedes Mal)
14. Bringt der Tag Migrationen mit: **DB-Backup** ziehen (`ARCHIVE_KEY` getrennt gesichert).
15. Neue `.env`-Werte laut Release-Notiz vorher eintragen.
16. Deploy:
```bash
cd ~/pressekonto.com
bash dev/betrieb/deploy.sh vX.Y.Z # ohne Migrationen
bash dev/betrieb/deploy.sh vX.Y.Z --migrations # mit Migrationen
```
Das Skript macht Folgendes:
- Es prüft, dass der Checkout sauber ist, und holt die Tags.
- Es zeigt neue Migrationen an. Ohne `--migrations` bricht es dann ab, **bevor** etwas verändert ist.
- Danach in dieser Reihenfolge: `down`, Checkout, `RELEASE`, `composer install`, gegebenenfalls `migrate`, Livewire-Assets, Caches, `queue:restart`, FPM, `up`, Version.
- Zum Schluss `php artisan monitoring:queue-probe --wait=60`: ein Test-Job muss innerhalb einer Minute von einem Worker abgeholt werden. Sonst endet das Skript mit **Exit 3** und dem Hinweis, Supervisor und den Worker in Ploi zu prüfen — die Seite ist dann schon live. (Seit `v1.3.2`; das Skript läuft aus der Kopie des *vorherigen* Stands, die Prüfung greift also ab dem Deploy nach `v1.3.2`. Beim Deploy von `v1.3.2` selbst danach von Hand `php artisan monitoring:queue-probe` aufrufen.)
`--clear-schedule` setzt zusätzlich die `withoutOverlapping`-Sperren zurück.
Das ist nur nötig, wenn sich Sperrdauern im Scheduler geändert haben.
Darf eine Migration länger auf Sperren warten:
`DB_LOCK_WAIT_TIMEOUT=300 bash dev/betrieb/deploy.sh vX.Y.Z --migrations`.
17. Prüfungen:
- `/health` mit Token liefert `"version":"vX.Y.Z"`.
- Admin ▸ Reports ▸ Betriebsüberwachung ist grün.
- `storage/logs` zeigt keine neuen Fehler.
- Smoke-Test aus dem Runbook (Abschnitt 8).
- Hat sich der Scheduler geändert: `php artisan schedule:list`.
Schlägt ein Schritt fehl, bleibt die Seite im Wartungsmodus. Das Skript nennt
den Rollback-Befehl.
## E — Rollback
```bash
cd ~/pressekonto.com
bash dev/betrieb/deploy.sh <vorheriger Tag>
```
Liegt die Abbruchstelle nach dem Checkout, ist der Checkout zwar sauber, aber
`vendor/` und die Caches können halb erneuert sein. Der Rollback-Aufruf baut
beides für den alten Tag neu auf. Migrationen sind additiv. Der alte Code läuft
mit den neuen Spalten, ein `migrate:rollback` gibt es auf Live nicht. Nur bei
Datenproblemen das DB-Backup einspielen und danach Runbook Abschnitt 9 (Archiv) befolgen.
## F — Erste Umstellung (erledigt 06.10.2026)
Gedacht war `v1.1.1` als reines Test-Release. Live ging direkt `v1.1.3`. Dazwischen
lagen ein Hotfix (`v1.1.2`) und ein Fehler, der erst bei der Umwandlung auffiel (`v1.1.3`).
Was bei der Umwandlung (B) auftrat, für den Fall, dass ein weiterer Server so umgestellt wird:
- **Auf Live fehlte die `.gitignore`**, sie war nie im Paket. Deshalb zeigte Schritt 9
auch `.env`, `vendor/`, `storage/` und `public/build` als `??`. Nach dem Checkout
verschwinden diese Zeilen.
- **Pfade mit Umlauten oder Leerzeichen** gibt Git in Anführungszeichen aus. Sie rutschen
am Filter in Schritt 9 vorbei. Das betraf nur Entwicklerdateien.
- **Per SFTP gelöschte Dateien liegen noch auf Live.** Gefunden wurde
`app/Actions/Fortify/UpdateUserProfileInformation.php` (seit 18.09. gelöscht). Sie ist
nach `~/vor-git/` verschoben.
- **Eigene Flux-Icons waren nie im Repository.** Das macOS-Muster `Icon` in der `.gitignore`
traf wegen `core.ignorecase` auch `resources/views/flux/icon/`. Behoben in `v1.1.3`.
Für den ersten Deploy war der Ordner per `.git/info/exclude` ausgeblendet, damit der
Checkout die identischen Dateien aus dem Tag übernimmt.
- **Doku war nach dem Checkout noch per SFTP hochgeladen worden** (`dev/betrieb/`). Die
Abweichungsprüfung hat den Deploy gestoppt, bevor etwas verändert war. Die Dateien sind
nach `~/vor-git/` gesichert und auf den Repo-Stand zurückgesetzt.
- **`deploy.sh` gab es in `v1.1.0` noch nicht.** Beim ersten Mal lief es aus dem Tag:
`git show v1.1.3:dev/betrieb/deploy.sh > /tmp/deploy.sh && bash /tmp/deploy.sh v1.1.3`.
- **Alte Build-Dateien** tauchten nach dem Deploy als `??` auf. Bis `v1.1.0` war
`public/build` ignoriert. Sie sind als `~/vor-git/build-reste.tgz` gesichert und entfernt.
Aufräumen nach ein paar stabilen Tagen: `~/deploy-v1.1.0.tgz`,
`~/backup-vor-v1.1.0.tgz`, `~/backup-vor-git.tgz`, `~/vor-git/`.
## Betrieb: PHP-FPM beim Deploy
Jeder Reload bricht Anfragen ab, die in dieser Sekunde laufen. Im Nginx-Fehlerlog
(`/var/log/nginx/pressekonto.com-error.log`, für `ploi` lesbar) erscheinen dann
einzelne `recv() failed (104: Connection reset by peer)`, am 06.10. genau zu den
Deploys von `v1.1.3` (15:37, 1×) und `v1.2.0` (16:06, 6×). Das sind kurze 502er,
fast nur für Bots, kein Absturz.
Abhilfe wäre `process_control_timeout = 10s`: Die alten Worker dürfen laufende
Anfragen noch zu Ende bringen. Die Einstellung gehört in den Abschnitt `[global]`
von `/etc/php/8.4/fpm/php-fpm.conf` (root). Ploi bietet nur „Edit FPM ini“
(`php.ini`, dort wirkungslos), „Edit CLI ini“ und „Edit pool“ (`www.conf`).
**Nie in die Pool-Datei eintragen** — dort ist der Schlüssel unbekannt, FPM
startet beim nächsten Reload nicht mehr und die Seite ist offline.
**Entscheidung 06.10.2026: nicht gesetzt.** Bei Bedarf über den Ploi-Support
oder mit root-Zugang, danach `php-fpm8.4 -t` und erst dann neu laden.
Übrige Einträge im Nginx-Fehlerlog sind unbedenklich: `access forbidden by rule`
(Ploi sperrt Punktdateien, Scanner suchen `/.env` u. Ä.) und `directory index of
…/public/api/ is forbidden` (dort liegt die API-Spezifikation `v1.yml`, ein Aufruf
von genau `/api/` bekommt 403). Ein Zugriffsprotokoll für die Site führt Ploi nicht.
## Später: Changelog
Der Changelog-Entwurf
([Decision-Update](../2026-10/Decision-Update_Changelog_Automatisierung_2026-10-04.md),
Stufe 2) dockt in `deploy.sh` nach `up` an. Ein Artisan-Befehl liest die Commits
zwischen dem vorherigen und dem neuen Tag. Die liegen jetzt auf dem Server vor.
Daraus legt er die Entwürfe an. Der LLM-Schritt läuft als Queue-Job, damit ein
Ausfall des Anbieters keinen Deploy abbricht.
## Protokoll
| Datum | Schritt | Ergebnis |
|---|---|---|
| 06.10.2026 | Vorbereitung | `origin` auf Forgejo (Hetzner) umgestellt, NAS als zweites Remote, Push erledigt. `deploy.sh` gegen ein simuliertes Repository geprüft: Abbruch bei neuen Migrationen ohne Option, Deploy, wiederholter Aufruf, Rollback, unbekannter Tag, Fehler mitten im Lauf. Die Umwandlung eines bestehenden Verzeichnisses (B) wurde ebenso simuliert: Abweichungen werden sichtbar, `.env` und ungetrackte Dateien bleiben, ignorierte Builds werden beim Checkout überschrieben. |
| 06.10.2026 | `v1.1.1` lokal | `/public/build` versioniert. Alte Build-Reste von 2025 lokal entfernt (nicht mehr referenziert). Suite 2.316 grün, einer übersprungen (parallel, 96 s); Larastan ohne Meldung. Build-Commit und Tag `v1.1.1` gesetzt, Push auf `origin`/`NAS` nach Freigabe. |
| 06.10.2026 | `v1.1.2` | Hotfix nach GlitchTip-Auswertung (anderer Chat). Gepusht ohne Tags, Tags mit `git push origin --tags` nachgeholt. Künftig `--follow-tags`. |
| 06.10.2026 | Umwandlung Live (A, B) | Server-Zugang zu Forgejo per Deploy-Key (nur lesen). Git 2.43. Befunde siehe F: fehlende `.gitignore`, Altdatei Fortify, Flux-Icons nicht im Repository. |
| 06.10.2026 | `v1.1.3` | `.gitignore`-Muster `Icon` korrigiert, vier Icons versioniert. Erster Git-Deploy `v1.1.0` → `v1.1.3`: keine Migration, Composer „Nothing to install", Caches, Worker, FPM ohne Fehler, Version `v1.1.3`. Danach alte Build-Dateien entfernt. `v1.1.1` und `v1.1.2` waren nie einzeln live. |
| 06.10.2026 | `v1.2.0` | Volt entfernt (Livewire-4-Single-File-Components). Erster Deploy, der nur noch `deploy.sh` brauchte: keine Migration, Composer entfernt `livewire/volt`. Vom Betreiber durchgeklickt (Login, Kundenbereich, Admin), unauffällig. |
| 06.10.2026 | FPM und Nginx-Fehlerlog | `sudo -S service php8.4-fpm reload` ohne Passwort bestätigt (Master bleibt, Worker neu) — `deploy.sh` lädt FPM künftig selbst, `PK_FPM_RELOAD` bleibt ungesetzt. Nginx-Fehlerlog ausgewertet: Verbindungsabbrüche nur zu den Deploy-Zeitpunkten, `process_control_timeout` bewusst nicht gesetzt (siehe „Betrieb: PHP-FPM beim Deploy“). |
| 06.10.2026 | Worker-Ausfall nach `v1.3.0` | Supervisor lief nicht. `queue:restart` beim Deploy von `v1.3.0` (16:51) hat den Worker beendet, niemand startete ihn neu — KI-Prüfungen, Mails und eine Zahlungsbestätigung lagen über eine Stunde. Aufgefallen über die Systemstatus-Kachel. Supervisor in Ploi neu gestartet, Rückstau mit einem Ersatz-Worker (`nohup`) abgearbeitet. Ursache für den gestoppten Supervisor ohne root-Zugang nicht feststellbar. Folgen: Worker-Prüfung am Ende von `deploy.sh` (`v1.3.2`), externer Monitor auf `/health` (status.adametz.media). |
| 06.10.2026 | `v1.3.2` | Worker-Prüfung im Deploy. Von Hand nachgeholt (Skript lief aus der Kopie von `v1.3.1`): `monitoring:queue-probe` → Test-Job nach 4 s abgeholt. `systemctl is-enabled supervisor` → `enabled`; ein Neustart ohne Autostart scheidet als Ursache des Ausfalls damit weitgehend aus. |