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>
262 lines
15 KiB
Markdown
262 lines
15 KiB
Markdown
# 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. |
|