- dev/betrieb/deploy.sh: Tag auschecken, composer, Caches leeren (bewusst
kein route:/config:cache wegen hostabhaengiger Routen und env()-Aufrufen),
horizon:terminate, FPM-Reload, Worker-Pruefung am Ende
- dev/betrieb/README.md: Einrichtung Server, Umstellung des Live-Verzeichnisses,
Release, Deploy, Rollback
- config('app.version') aus RELEASE, php artisan about --only=mivita
- monitoring:queue-probe prueft nach dem Deploy, ob Horizon Jobs abholt
- Vorlage dev/git-deploy-monitoring aus Pressekonto
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
261 lines
9.9 KiB
Markdown
261 lines
9.9 KiB
Markdown
# 01 — Git-Deploy per Tag
|
|
|
|
Ausgeliefert wird ein **Git-Tag**, nie ein Branch-Stand. Das Live-Verzeichnis ist
|
|
ein Git-Checkout dieses Tags. Ein Skript (`vorlagen/deploy.sh`) führt den Deploy
|
|
von Hand per SSH aus und bricht ab, **bevor** es etwas verändert, wenn etwas nicht
|
|
stimmt.
|
|
|
|
Erprobt mit: Ploi-verwaltetem Ubuntu-Server, PHP-FPM 8.4, Nginx, Supervisor,
|
|
MySQL, Forgejo als Git-Server. Läuft genauso mit Forge, plain VPS, GitHub/GitLab.
|
|
|
|
---
|
|
|
|
## Grundsätze
|
|
|
|
- **Nur Tags** (`vX.Y.Z`, SemVer): Feature-Release erhöht die mittlere Stelle,
|
|
Hotfix die letzte. Die Tag-Nachricht ist die Release-Notiz (Inhalt, Migrationen,
|
|
neue `.env`-Werte, genauer Deploy-Befehl).
|
|
- **Deploy nur von Hand** per SSH. Push-Webhooks und „Quick Deploy“ im
|
|
Server-Panel bleiben aus — ein Push ist noch kein Release.
|
|
- **Assets lokal bauen und committen** (`public/build` versioniert). Auf dem
|
|
Server gibt es kein Node. Vite benennt Dateien nach Inhalt — ohne
|
|
Frontend-Änderung kein Diff.
|
|
- **Auf dem Server wird nichts von Hand geändert.** Das Skript bricht ab, sobald
|
|
der Checkout vom Tag abweicht. Auch Hotfixes laufen über Commit → Tag → Deploy.
|
|
- **Kein `git clean` auf Live.** `.env`, `storage/`, `vendor/`, `RELEASE`,
|
|
`public/storage` sind ignoriert und bleiben unberührt.
|
|
- **Deploy im selben Verzeichnis** mit `php artisan down` (Sekunden), keine
|
|
Release-Verzeichnisse — `storage/app` bleibt an seinem Ort.
|
|
- **Migrationen sind additiv.** Der alte Code muss mit dem neuen Schema laufen;
|
|
dann ist der Rollback einfach der vorherige Tag. Kein `migrate:rollback` auf Live.
|
|
|
|
---
|
|
|
|
## Voraussetzungen im Repository
|
|
|
|
`.gitignore` muss enthalten bzw. darf **nicht** enthalten:
|
|
|
|
```gitignore
|
|
/RELEASE # schreibt deploy.sh beim Deploy
|
|
/.env
|
|
/vendor
|
|
/node_modules
|
|
/public/hot
|
|
/public/storage
|
|
/storage/*.key
|
|
# NICHT ignorieren: /public/build (wird versioniert)
|
|
```
|
|
|
|
Prüfen, ob ein Muster versehentlich echte Dateien trifft (macOS-Muster wie
|
|
`Icon` treffen bei `core.ignorecase=true` auch `…/icon/`):
|
|
|
|
```bash
|
|
git ls-files --others --ignored --exclude-standard | grep -v -E '^(vendor|node_modules|storage)/' | head -50
|
|
```
|
|
|
|
Version in der App: `vorlagen/laravel/app/Support/Monitoring/Release.php` plus
|
|
|
|
```php
|
|
// config/app.php
|
|
'version' => App\Support\Monitoring\Release::current(env('APP_VERSION')),
|
|
```
|
|
|
|
`deploy.sh` nach `dev/betrieb/deploy.sh` (oder anders, dann `DEPLOY_SCRIPT`
|
|
im Skript anpassen) und die Einstellungen oben setzen:
|
|
|
|
```bash
|
|
DEPLOY_SCRIPT="dev/betrieb/deploy.sh" # Pfad im Repository
|
|
PHP_FPM_SERVICE="php8.4-fpm" # systemd-Name
|
|
ABOUT_SECTION="" # Abschnitt aus AboutCommand::add, leer = RELEASE ausgeben
|
|
QUEUE_PROBE=1 # 0, wenn das Projekt keine Queue hat
|
|
```
|
|
|
|
`QUEUE_PROBE=1` setzt `monitoring:queue-probe` voraus (03).
|
|
|
|
---
|
|
|
|
## A — Einmalig: Server-Zugang zum Git-Server
|
|
|
|
Auf dem Server als App-Benutzer (bei Ploi `ploi`):
|
|
|
|
1. `git --version`
|
|
2. Deploy-Key erzeugen und im Git-Server **nur lesend** am Repository eintragen:
|
|
|
|
```bash
|
|
ssh-keygen -t ed25519 -f ~/.ssh/deploy_meineapp -N "" -C "meineapp-live"
|
|
cat ~/.ssh/deploy_meineapp.pub
|
|
```
|
|
|
|
3. `~/.ssh/config`:
|
|
|
|
```
|
|
Host git.example.com
|
|
Port 222
|
|
User git
|
|
IdentityFile ~/.ssh/deploy_meineapp
|
|
IdentitiesOnly yes
|
|
```
|
|
|
|
4. Host-Key aufnehmen, Fingerprint mit dem im Git-Server angezeigten vergleichen:
|
|
|
|
```bash
|
|
ssh-keyscan -p 222 git.example.com >> ~/.ssh/known_hosts
|
|
ssh-keygen -lf <(ssh-keyscan -p 222 git.example.com 2>/dev/null)
|
|
```
|
|
|
|
5. Test: `git ls-remote ssh://git@git.example.com:222/team/meineapp.git` zeigt die Tags.
|
|
6. **FPM-Reload ohne Passwort testen** (Ploi erlaubt das dem Benutzer `ploi`):
|
|
|
|
```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
|
|
```
|
|
|
|
Exit 0, `master process` mit gleicher PID, `pool www` neu → das Skript lädt
|
|
FPM selbst. Fragt `sudo` nach einem Passwort: beim Deploy
|
|
`DEPLOY_FPM_RELOAD=` setzen, das Skript wartet dann auf Enter, bis FPM im
|
|
Panel neu geladen ist. **Immer Reload, nie Restart** — Restart entfernt kurz
|
|
den Socket, Nginx liefert 502.
|
|
7. **Supervisor und Worker prüfen:** `systemctl is-enabled supervisor` → `enabled`;
|
|
im Panel ein Worker mit `php artisan queue:work <connection> --queue=<queues> --tries=3 --max-time=3600`.
|
|
|
|
## B — Einmalig: Live-Verzeichnis zum Checkout machen
|
|
|
|
### B1 — Neues Projekt
|
|
|
|
```bash
|
|
cd ~ && git clone ssh://git@git.example.com:222/team/meineapp.git meineapp.de
|
|
cd meineapp.de && git checkout --detach v1.0.0
|
|
cp /pfad/zur/.env .env && composer install --no-dev --optimize-autoloader
|
|
echo v1.0.0 > RELEASE && php artisan migrate --force && php artisan config:cache
|
|
```
|
|
|
|
### B2 — Bestehendes Verzeichnis (bisher SFTP o. Ä.) an Ort und Stelle umwandeln
|
|
|
|
Erst ein Tag `vA` erstellen, der **genau** dem Live-Stand entspricht.
|
|
|
|
```bash
|
|
cd ~ && tar czf backup-vor-git.tgz --exclude=./storage --exclude=./node_modules -C ~/meineapp.de .
|
|
cd ~/meineapp.de
|
|
git init -q
|
|
git remote add origin ssh://git@git.example.com:222/team/meineapp.git
|
|
git fetch --tags origin
|
|
git update-ref --no-deref HEAD "$(git rev-parse 'vA^{commit}')"
|
|
git read-tree HEAD # Index = Tag, Dateien bleiben unangetastet
|
|
git status --porcelain # Abweichungen sichten
|
|
```
|
|
|
|
| Zeile | Bedeutung | Vorgehen |
|
|
|---|---|---|
|
|
| ` M datei` | Live weicht vom Tag ab | `git diff -- datei`. Fehlt die Änderung im Repository: erst lokal nachziehen, neuer Tag. Sonst Altstand, wird überschrieben |
|
|
| ` D datei` | fehlt auf Live | meist Entwicklerdatei; der Checkout legt sie an |
|
|
| `?? datei` | nur auf Live | **nicht löschen** — nach `~/vor-git/` verschieben oder in `.gitignore` aufnehmen (neuer Tag) |
|
|
|
|
Bekannte Funde: fehlende `.gitignore` auf Live (dann erscheinen `.env`,
|
|
`vendor/` als `??` — verschwinden nach dem Checkout), per SFTP gelöschte
|
|
Altdateien, Pfade mit Umlauten (Git setzt sie in Anführungszeichen — beim
|
|
Filtern mit `grep` beachten).
|
|
|
|
Wenn nichts Unerklärtes übrig ist:
|
|
|
|
```bash
|
|
git checkout -q -f --detach vA
|
|
git status --porcelain # muss leer sein
|
|
```
|
|
|
|
Beim ersten Deploy existiert `deploy.sh` evtl. noch nicht im Live-Stand:
|
|
|
|
```bash
|
|
git show vB:dev/betrieb/deploy.sh > /tmp/deploy.sh && bash /tmp/deploy.sh vB
|
|
```
|
|
|
|
---
|
|
|
|
## C — Release lokal (jedes Mal)
|
|
|
|
```bash
|
|
# 1. sauber und grün
|
|
git status # nichts offen
|
|
php artisan test --compact --parallel && vendor/bin/phpstan analyse
|
|
|
|
# 2. Assets bauen und committen
|
|
npm ci && npm run build
|
|
git add public/build
|
|
git diff --cached --quiet public/build || git commit -m "build: Assets für vX.Y.Z"
|
|
|
|
# 3. Tag mit Release-Notiz, pushen (Tags mitnehmen!)
|
|
git tag -a vX.Y.Z -m "vX.Y.Z — <Inhalt>
|
|
|
|
Deploy: <keine Migration | N Migrationen>, <neue .env-Werte>
|
|
bash dev/betrieb/deploy.sh vX.Y.Z [--migrations]"
|
|
git push origin main --follow-tags
|
|
```
|
|
|
|
## 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 ~/meineapp.de
|
|
bash dev/betrieb/deploy.sh vX.Y.Z # ohne Migrationen
|
|
bash dev/betrieb/deploy.sh vX.Y.Z --migrations # mit Migrationen
|
|
```
|
|
|
|
Ablauf im Skript:
|
|
|
|
| Schritt | Bei Fehler |
|
|
|---|---|
|
|
| Arbeitsverzeichnis sauber? | Abbruch, nichts verändert |
|
|
| `git fetch --tags`, Tag vorhanden? | Abbruch, nichts verändert |
|
|
| Neue Migrationen? Ohne `--migrations` | Abbruch, nichts verändert |
|
|
| `down` → Checkout → `RELEASE` → `composer install` → `migrate` → Livewire-Assets → Caches → `queue:restart` → FPM-Reload → `up` | Seite bleibt im Wartungsmodus, Skript nennt den Rollback-Befehl |
|
|
| Version ausgeben | — |
|
|
| `monitoring:queue-probe --wait=60` | **Exit 3**: live, aber kein Worker — Supervisor/Worker prüfen |
|
|
|
|
Optionen: `--clear-schedule` (setzt `withoutOverlapping`-Sperren zurück, nur
|
|
wenn sich Sperrdauern geändert haben). Lange Migrationen:
|
|
`DB_LOCK_WAIT_TIMEOUT=300 bash dev/betrieb/deploy.sh vX.Y.Z --migrations`.
|
|
|
|
Exit-Codes: `0` fertig · `1` abgebrochen · `2` falscher Aufruf · `3` kein Worker.
|
|
|
|
Hinweis: Das Skript läuft aus einer Kopie des **vorher** ausgecheckten Stands
|
|
(Bash liest Skripte stückweise, Git ersetzt die Datei beim Checkout). Neue
|
|
Schritte im Skript greifen also erst beim übernächsten Deploy — beim
|
|
einführenden Deploy von Hand nachholen.
|
|
|
|
4. Prüfen: `/health` mit Token zeigt `"version":"vX.Y.Z"`, Uptime Kuma grün,
|
|
GlitchTip ohne neue Einträge, Logs ohne neue Fehler.
|
|
|
|
## E — Rollback
|
|
|
|
```bash
|
|
bash dev/betrieb/deploy.sh <vorheriger Tag>
|
|
```
|
|
|
|
Baut `vendor/` und Caches für den alten Tag neu auf. Nur bei Datenproblemen
|
|
das DB-Backup einspielen.
|
|
|
|
---
|
|
|
|
## Fallstricke
|
|
|
|
- **FPM-Restart statt Reload** → 502 für Sekunden (Nginx: `connect() to unix:…fpm.sock failed`).
|
|
Einzelne `recv() failed (104: Connection reset by peer)` genau zum Deploy-Zeitpunkt
|
|
sind normal (laufende Anfragen beim Reload). Abhilfe wäre
|
|
`process_control_timeout = 10s` im Abschnitt `[global]` von
|
|
`/etc/php/8.4/fpm/php-fpm.conf` (root) — **nie in die Pool-Datei** (`www.conf`),
|
|
dort ist der Schlüssel unbekannt und FPM startet nicht mehr.
|
|
- **Supervisor gestoppt** → `queue:restart` beendet den Worker, niemand startet
|
|
ihn neu. Deshalb die Worker-Prüfung am Ende. Notlösung ohne sudo:
|
|
`nohup php artisan queue:work --tries=3 --max-time=3600 >> storage/logs/worker-manual.log 2>&1 &`
|
|
- **Nie `queue:retry all`** nach Ausfällen: alte Jobs tragen den Stand ihrer
|
|
Klasse von damals und scheitern oder verschicken veraltete Mails. Erst
|
|
`queue:failed` ansehen, gezielt `queue:retry <uuid>`.
|
|
- **Push ohne Tags** → `deploy.sh` findet den Tag nicht. Immer `--follow-tags`.
|
|
- **`RELEASE` nicht von Hand neu erzeugen** auf einem anderen Stand als dem Tag
|
|
(ergibt `vX.Y.Z-3-gabc-dirty`). Das Skript schreibt sie.
|
|
- **Server-Panel-Logs**: Ploi führt für Sites kein Nginx-Zugriffsprotokoll;
|
|
das Fehlerlog `/var/log/nginx/<site>-error.log` ist für `ploi` lesbar.
|