12 KiB
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: Automaticher Deploy nach CI-Success auf main (patch default), manueller Deploy mit voller Kontrolle via
workflow_dispatch - Loop-Schutz: Version-Bump-Commits enthalten
[skip ci]— kein Re-Trigger der CI, kein Infinite-Loop - Main-Deploys duerfen VERSION bumpen und einen Git-Tag setzen
- Nicht-Main-Deploys (anderer
git_ref) deployen read-only und mutieren Git nicht - 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_runaufCI - Build & Test) → Default-Parameter: patch bump, all services, main ref - 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.messageauf[skip ci] - Beide Mechanismen zusammen verhindern Endlosschleife: CI → Deploy → Bump → CI …
Inputs (nur bei workflow_dispatch):
| Input | Typ | Default | Beschreibung |
|---|---|---|---|
version_bump |
choice (patch/minor/major) | patch | Version-Bump-Typ |
service |
string | (all) | Einzelner Service oder alle |
no_cache |
boolean | false | Docker-Build-Cache deaktivieren |
git_ref |
string | main | Branch/Tag/Commit zum Deployen |
Ablauf:
- Job-Level-Guard: Auto-Deploys fuer
[skip ci]-Commits werden gar nicht gestartet - Checkout des gewählten Git-Refs
- Wenn
git_ref = main: Version-Bump + Git-Tag + Push - Wenn
git_ref != main: VERSION nur lesen, kein Push, kein Tag - Safe Secret Handling:
.envwird aus Secret-Umgebungsvariablen in/tmp/nexus-deploy-envgeschrieben (mode 600), NICHT im Workspace - Code-Sync zum Host-Deploy-Pfad
docker compose build && up -d --wait --force-recreate.env-Tempfile wird mitshredgelöscht- Health-Check (exponentieller Backoff, 6 Versuche)
- Smoke-Test (
/dashboard,/health,/api/v1/operations/snapshoterwartet401) - 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:
- Backup-ID generieren (Timestamp-basiert)
docker exec nexus-postgres-1 pg_dumpall -U nexus→ gzip- Upload als Gitea-Artifact (90 Tage Retention, bereits komprimiert)
- Optional: Kopie auf Host-Pfad via Docker-Volume-Mount
- Integritäts-Check: gzip-Test + SQL-Header-Validierung
- Backup-Summary mit Restore-Befehl
Restore (manuell auf dem Host):
# 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:
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:
- Safety-Gate: Bestätigungstext muss
ROLLBACKsein - Checkout des Target-Tags
- Tag-Validierung (existiert? welcher Commit?)
- Safe Secret Handling (gleiches Tempfile-Pattern)
- Code-Sync des alten Stands zum Host
docker compose build --no-cache && up -d --wait --force-recreate- Health-Check + Smoke-Test (
/dashboard,/health,/api/v1/operations/snapshoterwartet401) - 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: Dual-Source-Architektur fuer das Owner-Passwort (Gitea-Secret ENV_OWNER_PASSWORD vs Host .env OWNER_PASSWORD) verursachte Drift wenn die DB jemals neu geseedet wurde.
Fix (3 Schichten):
- SeedAudit-Entity (DB-Migration
20260621081500_AddSeedAudit):EnsureDatabaseAsyncprueft dieSeedAudit-Tabelle auf Keyowner_createdVOR dem Seeden. Ist dieser Key vorhanden, wird der Owner NIE neu erstellt — selbst wenn die Users-Tabelle komplett geloescht wird. - Single Source of Truth: Deploy- und Rollback-Workflows lesen
OWNER_PASSWORDjetzt aus dem persistenten Host-.env(viagrepauf dem Deploy-Pfad), NICHT mehr aus separatem Gitea-Secret. Das Host-.envist die kanonische Quelle. - admin-reset-password Endpoint existiert als Recovery-Pfad (braucht
Admin__ResetTokenaus dem.env).
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_createdblockiert erneutes Seeden bei jedem Startup
Regel gegen Wiederholung: OWNER_PASSWORD nur im Host-.env aendern. Das Host-.env wird von CI-Deploys gelesen. Niemals ein separates Gitea-Secret fuer OWNER_PASSWORD anlegen.
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_PASSWORDwurde aus den Gitea-Secrets ENTFERNT (2026-06-21). OWNER_PASSWORD kommt ausschliesslich aus dem Host-.envauf dem Deploy-Pfad.
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):
- Secrets werden als Step-Environment aus Gitea Secrets bezogen und erst dann in
/tmp/nexus-deploy-env(mode 600) geschrieben - Die Temp-Datei wird via
docker run -vals read-only ins Compose-Environment gemountet - Nach Deploy/Rollback wird die Datei mit
shred -ugelöscht - Das
.enverscheint 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.
# 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
.env-Datei auf dem VPS anlegen (alle Secrets generieren/setzen)- Backup vor produktiven Infrastrukturarbeiten
- Docker-Stack auf dem VPS deployen
- Datenbankmigration läuft automatisch beim Start (via
MigrateAsync) - Nginx Proxy Manager und
nexus.noveria.netverbinden - HTTPS, Header, Cookies und externe Erreichbarkeit validieren
Abgeschlossene Deployment-Arbeit
- Produktions-
.envmit starken, getrennten Secrets angelegt (2026-06-08) - Datenbankmigration und kompletter Stack per Docker-Compose deployt
- Nexus auf dem VPS deployt (Docker Compose)
- Nginx mit Let's Encrypt SSL fuer
nexus.noveria.netkonfiguriert - HTTPS, Security-Header (HSTS, X-Content-Type-Options, X-Frame-Options), Cookies validiert
- Externe Erreichbarkeit bestaetigt (2026-06-09)
- CI/CD entkoppelt — Deploy darf automatisch (v3) oder manuell (2026-06-13)
- Automatischer Deploy nach CI-Success auf main mit Loop-Schutz via [skip ci] (2026-06-13)
- Safe Secret Handling: Tempfile in /tmp statt Workspace-Datei (2026-06-13)
- Rollback-Workflow implementiert mit Safety-Gate (2026-06-13)
- Main-Deploys koennen Version-Bump + Git-Tag automatisch setzen; Non-Main-Deploys bleiben read-only (2026-06-13)
- Reviewer-Handoff bei Deploy/Rollback-Fehlern (2026-06-13)
- Database-Backup-Workflow mit pg_dumpall + Gitea-Artifact (2026-06-13)
- Live-Recheck nach Deploy-Stoerung:
/health, SPA-Root undGET /api/dashboard/taskswieder 200; Bao-Folgetask zur Agent-Progress-Visibility erstellt (2026-06-20) - 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 (
<title>Nexus | Noveria Operations</title>) - /health → 200 Healthy, PostgreSQL + Runtime healthy
- /api/dashboard/tasks → 200 OK mit
X-Nexus-Api-Key - Follow-up-Task
Restore agent progress visibility in NexusfuerassignedTo=baoerfolgreich angelegt
2026-06-09
- https://nexus.noveria.net → 200 OK, SPA geladen
- /health → Healthy
- /dashboard, /login → SPA-Routing korrekt
- /api/v1/operations/snapshot → 401 Unauthorized (Auth-Schutz aktiv)
- Let's Encrypt TLS-Zertifikat aktiv
- Nginx-Proxy → 127.0.0.1:18880
Incident-Hinweis (2026-06-14)
- Verifizierter Ausfallpfad:
apikonnte wegen DB-Passwort-Mismatch nicht healthy werden; dadurch bliebwebperdepends_on: service_healthyim StatusCreated. - Nach einem isolierten API-Fix startet
webnicht automatisch nach. Sicherer Minimalpfad:docker compose pscurl http://127.0.0.1:18880/health- Falls
health=200, aber/dashboardnoch nicht200undwebaufCreatedsteht:docker compose up -d web - Danach extern
/dashboard,/healthund/api/v1/operations/snapshoterneut prüfen
- Der manuelle Helper
ops/deploy.shverifiziert deshalb jetzt nicht mehr nur/health, sondern auch/dashboardund den Auth-Schutz der Operations-API.
Offene Arbeit
- [!] Gitea-Deploy-Trigger reparieren:
POST /actions/workflows/deploy.yaml/dispatchesliefert aktuellHTTP 500; fuer Commit2d21885war deshalb kein erfolgreicher normaler Deploy-Run belegbar - Docker-Socket-Risiko im CD-Workflow final adressieren (kommt spaeter)
- Docker-Logs und Container-Health-Monitoring einrichten
- Restore-Drill fuer Backup/Recovery einmal realistisch durchspielen und dokumentieren
- Direkt-Pushes auf
mainwaehrend eines Main-Deploys organisatorisch vermeiden oder spaeter technisch haerter absichern
Deploy-Trigger-Actor (2026-06-14)
- Deploy-Trigger werden durch DevOps (nicht Iris) ausgelöst
- Git-Remote origin verwendet DevOps-Token → Gitea zeigt devops als Actor
- Workflow-Dispatch API-Calls mit DevOps-Token authentifizieren