# 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 git tag -a vX.Y.Z -m "vX.Y.Z — Deploy: , , 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 ``` 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 " 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.