mivita/dev/betrieb/README.md
Kevin Adametz 0b02b1b15c GlitchTip: Fehlerprotokoll fuer Server, Fehlersignale und Browser
- 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>
2026-10-08 11:46:36 +00:00

15 KiB
Raw Permalink Blame History

Betrieb mivita.care — Git-Deploy per Tag

Stand: 08.10.2026 · Vorlage und Hintergründe: dev/git-deploy-monitoring/ · Protokoll der Umstellung mit Aufräum-Liste: 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 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:

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
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):

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):

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.

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:
    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:

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)

# 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:

    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 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).

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):

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. 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:

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.