- sentry/sentry-laravel 4.28; config/sentry.php gehaertet (keine PII, keine Request-Bodys, before_send ErrorReportScrubber) - Handler: Integration::captureUnhandledException, CommandNotFoundException nicht melden - ReportErrorSignals (global): toter interner Link (nach Pfad wegen Catch-all-Route), 403/429 fuer Angemeldete, 419; gedrosselt - Browser: selbst gehostetes SDK (esbuild-Bundle 11.5.0), Loader ohne Build-Schritt und ohne Regex-Lookbehind, in 7 Layouts mit area-Tag - phpunit.xml: DSNs und ntfy-URL leer, CACHE_DRIVER=array (Tests schrieben bisher in den lokalen Datei-Cache) - dev/glitch-tip/README.md mit Instanz, Org, Projekt-IDs, API-Auswertung Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
299 lines
15 KiB
Markdown
299 lines
15 KiB
Markdown
# Betrieb mivita.care — Git-Deploy per Tag
|
||
|
||
**Stand:** 08.10.2026 · Vorlage und Hintergründe: [`dev/git-deploy-monitoring/`](../git-deploy-monitoring/README.md) ·
|
||
Protokoll der Umstellung mit Aufräum-Liste: [2026-10-08-protokoll-umstellung.md](2026-10-08-protokoll-umstellung.md)
|
||
|
||
Ausgeliefert wird ein **Git-Tag** (`vX.Y.Z`), nie ein Branch-Stand und keine Dateiliste per SFTP.
|
||
Das Live-Verzeichnis ist ein Git-Checkout dieses Tags; [`deploy.sh`](deploy.sh) führt den Deploy
|
||
von Hand per SSH aus und bricht ab, **bevor** es etwas verändert, wenn etwas nicht stimmt.
|
||
|
||
| | Wert |
|
||
|---|---|
|
||
| Server | Ploi, Benutzer `ploi`, PHP-FPM 8.4 (`php8.4-fpm`), Redis lokal |
|
||
| Queue | Horizon als Ploi-Daemon, `QUEUE_DRIVER=redis` (aktiv seit 08.10.2026) |
|
||
| Live-Verzeichnis | `/home/ploi/mivita.care` |
|
||
| Git-Server (Deploy liest hier) | `origin` = `ssh://git@forge.adametz.media:222/kadmin/mivita.git` |
|
||
| Release-Branch | `master` — Tags werden auf `master` gesetzt |
|
||
| Version in der App | Datei `RELEASE` (schreibt `deploy.sh`) → `config('app.version')`, `php artisan about --only=mivita` |
|
||
|
||
## Grundsätze
|
||
|
||
- **Nur Tags**, SemVer: Feature-Release erhöht die mittlere Stelle, Hotfix die letzte. Die
|
||
Tag-Nachricht ist die Release-Notiz (Inhalt, Migrationen, neue `.env`-Werte, Deploy-Befehl, Nacharbeiten wie Seeder).
|
||
- **Auf dem Server wird nichts von Hand geändert.** Kein SFTP-Upload mehr in den Code-Bereich. Das Skript
|
||
bricht ab, sobald der Checkout vom Tag abweicht. Auch Hotfixes laufen über Commit → Tag → Deploy.
|
||
- **Assets sind versioniert** (`public/js`, `public/css`, `public/assets`, `public/vendor`). Auf dem
|
||
Server wird nichts gebaut. Laravel Mix nur lokal, und nur wenn sich Quellen unter `resources/assets` ändern.
|
||
- **Migrationen additiv**, damit der Rollback einfach der vorherige Tag ist. Kein `migrate:rollback` auf Live.
|
||
- **Kein `route:cache`, kein `config:cache`, kein `optimize`.** Die Routen hängen vom Host ab
|
||
(`RouteServiceProvider::loadDomainAwareRoutes`): ein Routen-Cache von der Kommandozeile enthält nur die
|
||
`main`-Routen, Shop/CRM/Portal fallen aus. Außerdem liest Code `env()` direkt (`env('APP_KEY')` in
|
||
`App\User`, `UserRepository`, `CustomerRepository`; `BUSINESS_FORCE_EXECUTE`), das liefert bei
|
||
gecachter Config `null`. Das alte `clean_script_server.sh` ruft `optimize` auf — **nicht verwenden**.
|
||
|
||
---
|
||
|
||
## A — Einmalig: Server-Zugang zum Git-Server
|
||
|
||
Als `ploi` auf dem Server:
|
||
|
||
```bash
|
||
git --version
|
||
ssh-keygen -t ed25519 -f ~/.ssh/deploy_mivita -N "" -C "mivita-live"
|
||
cat ~/.ssh/deploy_mivita.pub # in Forgejo am Repository kadmin/mivita als Deploy-Key, NUR LESEN
|
||
```
|
||
|
||
`~/.ssh/config`:
|
||
|
||
```
|
||
Host forge.adametz.media
|
||
Port 222
|
||
User git
|
||
IdentityFile ~/.ssh/deploy_mivita
|
||
IdentitiesOnly yes
|
||
```
|
||
|
||
```bash
|
||
ssh-keyscan -p 222 forge.adametz.media >> ~/.ssh/known_hosts
|
||
ssh-keygen -lf <(ssh-keyscan -p 222 forge.adametz.media 2>/dev/null) # mit Forgejo vergleichen
|
||
git ls-remote ssh://git@forge.adametz.media:222/kadmin/mivita.git | grep tags # zeigt die Tags
|
||
```
|
||
|
||
**PHP und Composer prüfen** (die `composer.lock` verlangt PHP 8.4):
|
||
|
||
```bash
|
||
php -v && composer --version
|
||
cd /home/ploi/mivita.care && composer check-platform-reqs --no-dev
|
||
```
|
||
|
||
**FPM-Reload ohne Passwort testen** — immer Reload, nie Restart (Restart → kurz 502):
|
||
|
||
```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 # master-PID gleich, pool www neu
|
||
```
|
||
|
||
Fragt `sudo` nach einem Passwort: beim Deploy `DEPLOY_FPM_RELOAD= bash dev/betrieb/deploy.sh …`,
|
||
das Skript wartet dann auf Enter, bis FPM in Ploi neu geladen ist.
|
||
|
||
In Ploi: **„Quick Deploy“ und Deploy-Webhook aus.** Horizon einrichten: Abschnitt F.
|
||
|
||
## B — Einmalig: Live-Verzeichnis an Ort und Stelle zum Checkout machen
|
||
|
||
Umgestellt wird direkt auf **`v1.0.1`** (nicht `v1.0.0`): ab `v1.0.1` sind `storage/fonts`,
|
||
`storage/reports`, `storage/oauth-*.key` und `_static/` nicht mehr versioniert — ein Checkout von
|
||
`v1.0.0` nach `v1.0.1` würde sie auf Live löschen.
|
||
|
||
```bash
|
||
cd /home/ploi
|
||
tar czf backup-vor-git-$(date +%F).tgz --exclude=./storage --exclude=./node_modules --exclude=./vendor -C mivita.care .
|
||
cd mivita.care
|
||
git init -q
|
||
git remote add origin ssh://git@forge.adametz.media:222/kadmin/mivita.git
|
||
git fetch --tags origin
|
||
git config core.fileMode false # Dateirechte (SFTP 644 vs. Repo 755) nicht als Änderung werten
|
||
git update-ref --no-deref HEAD "$(git rev-parse 'v1.0.1^{commit}')"
|
||
git read-tree HEAD # Index = Tag, Dateien bleiben unangetastet
|
||
git show HEAD:.gitignore > .git/info/exclude # Live hatte keine .gitignore; bis zum Checkout
|
||
git status --porcelain | cut -c1-2 | sort | uniq -c
|
||
```
|
||
|
||
| Zeile | Bedeutung | Vorgehen |
|
||
|---|---|---|
|
||
| ` M datei` | Live weicht vom Tag ab | `git diff -- datei`. Ist es ein Live-Hotfix, der im Repository fehlt: erst lokal nachziehen, neuer Tag. Ist Live nur älter: wird beim Checkout aktualisiert |
|
||
| ` D datei` | fehlt auf Live | meist Doku/Tests (`dev/`, `tests/`); der Checkout legt sie an |
|
||
| `?? datei` | nur auf Live | **nicht löschen** — nach `~/vor-git/` verschieben oder (neuer Tag) in `.gitignore` aufnehmen |
|
||
|
||
Erwartete Funde bei mivita:
|
||
|
||
- **`public/vendor/…` als `??`**: Das Repository enthält nur noch die tatsächlich eingebundenen
|
||
Theme-Dateien (390 statt ~1300). Die übrigen (Demos, Tests, ungenutzte Bibliotheken, PHP-Beispielskripte
|
||
von slim-image-cropper) nach `~/vor-git/public-vendor/` verschieben:
|
||
```bash
|
||
mkdir -p ~/vor-git
|
||
git status --porcelain -- public/vendor | grep '^??' | cut -c4- | while read -r p; do
|
||
mkdir -p ~/vor-git/"$(dirname "$p")" && mv "$p" ~/vor-git/"$p"; done
|
||
```
|
||
- Per SFTP gelöschte Altdateien und alte Deploy-Archive (`storage/` ist ignoriert, bleibt unberührt).
|
||
- Pfade mit Umlauten/Leerzeichen setzt Git in Anführungszeichen — beim Filtern beachten.
|
||
|
||
Wenn nichts Unerklärtes übrig ist:
|
||
|
||
```bash
|
||
git checkout -q -f --detach v1.0.1
|
||
: > .git/info/exclude # ab jetzt gilt die .gitignore aus dem Checkout
|
||
echo v1.0.1 > RELEASE
|
||
git status --porcelain # muss leer sein
|
||
composer install --no-dev --optimize-autoloader --no-interaction
|
||
php artisan config:clear && php artisan route:clear && php artisan view:clear
|
||
php artisan horizon:terminate
|
||
echo "" | sudo -S service php8.4-fpm reload
|
||
php artisan about --only=mivita && php artisan monitoring:queue-probe --wait=60
|
||
```
|
||
|
||
Bringt `v1.0.1` gegenüber Live neue Migrationen mit: vorher DB-Backup, nach dem Checkout `php artisan migrate --force`.
|
||
|
||
---
|
||
|
||
## C — Release lokal (jedes Mal)
|
||
|
||
```bash
|
||
# 1. sauber und grün
|
||
git status # nichts offen
|
||
php artisan test --compact # bzw. die betroffenen Tests
|
||
|
||
# 2. nur wenn sich resources/assets geändert hat: Assets bauen und committen
|
||
npm run production && git add public/js public/css public/vendor public/mix-manifest.json
|
||
|
||
# 3. auf master, Tag mit Release-Notiz, pushen (Tags mitnehmen!)
|
||
git checkout master && git merge --ff-only <arbeitsbranch>
|
||
git tag -a vX.Y.Z -m "vX.Y.Z — <Inhalt>
|
||
|
||
Deploy: <keine Migration | N Migrationen>, <neue .env-Werte>, <Seeder/Commands danach>
|
||
bash dev/betrieb/deploy.sh vX.Y.Z [--migrations]"
|
||
git push origin master --follow-tags
|
||
git push nas master --follow-tags # Spiegel auf dem NAS
|
||
```
|
||
|
||
## 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 /home/ploi/mivita.care
|
||
bash dev/betrieb/deploy.sh vX.Y.Z # ohne Migrationen
|
||
bash dev/betrieb/deploy.sh vX.Y.Z --migrations # mit Migrationen
|
||
```
|
||
|
||
4. Nacharbeiten aus der Release-Notiz (Seeder, Snapshot-Neuaufbau, …).
|
||
|
||
| Schritt im Skript | Bei Fehler |
|
||
|---|---|
|
||
| Arbeitsverzeichnis sauber? Tag auf origin? Neue Migrationen ohne `--migrations`? | Abbruch, nichts verändert |
|
||
| `down` → Checkout → `RELEASE` → `composer install --no-dev` → `migrate` → Caches leeren → `horizon:terminate` → FPM-Reload → `up` | Seite bleibt im Wartungsmodus, Skript nennt den Rollback-Befehl |
|
||
| `about --only=mivita` (Version) | — |
|
||
| `monitoring:queue-probe --wait=60` (entfällt bei `QUEUE_WORKER="none"`) | **Exit 3**: live, aber Horizon holt keine Jobs ab — `horizon:status`, Daemon in Ploi prüfen |
|
||
|
||
Exit-Codes: `0` fertig · `1` abgebrochen · `2` falscher Aufruf · `3` kein Worker.
|
||
Option `--clear-schedule` setzt `withoutOverlapping`-Sperren zurück (nur wenn nötig).
|
||
Lange Migrationen: `DB_LOCK_WAIT_TIMEOUT=300 bash dev/betrieb/deploy.sh vX.Y.Z --migrations`.
|
||
|
||
Das Skript läuft aus einer Kopie des **vorher** ausgecheckten Stands — Änderungen an `deploy.sh`
|
||
greifen erst beim übernächsten Deploy.
|
||
|
||
**Prüfen:** `php artisan about --only=mivita` zeigt den Tag; Startseite, ein Berater-Shop, CRM-Login und
|
||
Checkout laden; `storage/logs/laravel-*.log` ohne neue Fehler; `/horizon` ohne neue Failed Jobs.
|
||
|
||
## E — Rollback
|
||
|
||
```bash
|
||
bash dev/betrieb/deploy.sh <vorheriger Tag>
|
||
```
|
||
|
||
Baut `vendor/` für den alten Tag neu auf. Nur bei Datenproblemen das DB-Backup einspielen.
|
||
|
||
## F — Einmalig: Horizon aktivieren (Umstellung von `QUEUE_DRIVER=sync`)
|
||
|
||
Bis Oktober 2026 lief Live mit `QUEUE_DRIVER=sync`: Jobs liefen im Request mit (Stripe-Webhook
|
||
wartete z. B. auf den Rechnungsnummer-Sync und SMTP). Mit Horizon laufen sie im Hintergrund.
|
||
Betroffen: `SyncStripeInvoiceReference`, Fehlermails aus `MyLog` (`MailLog`), `PaymentIncidentAlert`,
|
||
DHL-Jobs (nur bei `DHL_USE_QUEUE`).
|
||
|
||
Voraussetzung: Der Tag enthält die Horizon-Einstellungen (alle Queues inkl. `high-priority`/`dhl-*`,
|
||
`after_commit` an der Redis-Verbindung, Gate für System-Admins, `horizon:snapshot`).
|
||
|
||
```bash
|
||
cd /home/ploi/mivita.care
|
||
redis-cli ping # PONG
|
||
grep -E '^(APP_ENV|QUEUE_DRIVER|REDIS_HOST)=' .env # APP_ENV=production
|
||
```
|
||
|
||
1. In Ploi → Site → **Daemons**: Befehl `php8.4 artisan horizon`, Verzeichnis `/home/ploi/mivita.care`,
|
||
Benutzer `ploi`, 1 Prozess. Prüfen: `php artisan horizon:status` → `running`.
|
||
2. In `.env`: `QUEUE_DRIVER=redis` (`config/queue.php` liest `QUEUE_DRIVER`, nicht `QUEUE_CONNECTION`).
|
||
Ohne Config-Cache wirkt das sofort für Web-Requests; Horizon neu starten: `php artisan horizon:terminate`.
|
||
3. `php artisan monitoring:queue-probe --wait=60` → „Queue-Worker läuft“.
|
||
4. `/horizon` als System-Admin (admin ≥ 4) öffnen: Supervisor aktiv, keine Failed Jobs.
|
||
5. Nach der nächsten Stripe-Zahlung: Rechnungsnummer in den PaymentIntent-Metadaten.
|
||
|
||
Zurück: `QUEUE_DRIVER=sync` in `.env`, in `deploy.sh` `QUEUE_WORKER="none"`. Liegen gebliebene Jobs
|
||
vorher mit `php artisan queue:work redis --stop-when-empty` abarbeiten.
|
||
|
||
## G — Überwachung: Systemstatus und `/health`
|
||
|
||
**Systemstatus** im CRM unter Sysadmin → „Systemstatus“ (`https://my.mivita.care/sysadmin/status`, nur admin ≥ 4):
|
||
Datenbank, Cache, Queue (sync / database / Horizon), fehlgeschlagene Jobs der letzten 24 h,
|
||
Scheduler-Herzschlag, Plattenplatz, Version, zuletzt fehlgeschlagene Jobs.
|
||
|
||
**`/health`** liefert dieselben Prüfungen als JSON für einen externen Monitor, auf jeder Domain
|
||
(z. B. `https://mivita.care/health`). 200 = in Ordnung oder Warnung, 503 = kritisch. Ohne Token 404.
|
||
|
||
Einrichtung auf Live (einmalig):
|
||
|
||
```bash
|
||
cd /home/ploi/mivita.care
|
||
openssl rand -hex 24 # Wert als MONITORING_HEALTH_TOKEN in die .env
|
||
curl -s -H "Authorization: Bearer <token>" https://mivita.care/health
|
||
```
|
||
|
||
Der **Scheduler-Herzschlag** (`monitoring:heartbeat`, minütlich) zeigt, ob der Cron `schedule:run`
|
||
wirklich läuft. Steht er nach dem Deploy auf „Kritisch“, ist in Ploi kein Scheduler-Eintrag
|
||
(`php artisan schedule:run` jede Minute) aktiv.
|
||
|
||
Schwellen in `.env` (optional): `MONITORING_QUEUE_BACKLOG_SECONDS=300`, `MONITORING_DISK_MIN_FREE_PERCENT=10`.
|
||
|
||
**Fehlerprotokoll GlitchTip:** Exceptions, Fehlersignale (toter interner Link, 403/429, 419) und
|
||
Browserfehler — Einrichtung, Werte und Auswertung in [`dev/glitch-tip/README.md`](../glitch-tip/README.md).
|
||
Live-`.env`: `SENTRY_LARAVEL_DSN`, `SENTRY_BROWSER_DSN`, `SENTRY_ENVIRONMENT=production`,
|
||
`MONITORING_NTFY_URL`, `MONITORING_NTFY_TOPIC`, `MONITORING_NTFY_TOKEN`.
|
||
|
||
**Alarm-Mails** (`App\Support\OperationalAlert`): Schlägt eine geplante Aufgabe fehl (Exit-Code ≠ 0,
|
||
z. B. `business:store-optimized`, `user:make_abo_order`) oder ein Queue-Job endgültig, geht eine Mail an
|
||
`MONITORING_ALERT_EMAIL` (Standard: `EXCEPTION_MAIL`). Gleiche Meldungen höchstens einmal pro Stunde
|
||
(`MONITORING_ALERT_THROTTLE_MINUTES`); ins Log `storage/logs/monitoring-*.log` kommt jede. Optional
|
||
zusätzlich Push über ntfy (`MONITORING_NTFY_URL`, `_TOPIC`, `_TOKEN`). Test auf Live:
|
||
|
||
```bash
|
||
php artisan tinker --execute 'App\Support\OperationalAlert::send("Test-Alarm");'
|
||
```
|
||
|
||
Sperrdauern von `withoutOverlapping` sind kurz (10–60 Min.) statt 24 h: ein abgebrochener Lauf
|
||
blockiert die Aufgabe sonst einen ganzen Tag.
|
||
|
||
## H — Git-Historie bereinigt (08.10.2026)
|
||
|
||
Die Historie wurde mit `git filter-repo` neu geschrieben (7,2 GB → 41 MB, **alle Commit-Hashes und Tags
|
||
neu**). Entfernt: `.env`, `storage/oauth-*.key`, `storage/reports/`, Kundenliste, `KasSLLController.php`
|
||
(SSL-Schlüssel), `_ide_helper*` (eine Version 10 GB), Archive, `_static/`, `dev/app-bak/`.
|
||
|
||
Alte Klone sind unbrauchbar: neu klonen (bzw. `.git` ersetzen). Auf Live wurde `.git` neu aufgesetzt
|
||
(`git init`, `fetch --tags`, `update-ref` auf den Tag aus `RELEASE`, `read-tree`).
|
||
Hashes in älteren Doku-Dateien (`dev/…`) beziehen sich auf die alte Historie.
|
||
|
||
`deploy.sh` läuft nur noch, wenn `php artisan env` `production` meldet (Schutz gegen versehentliches
|
||
Ausführen auf dem Mac; zum Testen `DEPLOY_ALLOW_NON_PRODUCTION=1`).
|
||
|
||
## Fallstricke
|
||
|
||
- **FPM-Restart statt Reload** → 502 für Sekunden.
|
||
- **Horizon nicht neu gestartet** → Worker arbeiten mit altem Code (neue Job-Klassen unbekannt).
|
||
Das Skript ruft `horizon:terminate`; die Worker-Prüfung am Ende fängt einen nicht zurückgekehrten Horizon ab.
|
||
- **Neue Queue** (`onQueue('…')`) → in `config/horizon.php` unter `queue` eintragen, sonst bleibt der Job
|
||
liegen. Ein Test (`HorizonSetupTest`) prüft das für `app/Jobs`.
|
||
- **Nie `queue:retry all`**: alte Jobs tragen den Klassenstand von damals. Erst `queue:failed`, dann gezielt.
|
||
- **Push ohne Tags** → `deploy.sh` findet den Tag nicht. Immer `--follow-tags`.
|
||
- **Übersetzungen im Admin** (`TranslationFileController`) schreiben direkt nach `resources/lang` auf Live.
|
||
Danach bricht `deploy.sh` mit ` M resources/lang/…` ab. Dann die Datei von Live holen, lokal committen,
|
||
neuen Tag deployen (die Live-Änderung ist damit im Tag) — **nicht** auf Live verwerfen.
|
||
- **Nichts per SFTP in versionierte Ordner** (auch keine Bilder unter `public/img/…`): neue Dateien
|
||
gehören in einen Commit, sonst bricht `deploy.sh` wegen `??` ab.
|
||
- **Von Laravel erzeugte Dateien** (`bootstrap/cache/*.php`, `storage/fonts`, …) gehören nie ins
|
||
Repository — sie unterscheiden sich zwischen lokal (mit Dev-Paketen) und Live.
|
||
- **`.gitignore` enthält `Icon`** (macOS). Bei `core.ignorecase=true` trifft das auch Ordner `icon/` —
|
||
neue Ordner dieses Namens werden still ignoriert. Prüfen mit
|
||
`git ls-files --others --ignored --exclude-standard --directory | grep -v -E '^(vendor|node_modules|storage)/'`.
|
||
- **Wartungsmodus** gilt für alle Domains inkl. Webhooks (Stripe, PAYONE). Stripe und PAYONE wiederholen
|
||
Zustellungen; der Wartungsmodus dauert nur Sekunden.
|