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¶
-
Token erzeugen und in die Host-
.env(im Deploy-Verzeichnis, z. B./var/calserver/.env) eintragen: -
Registry-Zugang: Die
calhelp/*-Images sind privat. Der Weg istregistry.calhelp.demit dem Lizenzschluessel: Ist unter Administration → Abo & Module ein Schluessel hinterlegt, meldet sichdeploy.shdamit an, zieht das API-Image zur Probe und schreibt erst dannIMAGE_REGISTRY=registry.calhelp.dein 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-.envsteht 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.
-
Abo-Lizenz: Auf Installationen, die gegen einen Lizenzdienst laufen (
LICENSE_SERVER_URL+LICENSE_PUBLIC_KEYgesetzt), 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. -
Deploy ausfuehren —
deploy.shaktiviert das Compose-Profilv2-updaterautomatisch, sobaldUPDATER_TOKENgesetzt ist:
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¶
- Pruefen:
/admin/updates→ „Jetzt pruefen" fragt den Updater nach neuerenv<x.y.z>-Tags in der Registry (KanalUPDATE_CHANNEL, Defaultstable). - Anwenden: „Aktualisieren" schreibt einen Eintrag ins Versionslog
(
update_logs), der Updater pullt die Images, schreibtV2_IMAGE_TAGin die.envund erstellt die V2-Container neu (docker compose up -d --no-deps). Migrationen laufen beim Container-Start automatisch (Entrypoint). - 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).
- 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:
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).
- 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-Quellendevelop-latest/release-candidate: dann muss auf dem Quell-Branch gerade kein Build laufen und der letzte wirksameci.yml-Lauf gruen sein. Wirksam heisst: der Lauf hat ueberhaupt Jobs ausgefuehrt. Wartende und von der concurrency-Gruppe verdraengte Laeufe zaehlen nicht —ci.ymlnutztcancel-in-progress: false, GitHub haelt pro Gruppe genau einen wartenden Lauf, und jeder neue Push cancelt den bisher wartenden. Auf einem aktivendevelopist der neueste Lauf deshalb meistens einer, der noch keinen Pointer bewegt hat. Fuer<branch>-<sha7>,release-latest,beta-latestundv<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 fuercalserver-updaterundcalserver-seal, die frueher auf:latestzurueckfielen: ein nackteslatestpusht kein Build mehr. - Re-Tag ohne Rebuild: Jedes Image erhaelt per
docker buildx imagetools createaus der Digest-Referenz das unveraenderliche Semver-Tagv<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). - Rolling-Pointer: Erst wenn alle sieben v-Tags stehen, wird
release-latest(bzw.beta-latestbei Pre-Releases) auf dieselben Digests geschoben — der Pointer-Satz zeigt nie auf eine halb getaggte Version. - 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>mitappVersion: v<VERSION>gepackt und ueberdeploy-docs.ymlunterhttps://doc.calserver.com/chartsveroeffentlicht. Die Image-Tags im Chart sind leer und fallen aufappVersionzurueck; Cluster aktualisieren selbst perhelm upgrade --reuse-values <release> calwebapp/calserver --version <VERSION>(Hintergrund: ADR 2026-10-23). - Demo-Deploy: Der Demo-/Release-Server wird auf den frisch
promoteten Stand (
release-latest) aktualisiert — frueher der master-Build-Job inci.yml, jetzt Teil des Promotes. Ein Identitaets-Check stellt sicher, dass die V2-Container danach wirklichrelease-latestfahren (ein in der Host-.envgepinnterV2_IMAGE_TAGmacht den Lauf rot statt still zu unterlaufen). - Changelog-Push: Alle Releases werden als JSON-Snapshot per
POST /internal/updater/changelogan den Prod-Updater uebergeben und landen alsstorage/app/changelog.jsonin der V2-API — die Quelle der Release-Notes auf/admin/updates. - Prod-Trigger (optional):
POST /internal/updater/updatespielt die Version direkt auf dem Prod-System ein (abschaltbar per Dispatch-Optionskip_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/updatesbestaetigt.
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):
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¶
3. Deploy-Skript ausfuehren¶
Das deploy.sh-Skript automatisiert den gesamten Update-Prozess:
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:
Exit-Code 0 heisst „nichts offen". Kommt eine Liste, steht der Grund im Startprotokoll des Containers:
Derselbe Stand steht in der Oberflaeche unter Administration > Betrieb > Datenbankstand. Nachholen laesst er sich mit:
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.0oder ein CI-Tag wieupdater-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:
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:roselbst oder verzichtet auf die Einrichtung aus der Oberfläche (der Dienst siegelt dann normal weiter). PDF_SIGNING_PASSWORDwird zur Voraussetzung.deploy.sherzeugt es, wenn die.envdie 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:
- Pruefen Sie die Anwendung im Browser auf korrekte Funktion
- Kontrollieren Sie die Logs auf Fehlermeldungen:
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.