- 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>
15 KiB
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 unterresources/assetsändern. - Migrationen additiv, damit der Rollback einfach der vorherige Tag ist. Kein
migrate:rollbackauf Live. - Kein
route:cache, keinconfig:cache, keinoptimize. Die Routen hängen vom Host ab (RouteServiceProvider::loadDomainAwareRoutes): ein Routen-Cache von der Kommandozeile enthält nur diemain-Routen, Shop/CRM/Portal fallen aus. Außerdem liest Codeenv()direkt (env('APP_KEY')inApp\User,UserRepository,CustomerRepository;BUSINESS_FORCE_EXECUTE), das liefert bei gecachter Confignull. Das alteclean_script_server.shruftoptimizeauf — 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)
-
Bringt der Tag Migrationen mit: DB-Backup.
-
Neue
.env-Werte laut Release-Notiz vorher eintragen. -
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 -
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
- In Ploi → Site → Daemons: Befehl
php8.4 artisan horizon, Verzeichnis/home/ploi/mivita.care, Benutzerploi, 1 Prozess. Prüfen:php artisan horizon:status→running. - In
.env:QUEUE_DRIVER=redis(config/queue.phpliestQUEUE_DRIVER, nichtQUEUE_CONNECTION). Ohne Config-Cache wirkt das sofort für Web-Requests; Horizon neu starten:php artisan horizon:terminate. php artisan monitoring:queue-probe --wait=60→ „Queue-Worker läuft“./horizonals System-Admin (admin ≥ 4) öffnen: Supervisor aktiv, keine Failed Jobs.- 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('…')) → inconfig/horizon.phpunterqueueeintragen, sonst bleibt der Job liegen. Ein Test (HorizonSetupTest) prüft das fürapp/Jobs. - Nie
queue:retry all: alte Jobs tragen den Klassenstand von damals. Erstqueue:failed, dann gezielt. - Push ohne Tags →
deploy.shfindet den Tag nicht. Immer--follow-tags. - Übersetzungen im Admin (
TranslationFileController) schreiben direkt nachresources/langauf Live. Danach brichtdeploy.shmitM 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 brichtdeploy.shwegen??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. .gitignoreenthältIcon(macOS). Beicore.ignorecase=truetrifft das auch Ordnericon/— neue Ordner dieses Namens werden still ignoriert. Prüfen mitgit 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.