Zum Inhalt

Update-Anleitung

Anleitung zum Aktualisieren von calServer V2 auf eine neue Version.


In-App-Updates ueber den Updater-Sidecar (empfohlen)

calServer V2 bringt ein integriertes Update-System mit (portiert aus eforms.cloud): Ein separater Updater-Container (calserver-updater) nimmt authentifizierte Update-Auftraege der V2-API entgegen, prueft die Docker-Registry auf neue Versions-Tags, pullt die neuen Images und erstellt ausschliesslich die V2-Container neu (calserver-api-v2, calserver-frontend). Die V1-Container bleiben unberuehrt; V1 wird weiterhin ueber IMAGE_TAG + deploy.sh aktualisiert.

Architektur

Baustein Aufgabe
calserver-updater (Go-Sidecar) Einziger Container mit Docker-Socket-Mount. Endpunkte: /check, /update, /rollback, /status, /changelog. Schreibt V2_IMAGE_TAG in die Host-.env.
Laravel-API (/api/v2/admin/updates/*) Update-Pruefung, Ausloesen von Update/Rollback, Versionslog (update_logs), Status-Polling. Berechtigung: administration_management.
Admin-UI (/admin/updates) Aktuelle Version, Update-Pruefung, Update/Rollback per Klick, Update-Verlauf, Changelog.
CI (ci.yml) Pflegt nur Branch-Pointer (develop-latest fuer develop, release-candidate fuer release/*-Hotfix-Builds). Die unveraenderlichen Semver-Tags v<VERSION> — die Grundlage der Update-Pruefung — und der Rolling-Pointer release-latest kommen ausschliesslich vom Promote-Workflow.

Aktivierung

  1. Token erzeugen und in die Host-.env (im Deploy-Verzeichnis, z. B. /var/calserver/.env) eintragen:

    echo "UPDATER_TOKEN=$(openssl rand -hex 32)" >> .env
    
  2. Registry-Zugang: Die calhelp/*-Images sind privat. Der Weg ist registry.calhelp.de mit dem Lizenzschluessel: Ist unter Administration → Abo & Module ein Schluessel hinterlegt, meldet sich deploy.sh damit an, zieht das API-Image zur Probe und schreibt erst dann IMAGE_REGISTRY=registry.calhelp.de in die .env. Der Updater holt sich Instanz-ID und Schluessel bei jedem Update selbst aus dem API-Container (docker exec calserver-api-v2 php artisan license:registry-credentials); in der Host-.env steht der Schluessel nicht.

Rueckfall bis zur Umstellung aller Anlagen ist Docker Hub mit DOCKERHUB_USERNAME und DOCKERHUB_PULL_TOKEN. Ohne Lizenzschluessel und ohne diese beiden Variablen scheitert die Update-Pruefung mit HTTP 401; deploy.sh warnt dann. IMAGE_REGISTRY=docker.io in der .env haelt eine Anlage ausdruecklich auf Docker Hub.

  1. Abo-Lizenz: Auf Installationen, die gegen einen Lizenzdienst laufen (LICENSE_SERVER_URL + LICENSE_PUBLIC_KEY gesetzt), sperrt ein abgelaufenes Abo die Update-Pruefung und das Anwenden. Diese Seite zeigt den Abo-Zustand nur an; eingetragen und geprueft wird der Schluessel unter Administration → Abo & Module (/admin/subscription) — Details siehe Lizenz. Ohne hinterlegten Lizenzdienst ist die Pruefung inaktiv und aendert nichts am Update-Weg.

  2. Deploy ausfuehren — deploy.sh aktiviert das Compose-Profil v2-updater automatisch, sobald UPDATER_TOKEN gesetzt ist:

    ./deploy.sh
    

Ohne UPDATER_TOKEN bleibt alles beim Alten: Der Updater-Container wird nicht gestartet und die Seite /admin/updates ist schreibgeschuetzt (nur Verlauf und Changelog).

Ablauf eines In-App-Updates

  1. Pruefen: /admin/updates → „Jetzt pruefen" fragt den Updater nach neueren v<x.y.z>-Tags in der Registry (Kanal UPDATE_CHANNEL, Default stable).
  2. Anwenden: „Aktualisieren" schreibt einen Eintrag ins Versionslog (update_logs), der Updater pullt die Images, schreibt V2_IMAGE_TAG in die .env und erstellt die V2-Container neu (docker compose up -d --no-deps). Migrationen laufen beim Container-Start automatisch (Entrypoint).
  3. Bestaetigen: Die UI pollt den Log-Status, bis der neue Tag bestaetigt ist. Nach erfolgreichem Update raeumt der Updater alte, verwaiste V2-Images auf (Rollback-Puffer bleibt erhalten).
  4. Rollback: Jeder abgeschlossene oder fehlgeschlagene Eintrag im Verlauf bietet ein Rollback auf den zuvor laufenden Tag an.

Major-Updates: zweistufige Bestaetigung

Ein Sprung ueber eine Major-Grenze (z. B. 5.64.x → 6.0.0) laeuft nicht als gewoehnliches Update durch. Auf /admin/updates erscheint statt „Aktualisieren“ der Knopf „Major-Update vorbereiten“: zuerst Migrationshinweise und die Releases dazwischen, dann Backup-Bestaetigung plus Eintippen der Zielversion. Erst danach startet das Update.

Der Updater-Sidecar und die V2-API verlangen die Bestaetigung unabhaengig voneinander (409 bzw. 422) — ein System mit aelterem Updater-Image ist also trotzdem geschuetzt. Der Rueckweg ist nie gesperrt: ein Rollback ueber die Major-Grenze bleibt moeglich, stellt aber nur die Container wieder her, nicht das Datenbankschema. Daher die Backup-Pflicht.

ALLOW_MAJOR_UPDATE=false in der Host-.env sperrt Major-Updates flottenweit, auch mit Bestaetigung; true verzichtet auf die Rueckfrage.

Ausserdem sichtbar: der V1-Versatz. Der Updater erneuert nur die V2-Container, V1 folgt weiter IMAGE_TAG + deploy.sh. Die Versionskarte zeigt beide Tags.

Schritt fuer Schritt: Migration auf 6.0.

Beta-Kanal

Releases gehen zuerst ueber den Beta-Kanal. Ein Testsystem setzt dafuer UPDATE_CHANNEL=beta in der Host-.env und sollte einen echten Semver-Tag als V2_IMAGE_TAG fahren (nicht develop-latest), sonst ist die laufende Version nicht bestimmbar und der Versionsvergleich nur ein Anhaltspunkt.

ALLOWED_TAG_PATTERNS auf Bestandssystemen nachziehen

Wer das Muster in der Host-.env selbst gesetzt hat, muss das Pre-Release-Suffix ergaenzen — sonst findet die Pruefung ein Beta-Tag und das Anwenden scheitert mit 403:

ALLOWED_TAG_PATTERNS=^v\\d+\\.\\d+\\.\\d+(-[0-9A-Za-z.-]+)?$|^(develop|release|master|beta)-latest$|^latest$

Release-Prozess: CI „Promote to Prod"

Es gibt keinen master-Branch mehr: develop ist der Integrations- und Deploy-Branch (Dev-Server), release/*-Branches dienen Hotfix-Builds (Pointer release-candidate). Neue Versionen entstehen ausschliesslich ueber den manuellen CI-Workflow Promote to Prod (.github/workflows/promote-to-prod.yml, Actions → „Promote to Prod" → „Run workflow"):

Der Workflow laesst sich nur von develop oder release/* dispatchen (Git-Tag und Release haengen am Branch-Stand). Ohne Input ist die Quelle der neueste unveraenderliche Build-Tag <branch>-<sha7> des dispatchten Branches (z. B. develop-33b1d26) — den vergibt ci.yml allen sieben Deploy-Images im selben Lauf, ein Tag benennt also einen vollstaendigen, fertig gebauten Satz. Deshalb laesst sich jederzeit promoten, auch waehrend develop gerade baut: der promotete Stand ist ein anderer als der gerade entstehende. Die Zielversion kommt dann aus der VERSION-Datei an genau diesem Commit, nicht von der inzwischen weitergewanderten Branch-Spitze (Hintergrund: ADR 2026-08-15).

Die beweglichen Branch-Pointer develop-latest / release-candidate bleiben als Quelle waehlbar — dann greift das CI-Gate (siehe unten).

  1. Validierung + Digest-Pinning: Alle sieben Deploy-Images (calwebapp_bootstrap, report-runner, calserver-api-v2, calserver-frontend, calserver-updater, calserver-mcp, calserver-seal) werden unter dem Quell-Tag einmalig zu Digests aufgeloest; alle weiteren Schritte arbeiten nur noch mit diesen Digests. Ein Re-Verify am Ende prueft, dass sich zwischen erstem und zweitem Aufloesen keiner davon bewegt hat — ein parallel laufender CI-Build kann so keinen Mischstand in die Version taggen. Das CI-Gate greift nur noch bei den beweglichen Pointer-Quellen develop-latest/release-candidate: dann muss auf dem Quell-Branch gerade kein Build laufen und der letzte wirksame ci.yml-Lauf gruen sein. Wirksam heisst: der Lauf hat ueberhaupt Jobs ausgefuehrt. Wartende und von der concurrency-Gruppe verdraengte Laeufe zaehlen nicht — ci.yml nutzt cancel-in-progress: false, GitHub haelt pro Gruppe genau einen wartenden Lauf, und jeder neue Push cancelt den bisher wartenden. Auf einem aktiven develop ist der neueste Lauf deshalb meistens einer, der noch keinen Pointer bewegt hat. Fuer <branch>-<sha7>, release-latest, beta-latest und v<x.y.z> entfaellt das Gate: an einem fertig gebauten Stand bewegt sich nichts mehr. Fehlt ein Quell-Tag, bricht das Promote ab. Das gilt seit ADR 2026-11-09 auch fuer calserver-updater und calserver-seal, die frueher auf :latest zurueckfielen: ein nacktes latest pusht kein Build mehr.
  2. Re-Tag ohne Rebuild: Jedes Image erhaelt per docker buildx imagetools create aus der Digest-Referenz das unveraenderliche Semver-Tag v<VERSION>. Was validiert wurde, geht Byte fuer Byte in die Version. Existiert das Semver-Tag bereits mit anderem Digest, bricht das Promote schon in der Validierung ab — bevor irgendein Tag gesetzt wurde (Semver-Tags sind Rollback-Ziele laufender Installationen).
  3. Rolling-Pointer: Erst wenn alle sieben v-Tags stehen, wird release-latest (bzw. beta-latest bei Pre-Releases) auf dieselben Digests geschoben — der Pointer-Satz zeigt nie auf eine halb getaggte Version.
  4. GitHub Release: haengt am bestehenden Git-Tag <VERSION> (ohne v-Praefix, vom Version-Bump angelegt) und generiert die Release-Notes. 4b. Helm-Release-Chart: Das Chart wird am Release-Commit als Version <VERSION> mit appVersion: v<VERSION> gepackt und ueber deploy-docs.yml unter https://doc.calserver.com/charts veroeffentlicht. Die Image-Tags im Chart sind leer und fallen auf appVersion zurueck; Cluster aktualisieren selbst per helm upgrade --reuse-values <release> calwebapp/calserver --version <VERSION> (Hintergrund: ADR 2026-10-23).
  5. Demo-Deploy: Der Demo-/Release-Server wird auf den frisch promoteten Stand (release-latest) aktualisiert — frueher der master-Build-Job in ci.yml, jetzt Teil des Promotes. Ein Identitaets-Check stellt sicher, dass die V2-Container danach wirklich release-latest fahren (ein in der Host-.env gepinnter V2_IMAGE_TAG macht den Lauf rot statt still zu unterlaufen).
  6. Changelog-Push: Alle Releases werden als JSON-Snapshot per POST /internal/updater/changelog an den Prod-Updater uebergeben und landen als storage/app/changelog.json in der V2-API — die Quelle der Release-Notes auf /admin/updates.
  7. Prod-Trigger (optional): POST /internal/updater/update spielt die Version direkt auf dem Prod-System ein (abschaltbar per Dispatch-Option skip_prod_trigger) — erst nach erfolgreichem Demo-Deploy, dessen Smoke-Test das letzte Gate vor Prod ist. Blockiert das Major-Gate des Updaters (HTTP 409), bleibt der Lauf gruen mit Warnung: das Update wird dann bewusst interaktiv ueber /admin/updates bestaetigt.

Schritte 4+5 benoetigen die Repo-Secrets PROD_APP_URL und PROD_UPDATER_TOKEN sowie die nginx-Route /internal/updater/ (docker/nginx/vhost.d/default_location); ohne Secrets werden sie mit Hinweis uebersprungen. Kundensysteme sehen die neue Version danach ueber die Update-Pruefung auf /admin/updates und spielen sie selbst ein.

nginx-Route auf Bestandsservern nachziehen

Das CI-Deploy synchronisiert nginx/ bewusst nicht auf bestehende Server (host-verwaltete Konfiguration). Auf einem Bestandssystem die aktualisierte nginx/vhost.d/default_location einmalig aus dem Repo nach /var/calserver/nginx/vhost.d/ uebernehmen und den Proxy neu laden (docker exec nginx-proxy nginx -s reload).

Erst nach einem Promote findet die Update-Pruefung (stable-Kanal) eine neue Version — die regulaeren CI-Builds pflegen nur die Branch-Pointer develop-latest/release-candidate.

Relevante Umgebungsvariablen (Host-.env)

Variable Bedeutung Default
UPDATER_TOKEN Shared Secret API ↔ Updater (min. 32 Zeichen) — (aus = deaktiviert)
V2_IMAGE_TAG Aktiver V2-Image-Tag (verwaltet vom Updater) Fallback IMAGE_TAG
UPDATER_IMAGE_TAG Pin fuer das Updater-Image selbst — schreibbar ueber Administration -> Updates release-latest
UPDATER_IMAGE_REPO Registry/Repository des Updater-Images (fuer eigene Spiegelungen) calhelp/calserver-updater
ALLOWED_UPDATER_TAG_PATTERNS Regex-Whitelist fuer die Selbst-Pinnung des Updaters v<x.y.z[-pre]>, Branch-Pointer, release-candidate, updater-<hash>; latest nur noch fuer Bestands-Pins
REPORT_RUNNER_TAG Pin fuer den report-runner (nur Host-.env, kein UI-Knopf) leer = folgt V2_IMAGE_TAG
UPDATE_CHANNEL stable / beta / dev stable
UPDATE_SERVICES Compose-Services, die ein Update neu erstellt calserver-api-v2,calserver-frontend
UPDATER_COMPOSE_FILE Compose-Dateiliste des Updaters (Doppelpunkt-getrennt) — angleichen, wenn ein eigenes SSL-Overlay (z. B. docker-compose.ssl.yml) die V2-Services konfiguriert docker-compose.yml:docker-compose.v2.yml
ALLOWED_TAG_PATTERNS Regex-Whitelist erlaubter Tags v<x.y.z[-pre]>, develop/release/master/beta-latest; latest nur noch fuer Bestands-Pins
ALLOW_MAJOR_UPDATE Major-Updates: leer = Bestaetigung noetig, true = ohne Rueckfrage, false = gesperrt leer
UPDATE_MIGRATION_NOTES_URL Link, den das Major-Gate in der Bestaetigung anbietet Migrationsleitfaden auf doc.calserver.com
CLEANUP_* Image-Aufraeumen (KeepLast, MinAge, MinFree) sinnvolle Defaults

Fehlerbild: „depends on undefined service … invalid compose project"

Schlaegt ein In-App-Update mit einer Meldung wie

compose pull: exit status 15 (…
service "calserver-api-v2" depends on undefined service "report-runner":
invalid compose project)

fehl, sind die Compose-Dateien auf dem Host veraltet: Eine aeltere docker-compose.v2.yml traegt noch ein depends_on: report-runner, waehrend report-runner in der Dateiliste des Updaters nirgends definiert ist (er steht in einer neueren docker-compose.yml oder einem separaten Overlay). Docker Compose lehnt dann das gesamte Projekt ab, bevor ein einziges Image gepullt wird — jedes In-App-Update scheitert.

Aktuelle Updater-Versionen heilen das selbst: Der Updater erkennt genau diesen Fehler, legt ein generiertes Overlay .updater-compose-compat.yml im Deploy-Verzeichnis ab (setzt die betroffenen depends_on-Eintraege per !reset zurueck) und wiederholt den Vorgang. Das ist gefahrlos, weil der Updater pull und up immer auf die V2-Services beschraenkt und mit --no-deps ausfuehrt — depends_on wird dabei nie ausgewertet. deploy.sh laedt das Overlay nicht. Sobald die Host-Dateien aktualisiert sind, raeumt der Updater das Overlay automatisch wieder weg.

Auf einem bereits betroffenen System laeuft noch der alte, gepinnte Updater — einmalig manuell nachziehen (aktualisiert zugleich die Compose-Dateien und behebt die Ursache):

cd /var/calserver   # Deploy-Verzeichnis des Hosts
./deploy.sh

Alternativ, wenn nur der Updater erneuert werden soll und er noch antwortet: Administration -> Updates -> Updater-Container -> Jetzt aktualisieren (siehe Version des Updater-Containers festhalten).

Von Hand auf dem Host: in der .env UPDATER_IMAGE_TAG auf eine aktuelle Version (oder release-latest) setzen, dann docker pull calhelp/calserver-updater:<tag> und den Container einzeln neu erstellen (docker compose -f docker-compose.yml -f docker-compose.v2.yml up -d --no-deps calserver-updater schlaegt auf einem betroffenen Host noch am selben Fehler fehl — dann zuerst das stale depends_on: report-runner aus der lokalen docker-compose.v2.yml entfernen oder direkt deploy.sh nutzen).


Manuelles Update ueber deploy.sh

Der klassische Weg funktioniert unveraendert und ist der Fallback, wenn kein Updater-Sidecar aktiviert ist.

Voraussetzungen

Backup vor jedem Update

Erstellen Sie immer ein vollstaendiges Backup, bevor Sie ein Update durchfuehren. Siehe Backup und Wiederherstellung fuer Details.

  • Zugang zum Server via SSH
  • Docker und Docker Compose installiert
  • Ausreichend Speicherplatz fuer das neue Image

Update-Ablauf

1. Backup erstellen

# Datenbank-Backup
php artisan backup:run

# DMS-Dateien sichern
tar -czf /backup/dms-$(date +%Y%m%d).tar.gz /var/www/storage/app/dms

2. Neue Version herunterladen

cd /opt/calserver-v2
git pull origin main

3. Deploy-Skript ausfuehren

Das deploy.sh-Skript automatisiert den gesamten Update-Prozess:

./deploy.sh

Das Skript fuehrt folgende Schritte aus:

Schritt Beschreibung
Docker-Images bauen Neue Container-Images erstellen
Container neu starten Alte Container stoppen, neue starten
Migrationen ausfuehren Datenbankschema aktualisieren
Cache leeren Alle Caches invalidieren
Queue neu starten Worker-Prozesse neu starten

4. Manuelle Schritte (bei Bedarf)

Falls das Deploy-Skript nicht verwendet wird, koennen die Schritte manuell ausgefuehrt werden:

# Docker-Container neu bauen und starten
docker compose build
docker compose up -d

# Datenbank-Migrationen ausfuehren
docker compose exec app php artisan migrate --force

# Cache leeren
docker compose exec app php artisan cache:clear
docker compose exec app php artisan config:clear
docker compose exec app php artisan route:clear
docker compose exec app php artisan view:clear

# Queue-Worker neu starten
docker compose exec app php artisan queue:restart

5. Nach dem Update: Ist das Schema wirklich durch?

Der Entrypoint des API-Containers fuehrt die Migrationen bei jedem Start aus und laesst den Container auch dann hochkommen, wenn sie scheitern — eine Anlage dunkel zu lassen waere die schlechtere Stoerung. Der Preis: Ein abgebrochener Lauf ist unsichtbar, solange niemand nachfragt. Und abgebrochen heisst nicht „eine fehlt", sondern „alle ab dort fehlen" — migrate haelt bei der ersten gescheiterten Migration an.

Was dann passiert, zeigt sich weit weg von der Ursache: eine Verwaltungsseite, deren Tabelle es nicht gibt, antwortet mit 500; eine Grid-Ansicht laesst sich nicht mehr speichern, weil eine Spalte fehlt. Gesucht wird der Fehler dann in der Seite, nicht in einem Migrationslauf von vor drei Wochen.

deploy.sh prueft das seit 5.64 selbst und meldet ausstehende Migrationen als WARNING: MIGRATIONS ARE PENDING samt Liste. Von Hand:

docker exec calserver-api-v2 php artisan migrate:status --pending=1

Exit-Code 0 heisst „nichts offen". Kommt eine Liste, steht der Grund im Startprotokoll des Containers:

docker logs calserver-api-v2 | grep -A5 'Running migrations'

Derselbe Stand steht in der Oberflaeche unter Administration > Betrieb > Datenbankstand. Nachholen laesst er sich mit:

docker exec calserver-api-v2 php artisan migrate --force

Scheitert der Lauf erneut mit demselben Fehler, ist es eine Datenmigration, die an den Daten dieser Installation haengt (typisch: ein Wert, der gegen einen UNIQUE-Index laeuft). Die Fehlermeldung nennt Tabelle und Spalte — mit dieser Zeile in den Support, statt zu raten.

Token fuer den report-runner (ab report-runner 2.2.0)

deploy.sh erledigt das selbst

Ist REPORT_RUNNER_TOKEN in der Host-.env leer oder nicht vorhanden, erzeugt deploy.sh einen Zufallswert und schreibt ihn hinein — dieselbe Variable gilt fuer Runner und API. Es bleibt nur eine Aufgabe: den erzeugten Wert aus der .env in den Passwort-Manager uebernehmen (Vaultwarden, Item "calServer report-runner Token" je Installation). Der Deploy meldet es in einer Zeile: Generated REPORT_RUNNER_TOKEN and saved to .env.

Ohne Token startet der report-runner nicht

Der Runner kompiliert vom Aufrufer gelieferte Report-Vorlagen und haelt den PDF-Signaturschluessel. Ab 2.2.0 verlangt er deshalb ein geteiltes Geheimnis und verweigert den Start, solange weder Token noch ausdruecklicher Verzicht konfiguriert ist. Hintergrund: ADR-041.

Woran man den Fall erkennt: ein Container, der nicht startet, hat im Compose-Netz keinen DNS-Namen. Die API meldet dann nicht "Token fehlt", sondern cURL error 6: Could not resolve host: report-runner — bei Berichten wie beim Siegel-Abgleich. Die Adresse ist in diesem Fall nicht das Problem; docker logs report-runner --tail 20 nennt den Grund.

Wer den Stack von Hand fahrt (ohne deploy.sh, etwa in Kubernetes), setzt den Wert selbst:

# 1. Token erzeugen und im Passwort-Manager ablegen (Vaultwarden,
#    Item "calServer report-runner Token" je Installation)
openssl rand -hex 24

# 2. In die Host-.env eintragen — derselbe Wert gilt fuer API und Runner
#    REPORT_RUNNER_TOKEN=<erzeugter Wert>

# 3. Dienste neu starten. calserver (V1) gehoert dazu: es rendert Berichte
#    ueber denselben Runner, sobald das Flag use_report_runner gesetzt ist,
#    und liest das Token aus derselben Variable.
docker compose up -d report-runner calserver-api-v2 calserver

# 4. Pruefen, dass der Runner geschuetzt ist
docker compose exec report-runner wget -qO- http://127.0.0.1:8090/ready
#    erwartet: "authentication":"token"

Wer die Umstellung nicht sofort machen kann, setzt REPORT_RUNNER_ALLOW_ANONYMOUS=true fuer den Runner und behaelt den bisherigen Zustand. Das ist keine Loesung, sondern ein Aufschub: solange das gilt, kann jeder, der Port 8090 erreicht, sich ein beliebiges Dokument mit der Identitaet dieser Anlage signieren lassen. In Docker Compose ist der Port auf das Compose-Netz beschraenkt, in Kubernetes erreicht ihn jeder Pod im Namespace.

Version des Updater-Containers festhalten

Der Updater ist der Dienst, dessen Version auf einer Anlage wirklich festgehalten werden muss. Er haelt den Docker-Socket und das Deploy-Verzeichnis schreibend: eine Regression in ihm trifft nicht ein Feature, sondern den Weg, ueber den alle anderen Fehler behoben werden. Und er haengt am wandernden release-latest-Tag — ein docker compose pull auf dem Host hebt ihn also mit, ohne dass jemand ein Update ausgeloest haette.

Dafuer gibt es Administration -> Updates -> Updater-Container:

  • Festhalten auf eine Version (z. B. v6.0.0 oder ein CI-Tag wie updater-a1b2c3d4e5f6). Die Anlage aktualisiert weiter, der Updater bleibt stehen.
  • Freigeben loest die Pinnung, der Updater folgt wieder release-latest.
  • Jetzt aktualisieren zieht das konfigurierte Tag neu und tauscht den Container aus. Ohne Pinnung holt das die neueste release-latest, mit Pinnung eine unter demselben Tag nachgeschobene Korrektur.

Der Rueckweg ist der Punkt: eine Pinnung, die man nur setzen kann, zwingt beim naechsten Mal an die Host-.env — und dorthin kommt nicht jeder, der die Entscheidung treffen darf.

Der Updater tauscht sich selbst aus

Ein compose up aus dem Updater heraus wuerde ihn umbringen: Compose stoppt zuerst den alten Container, und der alte Container ist der Prozess, der Compose gerade ausfuehrt. Deshalb startet der Updater dafuer einen kurzlebigen Helfer-Container (calserver-updater-self-apply) aus dem gerade laufenden Image, der den Tausch von aussen macht und den Tod seines Erzeugers ueberlebt. Der Updater ist dabei einige Sekunden nicht erreichbar; die Seite kann in dieser Zeit „Zustand unbekannt" zeigen.

Der Helfer bleibt nach dem Lauf bewusst stehen, damit seine Logs da sind:

docker logs calserver-updater-self-apply

Reihenfolge: erst ziehen, dann schreiben

Der Sidecar zieht das Image, bevor er die .env anfasst. Ein Tippfehler im Tag laesst die Aktion damit folgenlos scheitern (HTTP 502, nichts veraendert) statt einen Container zu hinterlassen, der sein Image nicht findet — und damit den einzigen Fernwartungsweg der Anlage kappt.

Das normale Update fasst den Updater nicht an

UPDATE_SERVICES enthaelt den Updater nie, auch wenn ihn jemand dort eintraegt. Eine Anlage soll ihren Fernwartungsweg nicht nebenbei tauschen, weil jemand die Anwendung aktualisiert hat. Der Updater wird ausschliesslich ueber diese Karte erneuert (oder ueber deploy.sh).

Braucht den Updater-Sidecar

Der Abschnitt erscheint nur, wenn UPDATER_TOKEN gesetzt ist. Ohne den Sidecar gilt UPDATER_IMAGE_TAG aus der Host-.env, und die Seite ist an dieser Stelle schreibgeschuetzt.

Version des report-runner

Der Runner wandert mit der API: beide haengen am selben Image-Tag, ein Update bewegt sie zusammen. Das ist seit ADR 2026-08-14 so und schliesst die Luecke, aus der der Ausfall vom selben Tag entstand — vorher hing der Runner am V1-Tag und konnte allein weiterziehen, waehrend seine Konfiguration stehen blieb.

Administration -> Updates -> Berichts-Dienst zeigt seinen Zustand: laeuft er, und haengt er an einer festgehaltenen Version? Ein stehender Dienst faellt damit auf, bevor jemand einen Bericht druckt.

Festhalten laesst er sich nur von Hand ueber REPORT_RUNNER_TAG in der Host-.env — das ist ein Eingriff, kein Routinehandgriff mit Knopf. Ein Runner in der falschen Version macht Berichte kaputt; das ist aergerlich, aber reparierbar. Ein Updater in der falschen Version macht die Anlage unerreichbar, und deshalb sitzt der Knopf dort.

Siegel-Dienst calserver-seal (eigener Container)

Reporting und Siegeln sind getrennte Container (ADR 2026-08-06): der neue Dienst calserver-seal haelt die Siegel-Credentials (PKCS#12-Keystores), der report-runner verliert sie in einer spaeteren Version. V2-Deployments starten den Container automatisch mit.

deploy.sh erledigt das Token selbst

Wie beim Runner: ist SEAL_SERVICE_TOKEN in der Host-.env leer, erzeugt deploy.sh einen Zufallswert und schreibt ihn hinein (Generated SEAL_SERVICE_TOKEN and saved to .env). Der Wert gehoert zusaetzlich in den Passwort-Manager (Vaultwarden, Item "calServer Siegel-Dienst Token" je Installation). Es ist bewusst ein eigenes Geheimnis und nicht das Runner-Token: ein Token pro Vertrauensgrenze, unabhaengig rotierbar.

deploy.sh zieht den Siegel-Verkehr um (Phase 2)

Kennt die Host-.env die Zeile SEAL_SERVICE_URL noch gar nicht, setzt deploy.sh sie auf http://calserver-seal:8091 und persistiert das (Set SEAL_SERVICE_URL=... in .env). Ab dann siegelt die API ueber den eigenen Container. Eine vorhandene Zeile — auch eine leere — wird nie ueberschrieben: leer heisst bewusster Rueckfall auf den kombinierten Runner, und genau das ist auch der Rollback-Weg (Zeile leeren, Deploy erneut laufen lassen).

Keystore-Umzug fuer Bestandsanlagen: Hat die Anlage bisher ueber den report-runner gesiegelt, muessen die Credentials dem neuen Container gegeben werden — sonst ist die Credential-Liste nach dem Umzug leer und die Freigabe scheitert laut:

# 1. Keystore-Verzeichnis dem Siegel-Container geben. Default-Mount ist
#    ./pdf-signing (Host, relativ zum docker/-Verzeichnis) — die .p12-Dateien
#    dorthin legen oder SEAL_KEYSTORE_PATH auf das bisherige Verzeichnis
#    zeigen lassen. PDF_SIGNING_PASSWORD* gilt unveraendert.
#    In die Host-.env:
#    SEAL_KEYSTORE_PATH=<Pfad zu den .p12-Dateien>

# 2. Dienste neu erstellen (oder einfach deploy.sh laufen lassen)
docker compose up -d calserver-seal calserver-api-v2

# 3. Pruefen: derselbe Credential-Bestand wie zuvor (Grundeinstellungen →
#    Siegel-Abgleich), und der Dienst ist geschuetzt:
docker compose exec calserver-seal wget -qO- http://127.0.0.1:8091/ready
#    erwartet: "authentication":"token"

Die Registry liest das Keystore-Verzeichnis bei jedem Aufruf — nachgelegte .p12-Dateien wirken ohne Neustart. Rollback in dieser Phase ist trivial: SEAL_SERVICE_URL in der .env leeren, die API faellt auf den Runner zurueck (der seine Siegel-Endpunkte bis Version 3.0.0 behaelt).

Neu: Credentials aus der Oberfläche einrichten

Seit ADR 2026-08-09 legt der Siegel-Dienst Credentials auch selbst an — selbstsigniert, per Zertifikatsantrag an eine CA, als Import einer vorhandenen .p12 oder als Eintrag einer Fernsignatur. Dafuer gilt:

  • Der Keystore-Mount ist nicht mehr :ro. Ein Bestandsdeployment, das die Compose-Datei aus dem Repository uebernimmt, bekommt das automatisch; wer sie angepasst hat, entfernt das :ro selbst oder verzichtet auf die Einrichtung aus der Oberfläche (der Dienst siegelt dann normal weiter).
  • PDF_SIGNING_PASSWORD wird zur Voraussetzung. deploy.sh erzeugt es, wenn die .env die Zeile noch gar nicht kennt, und ueberschreibt eine vorhandene nie — ein neues Passwort ueber bestehende Keystores wuerde sie unlesbar machen. Der Wert gehoert in den Passwort-Manager.
  • Pruefen mit docker compose exec calserver-seal wget -qO- http://127.0.0.1:8091/ready: erwartet wird "provisioning":"enabled".

Der Ablauf im Einzelnen: Siegel einrichten.

Nach dem Update

Pruefung nach dem Update

Rufen Sie den Health-Check-Endpoint auf, um den Systemstatus zu ueberpruefen:

GET /api/v2/health

  • Pruefen Sie die Anwendung im Browser auf korrekte Funktion
  • Kontrollieren Sie die Logs auf Fehlermeldungen:
    docker compose exec app tail -f storage/logs/laravel.log
    

Rollback

Falls das Update fehlschlaegt:

# Datenbank-Backup wiederherstellen
php artisan backup:restore --latest

# Vorherige Container-Version starten
docker compose down
git checkout <vorherige-version>
docker compose up -d

Achtung

Ein Rollback der Datenbank-Migrationen kann zu Datenverlust fuehren. Verwenden Sie im Zweifelsfall das Datenbank-Backup.