Files
tessera-ctl/docs/anleitung-betrieb.md
T
schalli c5f4adeeed
Tessera CI/CD / Lint & Type Check (push) Successful in 45s
Tessera CI/CD / Tests (push) Successful in 1m0s
Tessera CI/CD / Build & Publish Images (push) Successful in 2m51s
docs(quick-260916-dcz): Betriebshandbuch Kapitel 9 (Changelog-Schritt, Gitea-Release, Seite Was ist neu), Anwender-, Entwicklungs- und CI-Handbuch
- Betrieb Kapitel 9: Vorschritt CHANGELOG.md vor dem Tag, automatischer Gitea-Release samt Verhalten bei fehlendem Abschnitt, Erstfreigabe v1.0.0 in der Vergangenheit, vierter Erkennungsweg "Was ist neu"
- Anwender: Satz zur Versionszeile in "Aufbau der Oberflaeche", neuer Abschnitt "Was ist neu" vor den Stolpersteinen, Inhaltsverzeichnis; Abschnitt "Dashboard" (dyv) unangetastet
- Entwicklung: Regel "Aenderungsliste" unter Konventionen und Fallstricke (Bauzeit-Einbettung, Importdisziplin, Kanalregel, Release-Skript)
- CI-Setup (ASCII): REGISTRY_TOKEN mit repository: write, vier Schritte im Job publish, Release je Tag, API-Basis im Job-Container

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018N9CD3ebPKm1b32bPpBknY
2026-09-16 11:25:10 +02:00

30 KiB
Raw Permalink Blame History

Tessera – Betriebshandbuch

Anleitung für den laufenden Betrieb von Tessera: Erstinstallation, neue Fassungen einspielen, Datenbank-Migrationen, Sicherung/Wiederherstellung und Fehlersuche. Richtet sich an Kolleg:innen, die die Docker-Container auf dem Server betreiben.

Für den Aufbau der CI/CD-Pipeline (Gitea Actions, act_runner) siehe docs/ci-cd-setup.md – dieses Dokument behandelt nur den Betrieb der bereits laufenden Installation, nicht deren automatisierten Build.

Inhaltsverzeichnis

  1. Überblick der Dienste
  2. Erstinstallation
  3. Konfiguration
  4. Neue Fassung einspielen
  5. Datenbank-Migrationen
  6. Sicherung und Wiederherstellung
  7. Protokolle und Fehlersuche
  8. Abgrenzung zur CI/CD-Pipeline
  9. Zwei Kanäle: Live und Beta

1. Überblick der Dienste

Tessera besteht aus drei Containern, definiert in docker-compose.yml (Basis) und docker-compose.prod.yml (Produktionsvariante mit fertigen Images statt lokalem Build):

Dienst Image / Build Host-Port Zweck
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, Mailhog, OpenLDAP, phpLDAPadmin) für die lokale Entwicklung sowie docker-compose.ci.yml für den Gitea act_runner – beide sind für den Produktivbetrieb ohne Belang und werden hier nicht weiter behandelt.

Netzwerke (siehe docker-compose.yml):

  • frontend-net – nur web, darüber ist der Browser-Zugriff erreichbar (Port 3000).
  • backend-net – web und api, Kommunikation zwischen Frontend-Server und Backend.
  • data-net – api und db, als internal: true markiert. Die Datenbank ist damit aus dem Host-Netzwerk grundsätzlich nicht erreichbar; Zugriff nur über docker compose exec db ... von innen.

Startreihenfolge: db → api → web, erzwungen über depends_on mit condition: service_healthy:

  • db: Healthcheck pg_isready -U tessera (Intervall 5 s, 5 Versuche).
  • api: Healthcheck wget --spider http://localhost:3001/health (Intervall 10 s, 3 Versuche, start_period 10 s in der Basis- / 20 s in der Prod-Variante). Der /health-Endpunkt (apps/api/src/health/health.controller.ts) prüft nur, dass der NestJS-Prozess antwortet – keine Datenbankverbindung. Ein "healthy" API-Container sagt also nichts darüber aus, ob Prisma tatsächlich verbunden ist.
  • web: hat selbst keinen Healthcheck. docker compose ps zeigt web daher nie als "healthy" an, nur als laufend – das ist normal.

Der Browser spricht ausschließlich mit web (Port 3000). Aufrufe unter /api-proxy/* werden von Next.js serverseitig auf API_INTERNAL_URL (http://api:3001 im Docker-Netz) umgeschrieben (siehe apps/web/next.config.ts). Der Browser erreicht die API also nie direkt und muss auch keine eigene CORS-Freigabe für die API-Origin haben – Port 3001 muss von außen in der Regel nicht erreichbar sein, ist es in den mitgelieferten Compose-Dateien aber (Host-Port 3001 ist gemappt).

2. Erstinstallation

Voraussetzung: Docker und Docker Compose (Plugin, docker compose ...) sind auf dem Zielsystem installiert.

  1. Repository auf den Server holen:

    git clone <gitea-url>/schalli/tessera-ctl.git
    cd tessera-ctl
    
  2. .env aus der Vorlage anlegen:

    cp .env.prod.example .env
    

    Die Vorlage .env.prod.example enthält die für den Produktivbetrieb relevanten Variablennamen; .env.example ist die schlankere Entwicklungsvariante. Welche Variablen zu setzen sind, steht in Kapitel 3. Konfiguration. Mindestens erforderlich, bevor irgendetwas startet:

    • DB_PASSWORD und dazu passend DATABASE_URL
    • JWT_SECRET
    • TESSERA_ADMIN_EMAIL, TESSERA_ADMIN_PASSWORD
    • TESSERA_ENCRYPTION_KEY (siehe Warnhinweis unten – ohne diesen Wert startet nichts)
  3. Encryption-Key erzeugen und eintragen:

    openssl rand -hex 32
    

    Das Ergebnis (64 Hex-Zeichen) als TESSERA_ENCRYPTION_KEY= in die .env eintragen. Dieser Schlüssel verschlüsselt alle bei Tessera hinterlegten Zugangsdaten (LDAP-Bind-Passwort, SMTP, Kalender- sowie DKV-/Ausschreibungs-Postfächer). Ohne diesen Wert startet die API nicht – docker-compose.prod.yml verwendet die Compose-Syntax ${TESSERA_ENCRYPTION_KEY:?...}, wodurch bereits docker compose selbst mit einer Fehlermeldung abbricht, bevor ein Container startet, falls die Variable fehlt. Zusätzlich prüft CryptoService (apps/api/src/crypto/crypto.service.ts) beim Hochfahren der API die Länge (muss genau 64 Hex-Zeichen sein) und wirft andernfalls einen Fehler.

    Diesen Schlüssel getrennt von jedem Datenbank-Dump aufbewahren (siehe Kapitel 6) – geht er verloren, sind alle gespeicherten Zugangsdaten unwiderruflich unbrauchbar und müssen von Hand neu eingegeben werden.

  4. Container starten:

    docker compose -f docker-compose.prod.yml pull
    docker compose -f docker-compose.prod.yml up -d
    

    Beim ersten Start seedet die API (AdminSeedService, apps/api/src/user/admin-seed.service.ts) automatisch den initialen Super-Admin-Account aus TESSERA_ADMIN_USER / _EMAIL / _PASSWORD sowie den Mandanten default. Das passiert nur, wenn noch kein Benutzer mit diesem Nutzernamen existiert – bei einer bereits befüllten Datenbank wird der Seed-Schritt stillschweigend übersprungen, auch wenn sich die Passwort-Variable in der .env danach ändert.

  5. Prüfen, ob alles läuft:

    docker compose -f docker-compose.prod.yml ps
    curl -s http://localhost:3001/health
    

    api sollte als healthy markiert sein, die /health-Abfrage liefert {"status":"ok",...}. Danach ist die Oberfläche unter Port 3000 erreichbar.

3. Konfiguration

Alle Variablen werden über .env im Projektverzeichnis eingelesen (Docker Compose liest diese Datei automatisch). Werte unten sind Platzhalter, keine echten Zugangsdaten.

Variable Pflicht in Prod? Default (Prod-Compose) Zweck
DB_PASSWORD ja – Passwort des PostgreSQL-Benutzers tessera, an den db-Container durchgereicht.
DATABASE_URL ja – Vollständiger Prisma-Connection-String, z. B. postgresql://tessera:<passwort>@db:5432/tessera. Muss zum DB_PASSWORD passen.
JWT_SECRET ja – Signiert/prüft die JWT-Auth-Cookies. Wird sowohl an api als auch an web durchgereicht – beide müssen denselben Wert erhalten, sonst schlägt die Anmeldung fehl. In der Basis-Compose (Dev) gibt es einen unsicheren Default (tessera-dev-jwt-secret-change-in-production), in Prod nicht.
TESSERA_ENCRYPTION_KEY ja – AES-256-GCM-Schlüssel (64 Hex-Zeichen) für gespeicherte Drittanbieter-Zugangsdaten. Siehe Kapitel 2. CALENDAR_ENCRYPTION_KEY ist der alte Variablenname und wird weiterhin akzeptiert (mit Warnung im Log).
TESSERA_ADMIN_USER nein admin Benutzername des initialen Super-Admin, nur beim allerersten Start relevant.
TESSERA_ADMIN_EMAIL ja (für den Seed) – E-Mail des initialen Super-Admin.
TESSERA_ADMIN_PASSWORD ja (für den Seed) – Initiales Passwort des Super-Admin.
TESSERA_FORCE_CHANGE nein true Erzwingt Passwortwechsel beim ersten Login des geseedeten Admin-Accounts.
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).
TESSERA_BUGREPORT_TO nein leer Rückfall-Postfach für den Knopf „Fehler melden“ in der Kopfleiste, falls unter Administrator → SMTP kein Feld „Fehlermeldungen an“ gesetzt ist. Leer = nur die Einstellung in der Oberfläche gilt. Wie IMAGE_TAG (Kapitel 9): die Serverdatei /opt/tessera/docker-compose.prod.yml bekommt die Zeile TESSERA_BUGREPORT_TO: ${TESSERA_BUGREPORT_TO:-} nur von Hand.
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 apps/web/Dockerfile, vor pnpm build). Next.js baut NEXT_PUBLIC_*-Variablen zur Build-Zeit in das an den Browser ausgelieferte JavaScript ein; der zur Laufzeit über Compose gesetzte Wert (${APP_URL:-http://localhost:3001}) hat auf das bereits gebaute Frontend-Bundle daher keine sichtbare Wirkung mehr. Praktisch reicht es, dass APP_URL für TESSERA_APP_URL (serverseitig, z. B. Mail-Links) korrekt gesetzt ist.

Wichtig – Konfigurationsdrift auf dem Server: Auf dem Produktivserver wurde die laufende Compose-Datei in der Vergangenheit direkt von Hand angepasst und ist damit nicht mehr zwingend identisch mit docker-compose.prod.yml in diesem Repository. Änderungen an docker-compose*.yml im Git-Repo wirken sich nicht automatisch auf die laufende Installation aus – der Deploy-Schritt zieht ausschließlich neue Images, die Compose-Datei selbst muss bei Bedarf separat auf dem Server aktualisiert werden. Vor grösseren Änderungen an der Compose-Struktur die auf dem Server tatsächlich liegende Datei prüfen, nicht blind von der Repo-Version ausgehen.

4. Neue Fassung einspielen

Das ist der wichtigste Ablauf im Tagesgeschäft. Zwei Befehle:

docker compose -f docker-compose.prod.yml pull
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 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 api und web betrifft das – db soll normalerweise nicht neu erstellt werden, sonst würde sie kurz neu starten).

Kontrolle, ob es wirklich funktioniert hat: Startzeitpunkt des Containers mit dem Erstellungszeitpunkt des zugehörigen Images vergleichen. Der Container muss nach dem Image erzeugt worden sein:

docker inspect -f '{{.State.StartedAt}}' tessera-api-1
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: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.

5. Datenbank-Migrationen

Ein separater Migrationsschritt ist nicht nötig. Der api-Container führt Prisma-Migrationen bei jedem Start automatisch aus, bevor der eigentliche Prozess hochfährt (apps/api/Dockerfile, CMD):

prisma migrate deploy --schema apps/api/prisma/schema.prisma && node apps/api/dist/main.js

Das heisst: Sobald ein neues Image (mit neuen Migrationen im Ordner apps/api/prisma/migrations/) per pull + --force-recreate eingespielt wird, laufen ausstehende Migrationen beim nächsten Start automatisch. Schlägt eine Migration fehl, startet node apps/api/dist/main.js erst gar nicht – der Container bleibt dann im Neustart-Loop bzw. terminiert, sichtbar in docker compose logs api.

Kontrolle, ob eine Migration tatsächlich angekommen ist:

docker compose -f docker-compose.prod.yml exec db \
  psql -U tessera -d tessera -c \
  "SELECT migration_name, finished_at FROM _prisma_migrations ORDER BY finished_at DESC LIMIT 5;"

Der Name der zuletzt erwarteten Migration lässt sich mit dem Ordnernamen in apps/api/prisma/migrations/ abgleichen (Namensschema YYYYMMDDHHMMSS_beschreibung).

6. Sicherung und Wiederherstellung

Es gibt aktuell kein eingebautes Backup-Skript und kein Makefile-Target dafür im Repository. Sicherung erfolgt manuell über die Postgres-Bordmittel.

Datenbank sichern

docker compose -f docker-compose.prod.yml exec db \
  pg_dump -U tessera -d tessera -F c -f /tmp/tessera-$(date +%Y%m%d).dump
docker compose -f docker-compose.prod.yml cp db:/tmp/tessera-$(date +%Y%m%d).dump ./

Datenbank wiederherstellen

docker compose -f docker-compose.prod.yml cp ./tessera-YYYYMMDD.dump db:/tmp/restore.dump
docker compose -f docker-compose.prod.yml exec db \
  pg_restore -U tessera -d tessera --clean --if-exists /tmp/restore.dump

Vor einer Wiederherstellung api stoppen, damit keine Schreibzugriffe während des Restores stattfinden.

Was sonst noch an Zustand existiert

  • PostgreSQL-Daten: liegen im benannten Docker-Volume pgdata (docker-compose.prod.yml, Mount pgdata:/var/lib/postgresql/data im db-Container). pgdata ist eines von zwei dauerhaften Docker-Volumes (siehe nächster Punkt) und kann zusätzlich über ein docker volume-Backup gesichert werden.

  • Verschlüsselungsschlüssel TESSERA_ENCRYPTION_KEY: liegt nur in .env auf dem Host, nicht im Datenbank-Dump. Getrennt sichern (siehe Kapitel 2/3) – ohne ihn sind alle per pg_dump gesicherten verschlüsselten Zugangsdaten wertlos.

  • Hochgeladene Dateien (Avatare unter user-files/avatars/, generierte DKV-Exporte unter user-files/, siehe apps/api/src/user/user.controller.ts und apps/api/src/dkv/dkv-export.service.ts): Diese Dateien liegen im benannten Docker-Volume user-files, gemountet auf /app/user-files im Dienst api. Der Mount ist in docker-compose.yml und docker-compose.prod.yml eingetragen:

    # beim Dienst api:
        - user-files:/app/user-files
    # im Top-Level-Block volumes:
      user-files:
    

    Damit überstehen Avatare und DKV-Exporte ein --force-recreate von api. Wie bei pgdata zeigt docker volume ls das Volume mit vorangestelltem Projektnamen an (<projekt>_user-files). Gesichert werden die Dateien weiterhin mit docker compose cp api:/app/user-files ./user-files-backup, alternativ über eine Sicherung des Docker-Volumes selbst (wie bei pgdata).

    Wichtig für den Betrieb auf einem bestehenden Server: /opt/tessera/docker-compose.yml ist keine Arbeitskopie dieses Repositorys – die Datei wurde dort von Hand bearbeitet und weicht ab. Ein Deploy holt ausschließlich Images und fasst diese Datei nicht an. Diese Compose-Änderung erreicht eine bereits laufende Installation deshalb nicht von selbst. Wer die Reparatur dort haben will, trägt dieselben zwei Zeilen selbst in /opt/tessera/docker-compose.yml ein (vorher sichern) und erstellt die Container einmal neu. Bis dahin gilt für die laufende Installation weiterhin der alte, verlustbehaftete Zustand – prüfbar mit docker inspect tessera-api-1 und einem Blick auf Mounts.

7. Protokolle und Fehlersuche

Logs ansehen:

docker compose -f docker-compose.prod.yml logs -f api
docker compose -f docker-compose.prod.yml logs -f web
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 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
docker compose up bricht sofort ab, ohne dass ein Container startet, mit einer Meldung zu TESSERA_ENCRYPTION_KEY Variable fehlt in .env – Compose selbst verweigert die Variablen-Interpolation (${TESSERA_ENCRYPTION_KEY:?...}) TESSERA_ENCRYPTION_KEY setzen (siehe Kapitel 2), danach erneut starten.
api-Container startet und beendet sich sofort wieder, Log zeigt TESSERA_ENCRYPTION_KEY is not set oder must be a 64-character hex string Key ist zwar irgendwo gesetzt, aber leer oder falsch lang Mit openssl rand -hex 32 neu erzeugen, Länge prüfen (64 Zeichen).
api wird nie healthy, Log zeigt Verbindungsfehler von Prisma/PostgreSQL db ist noch nicht bereit, DATABASE_URL falsch, oder DB_PASSWORD/DATABASE_URL passen nicht zusammen docker compose ps – ist db healthy? DATABASE_URL gegen DB_PASSWORD abgleichen.
Login funktioniert nicht, obwohl api und web laufen JWT_SECRET bei web und api unterschiedlich, z. B. nach halbherzigem Neustart nur eines Containers Beide Container mit demselben JWT_SECRET neu erstellen (--force-recreate api web).
Neue Version scheint nicht anzukommen, obwohl pull gelaufen ist Klassische up -d-Falle ohne --force-recreate (siehe Kapitel 4) StartedAt des Containers gegen Created des Images vergleichen, ggf. --force-recreate nachholen.
Initialer Admin-Login funktioniert nicht nach Änderung von TESSERA_ADMIN_PASSWORD Seed läuft nur, wenn der Benutzername noch nicht existiert; bestehende Accounts werden nicht überschrieben Passwort über die Anwendung selbst (bzw. direkt in der Datenbank) ändern, nicht über die .env-Variable.
Mails werden nicht versendet TESSERA_SMTP_HOST leer (Prod-Default) SMTP-Variablen vollständig setzen und Container neu erstellen.
Fehlermeldungen der Anwender kommen nicht an Kein Postfach gesetzt (weder „Fehlermeldungen an“ unter Administrator → SMTP noch TESSERA_BUGREPORT_TO), oder der SMTP-Versand des Mandanten scheitert Feld „Fehlermeldungen an“ (Administrator → SMTP) oder TESSERA_BUGREPORT_TO prüfen; API-Log nach Bug report durchsuchen (eine Zeile je gesendeter Meldung, Bug report mail failed bei Versandfehler).
Avatare/DKV-Exporte nach einem Deploy verschwunden Die verwendete Compose-Datei mountet user-files/ nicht als Volume – im Repository-Stand seit dieser Version behoben, betrifft nur eine Installation mit abweichender Compose-Datei Die zwei Zeilen aus Kapitel 6 in die verwendete Compose-Datei eintragen (auf dem Server: /opt/tessera/docker-compose.yml, vorher sichern) und api neu erstellen.

8. Abgrenzung zur CI/CD-Pipeline

Dieses Dokument beschreibt den manuellen Betrieb: Erstinstallation, Redeploy, Migrations-Kontrolle, Backup und Fehlersuche auf einer bereits eingerichteten Installation.

Wie die Gitea-Actions-Pipeline (Runner-Registrierung, .gitea/workflows/ci.yml, Berechtigungen des act_runner-Docker-Socket-Mounts) aufgesetzt wird, ist bewusst nicht Teil dieses Dokuments – das steht vollständig in docs/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 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:

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:

    image: git.vicolab.de/schalli/tessera-ctl/web:${IMAGE_TAG:-beta}
    image: git.vicolab.de/schalli/tessera-ctl/api:${IMAGE_TAG:-beta}

Danach wie in Kapitel 4:

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 Zeilen IMAGE_TAG=live und COMPOSE_FILE=docker-compose.prod.yml bereits. Wer eine neue .env aus der Vorlage anlegt, setzt IMAGE_TAG nur noch auf den gewünschten Kanal; auf einer älteren, von Hand gepflegten .env (wie auf alpha) werden die Zeilen einmal ergänzt.

Eine Version freigeben

Das Freigeben erledigt Claude; Sie sagen nur „Version X freigeben“. Zur Einordnung, was dabei passiert:

  1. Änderungsliste abschließen: In CHANGELOG.md wird der Abschnitt „Unveröffentlicht“ in „X.Y.Z – JJJJ-MM-TT“ umbenannt, darüber ein neues, leeres „Unveröffentlicht“ angelegt, und das Ganze auf main committet und gepusht. Erst dann wird zusammengeführt und getaggt:
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.

Beim Tag legt die Pipeline zusätzlich einen Release in Gitea an: Name „Tessera X.Y.Z“, Text ist der Abschnitt dieser Version aus CHANGELOG.md. Sie finden ihn im Repository unter „Releases“. Fehlt der Abschnitt in der Änderungsliste, schlägt genau dieser letzte Schritt fehl – die Abbilder sind dann trotzdem gebaut und abgelegt. Der Release wird nachgeholt, sobald der Abschnitt nachgetragen ist: entweder durch erneutes Auslösen des Tag-Laufs oder lokal per Skript (.gitea/scripts/publish-release.sh --tag vX.Y.Z).

Danach spielen Sie die Version auf dem Live-Server ein – Kapitel 4 gilt unverändert:

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: Die erste Freigabe ist erfolgt. Der Zweig live entstand am 2026-09-14 aus main (git checkout -b live main), bekam den Tag v1.0.0 und wurde zusammen mit dem Tag gepusht; seit 2026-09-15 läuft diese Version auf dem Live-Server. Der Release „Tessera 1.0.0“ in Gitea wurde nachträglich mit dem Skript angelegt, weil die Änderungsliste erst danach eingeführt wurde.

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/<kurzer-name> 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

Vier 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:

    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:

    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).

  4. Was sich geändert hat: Ein Klick auf die Versionszeile unten in der Seitenleiste öffnet die Seite „Was ist neu“ mit der Änderungsliste. Auf Live sehen Sie nur freigegebene Versionen; auf der Beta steht zusätzlich der Abschnitt „Noch nicht freigegeben (Beta)“ mit dem, was seit der letzten Freigabe dazugekommen ist.

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.