mivita/dev/betrieb/README.md
Kevin fdbb4f3853 bootstrap/cache nicht mehr versioniert
packages.php und services.php erzeugt Laravel selbst; nach composer install
--no-dev unterscheiden sie sich auf Live vom lokalen Stand (Dev-Pakete) und
blockierten deploy.sh. Anleitung: Fallstricke zu Admin-Uebersetzungen, SFTP
und erzeugten Dateien.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 11:55:46 +00:00

267 lines
13 KiB
Markdown

# Betrieb mivita.care — Git-Deploy per Tag
**Stand:** 07.10.2026 · Vorlage und Hintergründe: [`dev/git-deploy-monitoring/`](../git-deploy-monitoring/README.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` (bis zur Umstellung F: `sync`) |
| 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`.
## 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.