docs(quick-260914-ku1): Betriebshandbuch — Zwei Kanäle Live und Beta, Freigabe, Hotfix ohne Datenbankänderung, neuer Live-Server; ci-cd-setup auf gemessenen Stand
Tessera CI/CD / Lint & Type Check (push) Successful in 48s
Tessera CI/CD / Tests (push) Successful in 52s
Tessera CI/CD / Build & Publish Images (push) Successful in 3m34s

- anleitung-betrieb.md: neues Kapitel 9 (Kanal, IMAGE_TAG je Server, Freigabe,
  Hotfix-Ablauf mit Regel "Keine Datenbankaenderung als Hotfix", drei Kontrollwege,
  Einrichtung des Live-Servers, Erstfreigabe v1.0.0); Inhaltsverzeichnis, Tabelle in
  Kapitel 1, IMAGE_TAG in Kapitel 3, Etiketten in Kapitel 4, Startzeile in Kapitel 7
- ci-cd-setup.md: REGISTRY_TOKEN und Push ueber localhost:3002, Trigger main/live/v*,
  Jobs quality -> test -> publish, Etiketten- und Build-Arg-Tabellen, D-13 ueberholt,
  Tag-/Branch-Schutz-Empfehlung (T-KU1-04), Fehlerbehebung fuer den Stempel

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018N9CD3ebPKm1b32bPpBknY
This commit is contained in:
2026-09-14 15:44:16 +02:00
parent 9731501718
commit ea6aa995b2
2 changed files with 265 additions and 28 deletions
+184 -9
View File
@@ -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/<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
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.
+81 -19
View File
@@ -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