f95463ef50
Root cause: Dual-source architecture for owner password (Gitea secret ENV_OWNER_PASSWORD vs host .env OWNER_PASSWORD) caused drift when the DB was ever re-seeded or the volume recreated. Changes: - Add SeedAudit entity + migration to track one-time seed operations - EnsureDatabaseAsync checks SeedAudit BEFORE seeding — owner is never re-created even if the Users table is wiped - Deploy and rollback workflows now read OWNER_PASSWORD from the host's persistent .env (single source of truth) instead of Gitea secrets - compose.yaml documented: OWNER_PASSWORD only used during initial seed - Cleanup: .gitignore extended for core dumps, changelog/deployment.md updated with 2026-06-20 session notes After this fix the DB is the single source of truth for the owner password after initial seed. The host .env is the single reference for the initial value.
222 lines
10 KiB
Markdown
222 lines
10 KiB
Markdown
# Deployment
|
|
|
|
> Letzte Aktualisierung: 2026-06-20
|
|
> Status: ✅ CD v3 (Auto + Manual)
|
|
> 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_run` auf `CI - 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.message` auf `[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**:
|
|
1. Job-Level-Guard: Auto-Deploys fuer `[skip ci]`-Commits werden gar nicht gestartet
|
|
2. Checkout des gewählten Git-Refs
|
|
3. Wenn `git_ref = main`: Version-Bump + Git-Tag + Push
|
|
4. Wenn `git_ref != main`: VERSION nur lesen, kein Push, kein Tag
|
|
5. **Safe Secret Handling**: `.env` wird aus Secret-Umgebungsvariablen in `/tmp/nexus-deploy-env` geschrieben (mode 600), **NICHT** im Workspace
|
|
6. Code-Sync zum Host-Deploy-Pfad
|
|
7. `docker compose build && up -d --wait --force-recreate`
|
|
8. `.env`-Tempfile wird mit `shred` gelöscht
|
|
9. Health-Check (exponentieller Backoff, 6 Versuche)
|
|
10. Smoke-Test (`/dashboard`, `/health`, `/api/v1/operations/snapshot` erwartet `401`)
|
|
11. 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
|
|
|
|
### 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_OWNER_PASSWORD` | Owner-Account-Passwort |
|
|
| `ENV_OPENCLAW_TOKEN` | OpenClaw Gateway Token |
|
|
|
|
### 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 (`<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 Nexus` fuer `assignedTo=bao` erfolgreich 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: `api` konnte wegen DB-Passwort-Mismatch nicht healthy werden; dadurch blieb `web` per `depends_on: service_healthy` im Status `Created`.
|
|
- Nach einem isolierten API-Fix startet `web` nicht automatisch nach. Sicherer Minimalpfad:
|
|
1. `docker compose ps`
|
|
2. `curl http://127.0.0.1:18880/health`
|
|
3. Falls `health=200`, aber `/dashboard` noch nicht `200` und `web` auf `Created` steht: `docker compose up -d web`
|
|
4. Danach extern `/dashboard`, `/health` und `/api/v1/operations/snapshot` erneut prüfen
|
|
- Der manuelle Helper [`ops/deploy.sh`](/home/node/.openclaw/workspace/nexus/ops/deploy.sh) verifiziert deshalb jetzt nicht mehr nur `/health`, sondern auch `/dashboard` und den Auth-Schutz der Operations-API.
|
|
|
|
## Offene Arbeit
|
|
|
|
- [!] Gitea-Deploy-Trigger reparieren: `POST /actions/workflows/deploy.yaml/dispatches` liefert aktuell `HTTP 500`; fuer Commit `2d21885` war 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 `main` waehrend 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
|