# Deployment > Letzte Aktualisierung: 2026-06-21 > Status: ✅ CD v3 (Auto + Manual) + Owner-Passwort-Persistenz (SeedAudit) > Live-URL: https://nexus.noveria.net ## CD-Philosophie (v3) - **CI läuft automatisch** bei jedem Push → darf nie brechen - **CD auto + manuell**: Automatischer Deploy nach CI-Success auf main; manueller Deploy via `workflow_dispatch` - **Loop-Schutz**: Commits mit `[skip ci]` werden von Auto-Deploys ignoriert - Deploy liest und validiert `VERSION`, mutiert aber weder Git noch Tags - **Rollback** als eigener Workflow, manuell triggerbar - **Database-Backup** als eigener Workflow, manuell triggerbar (optionaler Nightly-Schedule) ## Workflows ### Deploy (`.gitea/workflows/deploy.yaml`) **Trigger**: - **Automatisch**: Nach erfolgreicher CI (`workflow_run` auf `CI - Build & Test`) → Deployt `main` mit dem im Repo gesetzten `VERSION`-Wert - **Manuell**: Via Gitea Actions → `workflow_dispatch` **Loop-Schutz**: - Version-Bump-Commits enthalten `[skip ci]` → Gitea startet keine neue CI - Auto-Deploy prüft zusätzlich `github.event.workflow_run.head_commit.message` auf `[skip ci]` - Beide Mechanismen zusammen verhindern Endlosschleife: CI → Deploy → Bump → CI … **Inputs**: keine. Der manuelle Deploy nutzt denselben Main-Deploy-Pfad wie der Auto-Deploy. **Ablauf**: 1. Job-Level-Guard: Auto-Deploys fuer `[skip ci]`-Commits werden gar nicht gestartet 2. Checkout von `main` 3. `VERSION` lesen und SemVer validieren 4. **Safe Secret Handling**: `.env` wird aus Secret-Umgebungsvariablen in `/tmp/nexus-deploy-env` geschrieben (mode 600), **NICHT** im Workspace 5. Code-Sync zum Host-Deploy-Pfad 6. `docker compose build && up -d --force-recreate` 7. `.env`-Tempfile wird mit `shred` gelöscht 8. Health-Check (Backoff, 6 Versuche) 9. Smoke-Test (`/dashboard`, `/health`, `/api/v1/operations/snapshot` erwartet `401`) 10. Bei Fehler: Reviewer-Handoff-Meldung mit Job-URL ### Backup (`.gitea/workflows/backup.yaml`) **Trigger**: Manuell via Gitea Actions → `workflow_dispatch` (optional: Nightly-Schedule via Cron) **Inputs**: | Input | Typ | Default | Beschreibung | |---|---|---|---| | `keep_on_host` | boolean | false | Backup auch auf Host-Pfad kopieren | | `host_backup_path` | string | `/opt/openclaw/backups` | Host-Zielpfad | **Ablauf**: 1. Backup-ID generieren (Timestamp-basiert) 2. `docker exec nexus-postgres-1 pg_dumpall -U nexus` → gzip 3. Upload als Gitea-Artifact (90 Tage Retention, bereits komprimiert) 4. Optional: Kopie auf Host-Pfad via Docker-Volume-Mount 5. Integritäts-Check: gzip-Test + SQL-Header-Validierung 6. Backup-Summary mit Restore-Befehl **Restore (manuell auf dem Host)**: ```bash # Aus Gitea-Artifact herunterladen oder von Host-Pfad: zcat nexus-backup-YYYY-MM-DDTHHMMSSZ.sql.gz | docker exec -i nexus-postgres-1 psql -U nexus -d postgres # Danach Stack neu starten: cd /opt/openclaw/data/openclaw/workspace/nexus docker compose up -d --wait ``` **Nightly-Schedule aktivieren**: In `backup.yaml` die Zeilen auskommentieren: ```yaml schedule: - cron: '0 3 * * *' # Jede Nacht um 03:00 UTC ``` ### Rollback (`.gitea/workflows/rollback.yaml`) **Trigger**: Manuell via Gitea Actions → `workflow_dispatch` **Inputs**: | Input | Typ | Beschreibung | |---|---|---| | `target_tag` | string | Git-Tag zum Zurückrollen (z.B. `v0.2.49`) | | `confirm` | string | Muss exakt `ROLLBACK` sein (Safety-Gate) | **Ablauf**: 1. Safety-Gate: Bestätigungstext muss `ROLLBACK` sein 2. Checkout des Target-Tags 3. Tag-Validierung (existiert? welcher Commit?) 4. Safe Secret Handling (gleiches Tempfile-Pattern) 5. Code-Sync des alten Stands zum Host 6. `docker compose build --no-cache && up -d --wait --force-recreate` 7. Health-Check + Smoke-Test (`/dashboard`, `/health`, `/api/v1/operations/snapshot` erwartet `401`) 8. Bei Fehler: Reviewer-Handoff mit manueller Rollback-Anleitung **DB-Migration bei Rollback**: Die API führt `MigrateAsync` beim Start aus. Wenn die Migrationen des Rollback-Tags ein Prefix der aktuellen DB sind (Normalfall), läuft EF Core sie als No-Op. Wenn ein Rollback-Tag vor einer destruktiven Migration liegt, ist manuelles DB-Intervention nötig — ein Edge Case, der DevOps signalisiert wird. ## Secrets und Konfiguration ### Owner Password Persistence (2026-06-21, permanent fix) **Root Cause**: Die fruehere Passwort-Injektion ueber Deploy-Runtime schuf einen unnötigen zweiten Pfad neben der DB und machte Passwort-Drift/Re-Seeding-Folgen möglich. **Fix (3 Schichten)**: 1. **SeedAudit-Entity** (DB-Migration `20260621081500_AddSeedAudit`): `EnsureDatabaseAsync` prueft die `SeedAudit`-Tabelle auf Key `owner_created` VOR dem Seeden. Ist dieser Key vorhanden, wird der Owner NIE neu erstellt — selbst wenn die Users-Tabelle komplett geloescht wird. 2. **Single Source of Truth**: Deploy- und Rollback-Workflows injizieren gar kein `OWNER_PASSWORD` mehr. Nach dem ersten Seed ist ausschließlich die DB kanonisch. 3. **admin-reset-password** Endpoint existiert als Recovery-Pfad (braucht `Admin__ResetToken` aus dem `.env`). Bootstrap läuft nur noch über `BOOTSTRAP_OWNER_EMAIL`. **Verifikation (2026-06-21)**: - Login funktioniert nach `docker compose down && up` (kompletter Stack-Neustart) - Login funktioniert nach `docker compose up -d --force-recreate --wait` - Login funktioniert nach `docker compose restart` - SeedAudit-Eintrag `owner_created` blockiert erneutes Seeden bei jedem Startup **Regel gegen Wiederholung**: Kein `OWNER_PASSWORD` mehr in Deploy-Runtime, Host-`.env` oder Secrets pflegen. Passwort-Änderungen laufen nur noch über App/DB-Pfade. ### Secrets in Gitea Folgende Secrets sind in Gitea (Repo → Settings → Actions → Secrets) konfiguriert: | Secret | Verwendung | |---|---| | `ENV_POSTGRES_PASSWORD` | PostgreSQL-Passwort | | `ENV_JWT_KEY` | JWT-Signing-Key (min. 32 Bytes) | | `ENV_OPENCLAW_TOKEN` | OpenClaw Gateway Token | > **Hinweis**: `ENV_OWNER_PASSWORD` bleibt entfernt. `OWNER_PASSWORD` wird auch nicht mehr aus Host-`.env` eingelesen. ### Safe Secret Handling (v3) **Vorher (unsicher)**: Secrets wurden via `${{ secrets.X }}` direkt in eine Datei im Workspace interpoliert, die dann zum Host synct wurde. Das `.env` lag potenziell lesbar im Workspace und auf dem Host-Dateisystem. **Jetzt (sicher)**: 1. Secrets werden als Step-Environment aus Gitea Secrets bezogen und erst dann in `/tmp/nexus-deploy-env` (mode 600) geschrieben 2. Die Temp-Datei wird via `docker run -v` als read-only ins Compose-Environment gemountet 3. Nach Deploy/Rollback wird die Datei mit `shred -u` gelöscht 4. Das `.env` erscheint **nie** im Workspace oder auf dem Host-Deploy-Pfad ## Build-Anleitung (lokal oder in CI) Die folgenden Befehle sind auf dem Build-System auszuführen. Vor dem Build müssen die Secrets in `.env` gesetzt sein. ```bash # 1. Backend veröffentlichen cd backend dotnet publish -c Release -o dist # 2. Frontend bauen (pnpm preferred) cd ../frontend pnpm install pnpm build # └─ Output: frontend/dist/ (statisch auslieferbar) # 3. Docker-Stack starten (wenn compose verwendet wird) cd .. docker compose up -d --build ``` Die Container holen sich ihre Umgebungsvariablen aus der `.env` im Projektstamm. Stelle sicher, dass `.env` existiert und alle `***`-Platzhalter ersetzt sind. ## Deployment-Plan 1. `.env`-Datei auf dem VPS anlegen (alle Secrets generieren/setzen) 2. Backup vor produktiven Infrastrukturarbeiten 3. Docker-Stack auf dem VPS deployen 4. Datenbankmigration läuft automatisch beim Start (via `MigrateAsync`) 5. Nginx Proxy Manager und `nexus.noveria.net` verbinden 6. HTTPS, Header, Cookies und externe Erreichbarkeit validieren ## Abgeschlossene Deployment-Arbeit - [x] Produktions-`.env` mit starken, getrennten Secrets angelegt (2026-06-08) - [x] Datenbankmigration und kompletter Stack per Docker-Compose deployt - [x] Nexus auf dem VPS deployt (Docker Compose) - [x] Nginx mit Let's Encrypt SSL fuer `nexus.noveria.net` konfiguriert - [x] HTTPS, Security-Header (HSTS, X-Content-Type-Options, X-Frame-Options), Cookies validiert - [x] Externe Erreichbarkeit bestaetigt (2026-06-09) - [x] CI/CD entkoppelt — Deploy darf automatisch (v3) oder manuell (2026-06-13) - [x] Automatischer Deploy nach CI-Success auf main mit Loop-Schutz via [skip ci] (2026-06-13) - [x] Safe Secret Handling: Tempfile in /tmp statt Workspace-Datei (2026-06-13) - [x] Rollback-Workflow implementiert mit Safety-Gate (2026-06-13) - [x] Main-Deploys koennen Version-Bump + Git-Tag automatisch setzen; Non-Main-Deploys bleiben read-only (2026-06-13) - [x] Reviewer-Handoff bei Deploy/Rollback-Fehlern (2026-06-13) - [x] Database-Backup-Workflow mit pg_dumpall + Gitea-Artifact (2026-06-13) - [x] Live-Recheck nach Deploy-Stoerung: `/health`, SPA-Root und `GET /api/dashboard/tasks` wieder 200; Bao-Folgetask zur Agent-Progress-Visibility erstellt (2026-06-20) - [x] Agent-Progress-Stand (`2d21885`) manuell als sauberer Commit-Snapshot live ausgerollt, nachdem der normale Gitea-Deploy-Trigger blockierte (2026-06-20) ## Verifizierung ### 2026-06-20 - https://nexus.noveria.net/ → 200 OK, SPA geladen (`