- 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>
235 lines
12 KiB
Markdown
235 lines
12 KiB
Markdown
# Betrieb B2in: Git-Deploy, Livegang, Monitoring
|
||
|
||
**Stand:** 08.10.2026. Grundlage ist der Baukasten unter `dev/git-deploy-monitoring/`. Dort steht,
|
||
warum etwas so gebaut ist. Hier steht, wie es für B2in läuft.
|
||
|
||
| Was | Wo |
|
||
|---|---|
|
||
| Deploy-Skript | `dev/betrieb/deploy.sh`, liefert nur Git-Tags `vX.Y.Z` aus |
|
||
| Vorschau auffrischen | `dev/betrieb/vorschau-auffrischen.sh` kopiert Live-DB und Live-Medien in die Vorschau |
|
||
| Inhaltsschritte Relaunch | `InteriorReleaseSeeder`, alle 29 Seeder in einem Lauf, idempotent |
|
||
| Remote `origin` | lokal `ssh://git@forge.adametz.media:222/kadmin/b2in.git`, auf dem Server `forge-b2in:kadmin/b2in.git` (Host-Alias, Abschnitt 2): Deploy-Quelle für Vorschau und Live |
|
||
| Remote `NAS` | `ssh://git@git.adametz.media:222/gitmedia/b2in.git`: lokale Sicherung, wird nie deployt |
|
||
| Fehlerprotokoll | GlitchTip `https://errors.adametz.media`, Organisation `adametz-media`, Projekte `server-b2in` (ID 8) und `browser-b2in` (ID 7) |
|
||
| Push-Alarme | ntfy `https://push.adametz.media`, Topic `b2in-ops` |
|
||
| Externer Monitor | Uptime Kuma auf `/health` (mit Token) und `/up` je Domain |
|
||
|
||
---
|
||
|
||
## 1. Release lokal (jedes Mal)
|
||
|
||
```bash
|
||
git status # nichts offen
|
||
php artisan test --compact
|
||
npm ci && npm run build:web && npm run build:portal # public/build ist versioniert, der Server hat kein Node
|
||
git add public/build && git diff --cached --quiet public/build || git commit -m "build: Assets für vX.Y.Z"
|
||
|
||
git tag -a vX.Y.Z -m "vX.Y.Z – <Inhalt>
|
||
|
||
Deploy: <keine Migration | N Migrationen>, <Seeder>, <neue .env-Werte>
|
||
bash dev/betrieb/deploy.sh vX.Y.Z [--migrations] [--seed=…]"
|
||
git push origin <branch> --follow-tags
|
||
git push NAS <branch> --follow-tags # Sicherung
|
||
```
|
||
|
||
Versionen folgen SemVer: Ein Feature-Release erhöht die mittlere Stelle, ein Hotfix die letzte.
|
||
Der Interior-Relaunch ist `v2.0.0`.
|
||
|
||
---
|
||
|
||
## 2. Einmalig auf dem Server
|
||
|
||
Als App-Benutzer, gilt für Vorschau und Live (gleicher Server):
|
||
|
||
1. **Deploy-Key**, nur Leserecht am Repository auf forge.adametz.media. Auf dem Server liegen mehrere
|
||
Deploy-Keys für denselben Forgejo (B2in, Breco), deshalb gibt es je Projekt einen Host-Alias.
|
||
Die Repository-Adresse heißt dann **`forge-b2in:kadmin/b2in.git`**, nicht `ssh://git@forge.adametz.media:222/…`;
|
||
mit der vollen Adresse greift kein Alias, und ssh bietet keinen Schlüssel an („Permission denied (publickey)“).
|
||
```bash
|
||
ssh-keygen -t ed25519 -f ~/.ssh/deploy_b2in -N "" -C "b2in-server"
|
||
cat ~/.ssh/deploy_b2in.pub # in Forgejo: kadmin/b2in ▸ Einstellungen ▸ Deploy-Schlüssel, ohne Schreibrecht
|
||
```
|
||
`~/.ssh/config`:
|
||
```
|
||
Host forge-b2in
|
||
HostName forge.adametz.media
|
||
Port 222
|
||
User git
|
||
IdentityFile ~/.ssh/deploy_b2in
|
||
IdentitiesOnly yes
|
||
HostKeyAlias forge.adametz.media
|
||
```
|
||
Test: `git ls-remote forge-b2in:kadmin/b2in.git` zeigt die Tags.
|
||
2. **Flux Pro für Composer:** `deploy.sh` führt `composer install` aus. Dafür braucht der Server die
|
||
Flux-Zugangsdaten, also `auth.json` im App-Verzeichnis (ignoriert, bleibt liegen) oder
|
||
`composer config --global http-basic.composer.fluxui.dev <e-mail> <lizenz>`.
|
||
3. **PHP-FPM-Reload ohne Passwort testen:** `echo "" | sudo -S service php8.4-fpm reload; echo $?`.
|
||
Ergibt das Exit 0, lädt das Skript FPM selbst neu. Fragt sudo nach einem Passwort, beim Deploy
|
||
`DEPLOY_FPM_RELOAD=` setzen; dann wartet das Skript, bis FPM im Panel neu geladen ist. Heißt
|
||
der Dienst anders: `DEPLOY_PHP_FPM_SERVICE=…`. **Immer Reload, nie Restart.**
|
||
4. **Cron für den Scheduler:** `* * * * * cd <app> && php artisan schedule:run >> /dev/null 2>&1`.
|
||
Er trägt den Herzschlag für `/health` und `cabinet:reset-overrides`. Eine Queue braucht B2in
|
||
nicht; der Worker-Test im Skript ist aus.
|
||
5. **Server-Panel:** „Quick Deploy“ und Push-Webhooks bleiben aus. Ein Push ist noch kein Release.
|
||
|
||
### `.env`-Werte je Umgebung
|
||
|
||
| Schlüssel | Live | Vorschau |
|
||
|---|---|---|
|
||
| `SENTRY_LARAVEL_DSN`, `SENTRY_BROWSER_DSN` | wie lokal (Projekte server-b2in / browser-b2in), **erst nach der Datenschutzerklärung (Abschnitt 6)** | gleich, optional |
|
||
| `SENTRY_ENVIRONMENT` | `production` | `preview` |
|
||
| `MONITORING_NTFY_URL`, `_TOPIC`, `_TOKEN` | wie lokal | leer (keine Push-Alarme aus der Vorschau) |
|
||
| `MONITORING_ALERT_EMAIL` | Ops-Postfach | leer |
|
||
| `MONITORING_HEALTH_TOKEN` | **eigener Wert** (`openssl rand -hex 24`), nicht der lokale | eigener Wert oder leer |
|
||
| `MAIL_MAILER` | wie bisher | `log` (die Vorschau hat Live-Daten) |
|
||
|
||
`APP_VERSION` bleibt leer. Die Version kommt aus der Datei `RELEASE`, die das Skript schreibt.
|
||
|
||
---
|
||
|
||
## 3. Livegang Interior-Relaunch (v2.0.0)
|
||
|
||
**Grundidee:** Auf Live hat der Kunde Displays, Playlists und die Mediathek gepflegt, an der Website
|
||
im CMS aber nichts geändert. Deshalb kopieren wir **nicht** die Vorschau-Datenbank nach Live und
|
||
übertragen Display-Daten einzeln. Stattdessen bleibt die Live-Datenbank die Grundlage, und
|
||
`InteriorReleaseSeeder` bringt deren Website-Inhalte auf den neuen Stand. Displays,
|
||
Display-Mediathek, CMS-Mediathek, Benutzer und hochgeladene Dateien bleiben unberührt. Angepasst
|
||
werden nur:
|
||
|
||
- das Display-Branding, und zwar nur bei Modulen, die noch kein Interior-Logo tragen
|
||
- absolute Medien-URLs, die auf `b2in.test` oder `b2in.eu` zeigen; sie werden host-unabhängig
|
||
|
||
Damit der Kunde genau dieses Ergebnis vorher sieht, läuft derselbe Ablauf zuerst auf der Vorschau
|
||
mit einer frischen Kopie der Live-Daten (Generalprobe).
|
||
|
||
### 3.1 Generalprobe auf der Vorschau
|
||
|
||
```bash
|
||
# Skripte aus dem Tag holen (Vorschau und Live sind noch keine Git-Checkouts)
|
||
git clone -q --depth 1 --branch v2.0.0 forge-b2in:kadmin/b2in.git /tmp/b2in-v2.0.0
|
||
|
||
# 1. Live-Daten und Live-Medien in die Vorschau (Live wird nur gelesen; legt ~/b2in-live-<zeit>.sql.gz an)
|
||
bash /tmp/b2in-v2.0.0/dev/betrieb/vorschau-auffrischen.sh <live> <vorschau>
|
||
|
||
# 2. Vorschau einmalig auf Git umstellen und v2.0.0 ausliefern, mit allen Inhaltsschritten
|
||
cd <vorschau>
|
||
bash /tmp/b2in-v2.0.0/dev/betrieb/deploy.sh v2.0.0 --initial --migrations --seed=InteriorReleaseSeeder
|
||
```
|
||
|
||
`--initial` sichert vorher den Code nach `~/<verzeichnis>-vor-git-<zeit>.tgz`. Dann zeigt es die
|
||
Dateien, die es nur auf dem Server gibt (Reste aus SFTP- und Archiv-Updates). Erst nach einem
|
||
`ja` verschiebt es sie nach `~/<verzeichnis>-vor-git-<zeit>/`. Danach ist `git status` leer.
|
||
|
||
Vorschau prüfen: die Liste unter 8.3 im Entwicklungsplan, dazu im Portal der Vorschau die
|
||
**Displays und Playlists mit Live vergleichen**. Dort müssen dieselben Module, Bilder und Videos
|
||
erscheinen. Neue Generalprobe, etwa nach einem Hotfix-Tag oder weil auf Live weiter gepflegt
|
||
wurde:
|
||
|
||
```bash
|
||
cd <vorschau> && bash dev/betrieb/deploy.sh v2.0.1 --migrations # nur bei neuem Tag
|
||
bash dev/betrieb/vorschau-auffrischen.sh <live> <vorschau> --seed=InteriorReleaseSeeder
|
||
```
|
||
|
||
### 3.2 Livegang nach der Freigabe
|
||
|
||
1. Wartungsfenster mit Marcel abstimmen. Die Displays holen ihre Konfiguration zyklisch, laufen
|
||
also weiter, ein Wechsel im Laden ist aber sichtbar.
|
||
2. **DB-Backup von Live:**
|
||
`mysqldump --single-transaction --no-tablespaces -u … -p b2in_live | gzip > ~/b2in-live-vor-v2.sql.gz`
|
||
3. Live-`.env` ergänzen (Tabelle oben). `SENTRY_*` erst nach Abschnitt 6.
|
||
4. Gleicher Befehl wie auf der Vorschau, nur im Live-Verzeichnis:
|
||
```bash
|
||
cd <live>
|
||
bash /tmp/b2in-v2.0.0/dev/betrieb/deploy.sh v2.0.0 --initial --migrations --seed=InteriorReleaseSeeder
|
||
rm -rf /tmp/b2in-v2.0.0
|
||
```
|
||
5. Prüfen: `php artisan about --only=b2in` zeigt `v2.0.0`. `/health` mit Token liefert
|
||
`"status":"ok"` (der Herzschlag braucht nach dem Deploy bis zu einer Minute). Danach die
|
||
Liste 8.3 im Entwicklungsplan, die vier Displays sichten, `storage/logs` und GlitchTip auf neue
|
||
Fehler prüfen.
|
||
|
||
**Rückweg** (nur Live, falls etwas grundlegend nicht passt): Seite in den Wartungsmodus nehmen,
|
||
`.git` entfernen, die Code-Sicherung `~/<live>-vor-git-<zeit>.tgz` zurückspielen, das DB-Backup
|
||
aus Schritt 2 einspielen und FPM neu laden. Die geparkten Nur-Server-Dateien aus
|
||
`~/<live>-vor-git-<zeit>/` liegen ebenfalls noch vor.
|
||
|
||
---
|
||
|
||
## 4. Folge-Deploys (ab v2.0.0)
|
||
|
||
```bash
|
||
cd <app>
|
||
bash dev/betrieb/deploy.sh vX.Y.Z # ohne Migrationen
|
||
bash dev/betrieb/deploy.sh vX.Y.Z --migrations # mit Migrationen, vorher DB-Backup
|
||
bash dev/betrieb/deploy.sh vX.Y.Z --seed=<Seeder> # Inhaltsschritt laut Release-Notiz
|
||
bash dev/betrieb/deploy.sh <vorheriger Tag> # Rollback
|
||
```
|
||
|
||
Das Skript bricht ab, **bevor** es etwas ändert, wenn:
|
||
|
||
- im Verzeichnis jemand von Hand etwas geändert hat
|
||
- der Tag fehlt
|
||
- ein Build-Manifest fehlt
|
||
- neue Migrationen ohne `--migrations` kommen
|
||
|
||
Auf dem Server wird nichts von Hand geändert, auch Hotfixes laufen über Commit, Tag und Deploy.
|
||
Exit-Codes: `0` fertig · `1` abgebrochen · `2` falscher Aufruf.
|
||
|
||
---
|
||
|
||
## 5. Monitoring
|
||
|
||
Eingebaut nach `dev/git-deploy-monitoring/` (02 und 03), Tests unter `tests/Feature/Monitoring/`.
|
||
|
||
| Ausfall | Wer meldet | Kanal |
|
||
|---|---|---|
|
||
| Exception (Request, Command, Livewire) | Sentry-SDK | GlitchTip `server-b2in` |
|
||
| JavaScript-Fehler (Website und Portal) | `resources/js/error-reporting.js` | GlitchTip `browser-b2in` (Tag `area`: `web`/`portal`) |
|
||
| Toter interner Link, 419, 403/429 für Angemeldete | `ReportErrorSignals` | GlitchTip (Warnung) |
|
||
| Geplanter Task fehlgeschlagen | `onFailure` in `routes/console.php` | Log `monitoring`, Mail, ntfy |
|
||
| Cron steht, DB/Cache weg, Platte voll | `/health` | Uptime Kuma, dann ntfy und Mail |
|
||
| Server, FPM, Nginx oder SSL weg | `/up` nicht erreichbar | Uptime Kuma |
|
||
|
||
**Uptime Kuma** (status.adametz.media):
|
||
|
||
| Monitor | Ziel | Intervall |
|
||
|---|---|---|
|
||
| B2in /health | `https://b2in.eu/health`, Header `{"Authorization": "Bearer <MONITORING_HEALTH_TOKEN von Live>"}`, Status 200–299 | 60 s, 2 Wiederholungen |
|
||
| B2in /up | `https://b2in.eu/up` und `https://portal.b2in.eu/up` | 5 min |
|
||
| Zertifikate | Ablaufwarnung 14 Tage vorher | – |
|
||
|
||
`/health` antwortet auf jeder Domain, ist von der Basic Auth ausgenommen und liefert ohne oder
|
||
mit falschem Token 404.
|
||
|
||
**Abnahme auf Live:**
|
||
|
||
- `php artisan tinker --execute 'App\Support\OperationalAlert::send("Test B2in");'`: Push und Mail kommen an
|
||
- `php artisan tinker --execute 'report(new RuntimeException("Test B2in"));'`: erscheint in `server-b2in`, Umgebung `production`, Release `v2.0.0`
|
||
- Browser-Konsole auf b2in.eu: `setTimeout(() => { throw new Error('Test test@example.com') })` erscheint in `browser-b2in`, die E-Mail als `[email]`
|
||
- Uptime Kuma: Ein Test-Monitor mit falschem Token löst einen Alarm aus, danach löschen
|
||
|
||
**GlitchTip auswerten** (lokal, Lese-Token `GLITCHTIP_API_TOKEN` in der `.env`): Die Befehle stehen
|
||
in `dev/git-deploy-monitoring/02-fehlerprotokoll-glitchtip.md`, Abschnitt 7, mit
|
||
`B=https://errors.adametz.media/api/0`, `ORG=adametz-media`, Projekt `8` (Server) bzw. `7` (Browser).
|
||
|
||
Lokal sind die DSNs gesetzt (`SENTRY_ENVIRONMENT=local`). Fehler beim Entwickeln landen also in
|
||
GlitchTip, aber getrennt nach Umgebung. Die Testsuite meldet nie etwas; `phpunit.xml` leert DSNs
|
||
und ntfy.
|
||
|
||
---
|
||
|
||
## 6. Datenschutzerklärung (vor dem Einschalten der DSNs auf Live)
|
||
|
||
Die Fehlerprotokollierung muss in die Datenschutzerklärung, bevor auf Live `SENTRY_*` gesetzt wird.
|
||
Hier ein Textvorschlag, den Marcel bzw. die Rechtsberatung freigeben muss:
|
||
|
||
> **Fehlerprotokollierung.** Tritt beim Aufruf unserer Website ein technischer Fehler auf,
|
||
> speichern wir dazu die aufgerufene Adresse (ohne Formularinhalte und ohne Zugangs-Token), Browser
|
||
> und Betriebssystem, die Fehlermeldung und, bei angemeldeten Nutzern, deren interne Nutzer-ID.
|
||
> IP-Adressen und Cookies werden nicht übermittelt. Die Daten verarbeiten wir auf einem eigenen
|
||
> Server in Deutschland, um Fehler zu erkennen und zu beheben. Rechtsgrundlage ist unser
|
||
> berechtigtes Interesse am sicheren und fehlerfreien Betrieb (Art. 6 Abs. 1 lit. f DSGVO). Die
|
||
> Einträge werden nach 90 Tagen gelöscht.
|
||
|
||
Vor der Freigabe prüfen: Serverstandort und Aufbewahrungsfrist (`GLITCHTIP_RETENTION_DAYS`).
|
||
Eingetragen wird der Text in `resources/lang/de/b2in_legal.php` und im CMS unter „Rechtliches“.
|