diff --git a/docs/anleitung-betrieb.md b/docs/anleitung-betrieb.md index 97308b5..032edb9 100644 --- a/docs/anleitung-betrieb.md +++ b/docs/anleitung-betrieb.md @@ -19,6 +19,7 @@ Betrieb der bereits laufenden Installation, nicht deren automatisierten Build. 6. [Sicherung und Wiederherstellung](#6-sicherung-und-wiederherstellung) 7. [Protokolle und Fehlersuche](#7-protokolle-und-fehlersuche) 8. [Abgrenzung zur CI/CD-Pipeline](#8-abgrenzung-zur-cicd-pipeline) +9. [Zwei Kanäle: Live und Beta](#9-zwei-kanäle-live-und-beta) --- @@ -29,8 +30,8 @@ Tessera besteht aus drei Containern, definiert in `docker-compose.yml` (Basis) u | Dienst | Image / Build | Host-Port | Zweck | |--------|---------------|-----------|-------| -| `web` | `apps/web/Dockerfile` (Basis) bzw. `git.vicolab.de/schalli/tessera-ctl/web:latest` (Prod) | 3000 | Next.js-Frontend (Portal, Dashboard) | -| `api` | `apps/api/Dockerfile` (Basis) bzw. `git.vicolab.de/schalli/tessera-ctl/api:latest` (Prod) | 3001 | NestJS-Backend (REST-API, Prisma/PostgreSQL-Zugriff) | +| `web` | `apps/web/Dockerfile` (Basis) bzw. `git.vicolab.de/schalli/tessera-ctl/web:${IMAGE_TAG}` (Prod; `beta` oder `live`, siehe Kapitel 9) | 3000 | Next.js-Frontend (Portal, Dashboard) | +| `api` | `apps/api/Dockerfile` (Basis) bzw. `git.vicolab.de/schalli/tessera-ctl/api:${IMAGE_TAG}` (Prod; `beta` oder `live`, siehe Kapitel 9) | 3001 | NestJS-Backend (REST-API, Prisma/PostgreSQL-Zugriff) | | `db` | `postgres:16-alpine` | kein Host-Port | PostgreSQL-Datenbank | Zusätzlich existiert `docker-compose.dev.yml` (Bind-Mounts für Live-Reload, @@ -159,6 +160,7 @@ Zugangsdaten. | `TESSERA_SMTP_HOST` / `_PORT` / `_SECURE` / `_USER` / `_PASSWORD` / `_FROM` | nein (aber ohne Host kein Mailversand) | Host leer, Port `587`, `_SECURE=false` | SMTP-Relay für ausgehende Mails (Passwort-Reset, Benachrichtigungen). | | `APP_URL` | empfohlen | `http://localhost:3001` (für `NEXT_PUBLIC_API_URL`) / `http://localhost:3000` (für `TESSERA_APP_URL`) | Öffentliche Basis-URL der Web-Oberfläche. Wird serverseitig u. a. für in E-Mails generierte Links verwendet (`TESSERA_APP_URL`). | | `API_INTERNAL_URL` | fest verdrahtet | `http://api:3001` | Adresse, unter der `web` die API **innerhalb** des Docker-Netzes erreicht; dorthin schreibt Next.js die `/api-proxy/*`-Rewrites um. In der Regel nicht ändern. | +| `IMAGE_TAG` | empfohlen | `beta` | Welcher Kanal auf diesem Server läuft: `beta` (alle Neuerungen, alpha) oder `live` (nur freigegebene Versionen, tessera.ctl.de). Siehe Kapitel 9. | Hinweis zu `NEXT_PUBLIC_API_URL`: Diese Variable wird beim Image-Build bereits fest auf `/api-proxy` gesetzt (`ENV NEXT_PUBLIC_API_URL=/api-proxy` in @@ -191,7 +193,7 @@ docker compose -f docker-compose.prod.yml up -d --force-recreate api web **Verifizierte Falle:** `docker compose up -d` **ohne** `--force-recreate` ersetzt einen bereits laufenden Container **nicht**, wenn Compose der Meinung ist, an der Service-Definition habe sich nichts geändert – auch wenn `pull` gerade ein neues -Image unter demselben Tag (`:latest`) heruntergeladen hat. Der alte Container läuft +Image unter demselben Etikett (`beta` bzw. `live`) heruntergeladen hat. Der alte Container läuft dann unverändert mit dem alten Code weiter, ohne Fehlermeldung. Das Deployment wirkt erfolgreich, ist es aber nicht. Dieser Fehler ist dem Team schon mehrfach passiert. Deshalb: nach jedem `pull` **immer** `--force-recreate` verwenden (nur @@ -204,12 +206,15 @@ Erstellungszeitpunkt des zugehörigen Images vergleichen. Der Container muss ```bash docker inspect -f '{{.State.StartedAt}}' tessera-api-1 -docker image inspect -f '{{.Created}}' git.vicolab.de/schalli/tessera-ctl/api:latest +docker image inspect -f '{{.Created}}' git.vicolab.de/schalli/tessera-ctl/api:beta docker inspect -f '{{.State.StartedAt}}' tessera-web-1 -docker image inspect -f '{{.Created}}' git.vicolab.de/schalli/tessera-ctl/web:latest +docker image inspect -f '{{.Created}}' git.vicolab.de/schalli/tessera-ctl/web:beta ``` +(Auf dem Live-Server statt `:beta` jeweils `:live` einsetzen – das Etikett, das in +der `.env` als `IMAGE_TAG` steht, siehe Kapitel 9.) + (Container-Namen mit `docker compose -f docker-compose.prod.yml ps` prüfen, falls sie auf dem Server abweichen.) Liegt `StartedAt` **vor** `Created` des Images, läuft noch die alte Version – dann `--force-recreate` nachholen. @@ -317,8 +322,10 @@ docker compose -f docker-compose.prod.yml logs -f db **Gesunder Start sieht so aus:** `db` wird `healthy`, danach startet `api` und protokolliert die durchgelaufenen Prisma-Migrationen sowie zuletzt -`Tessera API running on port 3001` (aus `apps/api/src/main.ts`). Erst danach startet -`web`, weil `depends_on: api: condition: service_healthy` das erzwingt. +`Tessera API running on port 3001` und direkt darunter eine Zeile wie +`Tessera API v1.0.0 (live) abc1234` (beides aus `apps/api/src/main.ts`) – Version, +Kanal und Kurzkennung des Standes, der gerade läuft (siehe Kapitel 9). Erst danach +startet `web`, weil `depends_on: api: condition: service_healthy` das erzwingt. | Symptom | Wahrscheinliche Ursache | Prüfen / Beheben | |---|---|---| @@ -342,5 +349,173 @@ Berechtigungen des `act_runner`-Docker-Socket-Mounts) aufgesetzt wird, ist bewus nicht Teil dieses Dokuments – das steht vollständig in [`docs/ci-cd-setup.md`](./ci-cd-setup.md). Die Grenze zwischen beiden Dokumenten: Sobald ein neues Image lokal vorliegt oder in einer Registry verfügbar ist, beginnt -dieses Betriebshandbuch (Kapitel 4); alles davor – wie das Image entsteht – gehört -in das CI/CD-Runbook. +dieses Betriebshandbuch (Kapitel 4 und 9); alles davor – wie das Image entsteht – +gehört in das CI/CD-Runbook. + +## 9. Zwei Kanäle: Live und Beta + +Seit September 2026 gibt es Tessera in zwei Ausgaben, die getrennt voneinander +laufen. Dieses Kapitel erklärt, was das bedeutet, welche Zeile auf welchem Server +stehen muss, wie eine Version freigegeben wird, wie ein dringender Fehler auf Live +behoben wird, und wie Sie jederzeit sehen, welche Fassung gerade läuft. + +### Was ein Kanal ist + +Ein Kanal ist eine Ausgabe von Tessera, die auf einem bestimmten Server läuft und +nach eigenen Regeln neue Stände bekommt. Es gibt zwei: + +- **Beta** – alles Neue, sofort nach jeder Änderung. Läuft unter + `alpha.tessera.ctl.de`. Das Etikett (die Kennzeichnung des Docker-Images in der + Registry) heißt `beta`. Das ältere Etikett `latest` ist nur ein zweiter Name für + genau dasselbe Beta-Image; es bleibt vorerst bestehen, damit nichts kaputtgeht, + und kann später wegfallen. +- **Live** – nur freigegebene Versionen mit einer Nummer. Läuft unter + `tessera.ctl.de` auf dem neuen Server. Das Etikett heißt `live`; zusätzlich trägt + jede freigegebene Version ihre Nummer als eigenes Etikett (`v1.0.0`, `v1.0.1`, …), + damit man jederzeit auch einen älteren Stand gezielt holen kann. + +Die Versionsnummer kommt aus der Freigabe (in Git heißt das „Tag“ – eine Markierung +an einem bestimmten Stand), nicht aus einer Datei im Code. Zwischen zwei Freigaben +zeigt die Beta eine Kennung wie `v1.0.0-12-gabc1234`: das bedeutet „12 Änderungen +nach Version 1.0.0, Stand abc1234“. Vor der allerersten Freigabe steht dort nur die +Kurzkennung des Standes (sieben Zeichen, z. B. `abc1234`). + +### Die eine Zeile je Server + +Welchen Kanal ein Server bekommt, entscheidet **eine einzige Zeile** in der Datei +`/opt/tessera/.env`: + +- auf **alpha** (Beta): `IMAGE_TAG=beta` +- auf dem **neuen Live-Server**: `IMAGE_TAG=live` + +Fehlt die Zeile ganz, nimmt die Compose-Datei von selbst `beta`. Für den Live-Server +ist die Zeile also Pflicht, sonst zieht er die Beta. + +Weil `/opt/tessera` keine Arbeitskopie des Repositorys ist (siehe Kapitel 3, +„Konfigurationsdrift“), muss die Compose-Datei auf dem Server einmal von Hand +angepasst werden. Auf alpha ist das die Datei `/opt/tessera/docker-compose.prod.yml` +(die `.env` dort verweist mit `COMPOSE_FILE` auf sie). Zuerst eine Sicherung: + +```bash +cd /opt/tessera +cp docker-compose.prod.yml docker-compose.prod.yml.bak.$(date +%Y%m%d) +``` + +Dann die zwei `image:`-Zeilen (eine beim Dienst `web`, eine beim Dienst `api`) auf +diese Form bringen – der einzige Unterschied zu heute ist das Ende der Zeile: + +```yaml + image: git.vicolab.de/schalli/tessera-ctl/web:${IMAGE_TAG:-beta} +``` + +```yaml + image: git.vicolab.de/schalli/tessera-ctl/api:${IMAGE_TAG:-beta} +``` + +Danach wie in Kapitel 4: + +```bash +docker compose -f docker-compose.prod.yml pull +docker compose -f docker-compose.prod.yml up -d --force-recreate api web +``` + +Hinweis: Die Vorlage `.env.prod.example` im Repository enthält die Zeile +`IMAGE_TAG` noch nicht. Wer eine neue `.env` aus der Vorlage anlegt, ergänzt die +Zeile von Hand. + +### Eine Version freigeben + +Das Freigeben erledigt Claude; Sie sagen nur „Version X freigeben“. Zur Einordnung, +was dabei passiert: + +```bash +git checkout live +git merge --ff-only main +git tag -a vX.Y.Z -m "Tessera X.Y.Z" +git push origin live vX.Y.Z +``` + +Der zweite Befehl übernimmt den Stand der Beta in den Live-Zweig. Wenn er sich +weigert, ist eine frühere Korrektur (siehe Hotfix, Schritt 5) noch nicht zurück in +`main` – dann wird erst das nachgeholt. Der Push löst die Pipeline zweimal aus: der +Zweig `live` wird nur geprüft, der Tag `vX.Y.Z` wird gebaut und als `live` und +`vX.Y.Z` abgelegt. Das dauert etwa vier bis sechs Minuten. + +Danach spielen Sie die Version auf dem Live-Server ein – Kapitel 4 gilt unverändert: + +```bash +docker compose -f docker-compose.prod.yml pull +docker compose -f docker-compose.prod.yml up -d --force-recreate api web +``` + +**Erstfreigabe v1.0.0:** Den Zweig `live` gibt es noch nicht. Er entsteht beim +ersten Mal aus `main` (`git checkout -b live main`), bekommt den Tag `v1.0.0` und +wird zusammen mit dem Tag gepusht. Das erfolgt, sobald der Knopf „Fehler melden“ +eingebaut ist – nicht in diesem Durchlauf. + +### Einen Fehler auf Live beheben (Hotfix) + +Ein Hotfix ist eine kleine Korrektur, die auf Live landen muss, **ohne** die +Neuerungen der Beta mitzunehmen. Ablauf (Claude führt die Git-Schritte aus, Sie +spielen ein): + +1. Den Live-Stand holen: `git checkout live && git pull`. +2. Einen Korrekturzweig `hotfix/` von `live` anlegen. +3. Die Korrektur machen und die Tests laufen lassen. +4. Nach `live` mergen, die nächste Nummer vergeben (`vX.Y.(Z+1)`, also z. B. + `v1.0.1` nach `v1.0.0`) und beides pushen: `git push origin live vX.Y.(Z+1)`. + Danach spielen Sie die Version auf dem Live-Server ein (Befehle wie oben). +5. Die Korrektur in die Beta übernehmen: `git checkout main && git merge live`. + Vorher wird geprüft, ob die Korrektur dort noch zusammenpasst (Konflikte, Tests), + dann `git push` – die Beta bekommt sie mit dem nächsten Pipeline-Lauf. + +**Keine Datenbankänderung als Hotfix.** Der Grund in Alltagssprache: +Datenbankänderungen (Migrationen) tragen einen Zeitstempel im Namen und werden in +dieser Reihenfolge ausgeführt. Die Beta hat womöglich schon neuere Änderungen +eingespielt. Eine Hotfix-Änderung mit noch späterem Zeitstempel landet beim +Übernehmen in die Beta hinter Änderungen, die sie eigentlich nicht kennt – das ist +der eine Fall, der beim Zusammenführen still kaputtgehen kann. Braucht eine Korrektur +eine Datenbankänderung, wird sie als reguläre Version über `main` freigegeben. + +### Woran Sie erkennen, welche Version läuft + +Drei Wege, vom einfachsten zum genauesten: + +1. **In der Oberfläche:** Unten in der Seitenleiste steht `v1.0.0 · Live` bzw. + `v1.0.0-12-gabc1234 · Beta`. Wenn Sie die Maus darüber halten, erscheinen die + Kurzkennung des Standes und die Version, die der Server meldet. Weichen + Oberfläche und Server voneinander ab, wurde nur einer der beiden Container neu + erstellt – dann Kapitel 4 anwenden (`--force-recreate api web`). +2. **Auf dem Server per Abfrage:** + + ```bash + curl -s http://localhost:3001/health/version + ``` + + Die Antwort enthält die Felder `version`, `channel` (`beta` oder `live`), + `commit` (Kurzkennung) und `buildTime` (wann das Image gebaut wurde). +3. **Im Protokoll:** + + ```bash + docker compose -f docker-compose.prod.yml logs api | grep "Tessera API" + ``` + + Zeigt die Startzeile `Tessera API v1.0.0 (live) abc1234` (siehe Kapitel 7). + +### Den neuen Live-Server einrichten + +Kapitel 2 gilt vollständig. Die Abweichungen gegenüber alpha: + +- In der `.env` steht `IMAGE_TAG=live`. +- Eigene, neu erzeugte Geheimnisse: `JWT_SECRET`, `TESSERA_ENCRYPTION_KEY`, + `DB_PASSWORD` und das Admin-Passwort. Nichts davon von alpha übernehmen. +- Eine eigene, leere Datenbank. Die API legt beim ersten Start den ersten Admin an + (Kapitel 2, Schritt 4). Die alpha-Datenbank wird **nicht** kopiert – es sei denn, + das wird ausdrücklich gewünscht. In diesem Fall gilt Kapitel 6 (Wiederherstellung) + **und** der Live-Server muss denselben `TESSERA_ENCRYPTION_KEY` wie alpha + bekommen, sonst sind alle gespeicherten Zugangsdaten unbrauchbar. +- `APP_URL=https://tessera.ctl.de`. +- Der erste `pull` holt das Etikett `live`. Vor der Erstfreigabe v1.0.0 gibt es + dieses Etikett noch nicht – deshalb erst freigeben, dann installieren. Für einen + Probelauf davor kann vorübergehend `IMAGE_TAG=beta` stehen; danach auf `live` + umstellen und `pull` + `up -d --force-recreate api web` wiederholen. diff --git a/docs/ci-cd-setup.md b/docs/ci-cd-setup.md index c702b96..0dd530e 100644 --- a/docs/ci-cd-setup.md +++ b/docs/ci-cd-setup.md @@ -80,11 +80,24 @@ docker ps --filter name=gitea-runner ### Gitea Secrets (fuer die CI-Pipeline) -In Gitea unter **Repository > Settings > Actions > Secrets** koennen Secrets -fuer die Pipeline konfiguriert werden. Aktuell werden keine Secrets in der -Pipeline benoetigt, da Images lokal gebaut und deployed werden (kein Registry-Push). +In Gitea unter **Repository > Settings > Actions > Secrets** werden die Secrets +fuer die Pipeline konfiguriert. Benoetigt wird genau eines: -Falls kuenftig Deploy-Pfade oder Credentials benoetigt werden: +| Secret | Beschreibung | +|--------|--------------| +| `REGISTRY_TOKEN` | Gitea-Zugangstoken (Access Token) mit Schreibrecht auf Pakete (`package: write`). Wird im Job `publish` fuer `docker login localhost:3002 --password-stdin` verwendet. | + +Das Token erscheint nie im Log: es wird per `--password-stdin` uebergeben und +Gitea maskiert Secret-Werte in der Job-Ausgabe. Das Veroeffentlichungs-Skript +`.gitea/scripts/publish-images.sh` kennt das Token nicht; der Login bleibt im +Workflow. + +Der Push geht ueber `localhost:3002` (Gitea laeuft auf demselben Rechner wie der +Runner), weil der Nginx Proxy Manager vor `git.vicolab.de` grosse Image-Blobs +blockt. Das Pullen auf den Servern laeuft ueber `git.vicolab.de` +(`docker-compose.prod.yml`). + +Weitere Secrets bei Bedarf: 1. In Gitea **Settings > Actions > Secrets** den Secret anlegen 2. In `.gitea/workflows/ci.yml` ueber `${{ secrets.SECRET_NAME }}` referenzieren @@ -94,27 +107,62 @@ hardcoden. ## 4. Pipeline-Ueberblick -Die CI/CD-Pipeline (`.gitea/workflows/ci.yml`) wird bei jedem Push auf `main` -ausgefuehrt und besteht aus drei aufeinander aufbauenden Jobs: +Die CI/CD-Pipeline (`.gitea/workflows/ci.yml`) laeuft bei jedem Push auf die +Zweige `main` und `live` sowie bei jedem Tag `v*` (z. B. `v1.0.0`) und besteht +aus drei aufeinander aufbauenden Jobs: -1. **quality** -- Lint (Biome) und TypeScript Type-Check +1. **quality** -- Lint und TypeScript Type-Check (Lint ist derzeit ein Leerlauf, + siehe WINDOWS #35; der Type-Check ist echt) 2. **test** -- Vitest Unit- und Integrationstests -3. **build-deploy** -- Docker Images bauen und Services neu starten +3. **publish** -- Docker Images mit Versionsstempel bauen und in die Gitea-Registry + veroeffentlichen -Ablauf: `quality` -> `test` -> `build-deploy` (jeder Job nur bei Erfolg des -vorherigen). +Ablauf: `quality` -> `test` -> `publish` (jeder Job nur bei Erfolg des +vorherigen). Der Job `publish` besteht aus drei Schritten: `actions/checkout@v4` +mit `fetch-depth: 0` (volle Historie samt Tags, sonst liefert `git describe` +nichts), Login in die Registry (siehe Abschnitt 3) und der Aufruf von +`.gitea/scripts/publish-images.sh`. -### Kein Registry-Push +### Zwei Kanaele: Etiketten je Anlass -Images werden **lokal auf dem Server gebaut** und nicht in eine Registry -gepusht (D-13). Da der Runner und die Applikation auf demselben Server laufen, -baut die Pipeline die Images direkt mit `docker compose build` und startet die -Services mit `docker compose up -d` neu. +Das Skript `.gitea/scripts/publish-images.sh` entscheidet allein anhand +`GITHUB_REF`, ob und unter welchen Etiketten veroeffentlicht wird: -Vorteile: -- Keine Registry-Infrastruktur noetig -- Schnellerer Deploy (kein Push/Pull ueber Netzwerk) -- Einfachere Konfiguration +| Anlass | Kanal (`APP_CHANNEL`) | Etiketten in der Registry | +|--------|----------------------|---------------------------| +| Push auf `main` | `beta` | `beta` und `latest` (`latest` ist nur ein Alias fuer `beta` und entfaellt spaeter) | +| Tag `vX.Y.Z` | `live` | `live` und `vX.Y.Z` | +| Push auf `live` ohne Tag | -- | keine; der Lauf prueft nur (`quality`, `test`), das Skript endet mit "nichts zu tun" | + +Das Kanalmodell fuer den Betrieb (welcher Server welches Etikett zieht, Freigabe, +Hotfix) steht in `docs/anleitung-betrieb.md`, Kapitel 9. + +### Versionsstempel + +Das Skript berechnet vier Werte und gibt sie als `--build-arg` an beide +Dockerfiles (`apps/web/Dockerfile`, `apps/api/Dockerfile`): + +| Build-Arg | Quelle | +|-----------|--------| +| `APP_VERSION` | `git describe --tags --always` (ohne Tag: kurzer Commit-SHA) | +| `APP_CHANNEL` | `beta` oder `live`, siehe Tabelle oben | +| `APP_COMMIT` | `git rev-parse --short HEAD` | +| `APP_BUILD_TIME` | `date -u`, ISO-Format | + +Die API liest die Werte zur Laufzeit (`GET /health/version`, Startzeile im Log). +Das Web-Image bettet `NEXT_PUBLIC_APP_*` beim Build in das Browser-Bundle ein -- +deshalb laeuft die `builder`-Stufe des Web-Images jetzt bei jedem Pipeline-Lauf +neu, die Laufzeit liegt eher bei 4-6 statt 2 Minuten. + +Lokale Probe ohne Docker-Aufruf: + +```bash +GITHUB_REF=refs/tags/v1.2.3 sh .gitea/scripts/publish-images.sh --print-plan +``` + +Hinweis: Die fruehere Entscheidung D-13 (Images lokal bauen, keine Registry) ist +ueberholt -- seit der Einfuehrung der Gitea-Registry werden Images gepusht und von +den Servern per `docker compose pull` geholt. ## 5. Sicherheitshinweise @@ -146,6 +194,12 @@ Workflow-Dateien in `.gitea/workflows/` werden direkt aus dem Repository geladen. Da nur vertrauenswuerdiger Code gepusht wird (D-11, Claude als einziger Committer), ist das Risiko einer manipulierten Pipeline minimal. +Ein Tag-Push `v*` ist der Freigabe-Hebel fuer Live: wer ihn setzen darf, kann +das `live`-Etikett neu belegen. Heute hat nur das Konto `schalli` Schreibrecht +(0 Kollaborateure, keine Branch-Regeln). Kommen weitere Konten dazu, in Gitea +unter **Repository > Settings > Branches / Tags** eine Tag-Schutzregel fuer `v*` +und einen Branch-Schutz fuer `live` anlegen (T-KU1-04). + ## 6. Fehlerbehebung ### Runner registriert sich nicht @@ -167,3 +221,11 @@ einziger Committer), ist das Risiko einer manipulierten Pipeline minimal. erreichbar ist 2. Docker Daemon Status pruefen: `docker info` 3. Disk Space pruefen: `df -h` + +### Stempel zeigt `dev` oder nur eine Kurzkennung statt des Tags + +1. Im Job `publish` pruefen, dass `actions/checkout@v4` mit `fetch-depth: 0` + auscheckt -- ohne Tags liefert `git describe --tags --always` nur den SHA +2. Pruefen, ob der Tag wirklich gepusht wurde: `git ls-remote --tags origin` +3. `dev` bedeutet: das Image wurde ohne Build-Args gebaut (lokal statt ueber + das Skript) -- das ist fuer lokale Builds normal