Deployment & Betrieb¶
Wie man den DICOM-Router baut, paketiert und betreibt. Dies ist Referenz für den
aktuellen Build; die massgebliche Schrittliste für Installation/Update wird mit der
Distribution selbst in deploy/systemd/dicom-router.service ausgeliefert (deren
Kopfzeile dokumentiert die exakten Kommandos).
Verwandte Doku: User-Guide · Configuration · Management & Drain
Voraussetzungen¶
- Java 25 oder höher (
maven.compiler.releaseist25) - Maven 3.9+
- PostgreSQL — nur wenn datenbankgestütztes Connection-Tracking genutzt wird
(
DB_ENABLED=true). DICOM-Routing funktioniert auch ohne. - Bash auf den Betriebs-Hosts für die Skripte unter
bin/. Ein zusätzlichesnetcat-Paket brauchtdicom-router-ctlnicht: es spricht den Management-Port bevorzugt über Bashs eingebautes/dev/tcpan und nutztncnur als Rückfallweg (jede Variante, siehe management.md, Abschnitt 4).
Aus dem Quellcode bauen¶
Der vollständige Build dauert ~2 Minuten. Die lauffähige Anwendung ist
dicom-router-core/target/dicom-router-core-<version>.jar, mit ihren Abhängigkeiten
unter dicom-router-core/target/lib/ und dem Beispiel-Plugins-JAR unter
dicom-router-plugins/target/.
Direkt starten¶
Die Anwendung sucht ihre Konfiguration unter ./config/dicom-router.yaml (relativ zum
Arbeitsverzeichnis). Existiert die Datei nicht, wird beim ersten Start eine
Standard-Vorlage aus dem JAR kopiert. Für die YAML-Struktur siehe
configuration.md und für die Auflösung des Loggings
management.md (Abschnitt Logging).
Distributions-tar.gz (Produktion)¶
Das Release-Archiv (von CI gebaut, .gitea/workflows/build.yml) trennt App-Code
(bei jedem Update ersetzt) von betreiberverwalteter Config (über Updates erhalten).
Das tar.gz enthält nur die App-Seite; config/ wird vom Betreiber bei der
Erstinstallation angelegt und von tar nie angefasst.
/opt/imconnect/ # aus tar.gz entpackt (App-Seite)
├── dicom-router-core-<version>.jar # Hauptanwendung
├── lib/ # Abhängigkeiten
├── plugins/ # Beispiel-Plugins-JAR
├── bin/
│ ├── start.sh # lädt config/dicom-router.env, setzt -Dlogback.configurationFile, dann java -jar
│ ├── dicom-router-ctl # Management-CLI
│ ├── dicom-router-sync # Config auf alle Cluster-Hosts verteilen + reloaden
│ └── dicom-router-haproxy-sync # HAProxy-Rückweg-Config verteilen + reloaden
├── share/ # Vorlagen — bei Erstinstallation nach config/ kopiert
│ ├── dicom-router.yaml.example
│ ├── logback.xml.example
│ ├── dicom-router.env.example
│ ├── dicom-router.service.example
│ ├── cluster-hosts.example
│ ├── haproxy-hosts.example # HAProxy-Hosts für dicom-router-haproxy-sync
│ ├── audit-retention.sql.example # Retention-Beispiel für die Audit-Tabellen (psql)
│ ├── audit-timestamp-utc-fix.sql.example # einmalige Zeitstempel-Korrektur für Bestände < 1.17.2
│ └── haproxy-example.cfg
├── docs/ # Betreiber-Doku offline (User-Guide, Plugin-Referenz, Configuration, Management, Deployment)
├── README.md
├── CHANGELOG.md
├── LICENSE # proprietäre Nutzungsbedingungen
└── config/ # betreiberverwaltet, übersteht Updates
├── dicom-router.yaml
├── logback.xml
└── dicom-router.env # chmod 600 — enthält DB-Zugangsdaten
bin/start.shliegt wie die übrigen Skripte im Quellbaum unterbin/; der CI-Build kopiert es unverändert in die Distribution.
share/audit-retention.sql.example(Quelle:deploy/sql/audit-retention.sql.example) ist ein kommentiertes psql-Skript, das die Audit- und die Q/R-Tracking-Tabellen der Datenbank nach einer einstellbaren Frist aufräumt. Es ist damit nicht nur für Audit-Betreiber relevant:cmove_tracking_studiesspeichert StudyInstanceUIDs, sobald eineCMoveTrackingStagemitstore: databaseläuft — unabhängig vonaudit.enabled. Wer die Q/R-Bridge mit Datenbank-Store betreibt, braucht das Skript also auch bei ausgeschaltetem Audit-Trail, sonst bleiben abgelaufene Tracking-Zeilen samt UIDs faktisch liegen (die Anwendung räumt nur beiläufig auf, beim zufälligen Wiederverwenden derselben Nummer). Der Router liest es nie und braucht es nicht zum Starten — er löscht selbst nichts, Retention ist bewusst Betreiber-Sache und zugleich eine Datenschutz-Massnahme. Wie die übrigen Vorlagen wird es trotzdem nachconfig/kopiert (config/audit-retention.sql) und dort bearbeitet: Die Fristen müssen bewusst gesetzt werden, undconfig/übersteht Updates, währendshare/bei jedem Update ersetzt wird. Auf diesen Pfad beziehen sich die cron- und systemd-Timer-Beispiele im Skript. Sie rufen nicht das Skript direkt auf, sondern den ebenfalls dort ausgeschriebenen Loop-Wrapperaudit-retention-run.sh: Ein Lauf löscht höchstensbatch_sizeZeilen pro Statement, ein einzelner Aufruf pro Tag hält also nur unterhalb dieses Volumens mit. Der Wrapper ruft das Skript in einer Schleife auf, bis dessen SchlusszeileAUDIT_RETENTION_DELETED_TOTAL 0meldet. Hintergrund indocs/database.mdim Repository (in der Distribution nicht enthalten).
Erstinstallation¶
- Entpacken:
tar -xzf dicom-router-X.Y.Z.tar.gz --strip-components=1 -C /opt/imconnect config/ausshare/*.examplebootstrappen und anpassen (dicom-router.yaml,logback.xml,dicom-router.env).
Die vollständige Schrittliste (dedizierter Benutzer, Ownership, Verzeichnisse) steht in
der Kopfzeile von deploy/systemd/dicom-router.service — diese Datei ist die
massgebliche Quelle.
Update¶
systemctl stop dicom-router
# Zuerst die App-Dateien des vorigen Releases entfernen: die Modul-JARs sind versionsbenannt,
# ein Entpacken über den alten Baum sammelt sie an und start.sh verweigert den Start
# (vor v1.6.1 startete stattdessen stillschweigend das ALTE Core-JAR).
# config/ und etwaige Drittanbieter-Plugin-JARs bleiben unangetastet.
rm -f /opt/imconnect/dicom-router-core-*.jar
rm -f /opt/imconnect/plugins/dicom-router-plugins-*.jar
rm -rf /opt/imconnect/lib /opt/imconnect/share
tar -xzf dicom-router-X.Y.Z.tar.gz --strip-components=1 -C /opt/imconnect
systemctl start dicom-router
Das Verzeichnis config/ bleibt über Updates erhalten.
systemctl stop stoppt verlustfrei: Die Unit drained zuerst (keine neuen
Verbindungen) und wartet, bis alle laufenden Übertragungen abgeschlossen sind — der
Stopp kann sich also hinziehen, solange z. B. ein grosser C-STORE läuft. Neue
Verbindungen werden im Fenster zwischen stop und start abgelehnt; wer das
vermeiden muss, betreibt mehrere Instanzen hinter HAProxy und updatet rollend
(eine Instanz drainen, updaten, wieder aufnehmen — siehe
management.md).
systemd¶
Die Quell-Vorlagen liegen in deploy/systemd/ (dicom-router.service,
dicom-router.env.example); CI kopiert sie mit dem Suffix .example nach share/.
Die Unit lädt alle Laufzeit-Einstellungen (LOG_DIR, DB-Zugangsdaten, JAVA_OPTS, …)
aus dem Env-File (EnvironmentFile=). Als Basis für /etc/systemd/system/ verwenden.
Die Unit härtet mit ProtectSystem=strict ab und erlaubt Schreibzugriffe nur auf
/var/imconnect und /var/log/imconnect (plus /tmp//var/tmp via
PrivateTmp). /opt/imconnect — das App-Verzeichnis selbst — ist bewusst
nicht beschreibbar: der Prozess liest nur daraus (JAR, lib/, config/) und
schreibt nie, sodass ein read-only-Zustand einen kompromittierten Prozess daran
hindert, seine eigenen Binaries oder seine Config zu manipulieren. Der Audit-Spool
(audit.spool-path, Default /var/imconnect/audit-spool) muss innerhalb eines der
beschreibbaren Pfade bleiben — ausserhalb kann der Audit-Trail seinen Spool nicht öffnen
und deaktiviert sich selbst (lautstark, Routing unbeeinträchtigt). Laufen mehrere
Router-Instanzen auf einem Host, gibt man jeder ihren eigenen spool-path (das
Spool-Verzeichnis ist pro Instanz).
Dateirechte: router.storage-path hält transient unverschlüsselte Patientendaten
(P-DATA-Spill-Dateien während der Verarbeitung), ebenso PDU-Dumps
(router.log-folder), Tap-Captures/-Spools und der Audit-Spool (audit.spool-path:
unverschlüsseltes JSONL mit Patienten-ID, Study-/SOP-Instance-UIDs und Accession
Number, mit capture-patient-name zusätzlich dem Patientennamen — die Einträge
liegen dort, bis der Shipper sie in die Datenbank verschoben hat); der Router legt
alle diese Dateien und die selbst erstellten Verzeichnisse owner-only an
(0600/0700). Das Löschen
ist ein gewöhnliches unlink, kein Überschreiben der Blöcke — wer das Restrisiko auf
dem Datenträger ausschliessen will, legt die Pfade auf ein verschlüsseltes Volume.
Die Unit setzt zusätzlich UMask=0027, womit auch alles Übrige, was der Prozess
schreibt (z. B. die Application-Logs), höchstens gruppenlesbar entsteht.
Das Env-File config/dicom-router.env gehört auf chmod 600. Es enthält
JDBC_PASSWORD im Klartext, und aus JAVA_BIN/JAVA_OPTS baut
bin/start.sh die Kommandozeile des JVM-Starts — Schreibrechte auf die Datei
bedeuten damit Code-Ausführung als Router-User. bin/start.sh liest beim
Start die Rechte-Bits der Datei und schreibt eine Warnung auf stderr, wenn
davon etwas ausserhalb des Eigentümers gesetzt ist; der Start läuft trotzdem
weiter, und wo kein stat verfügbar ist, entfällt die Prüfung wortlos. Sie
sagt nur etwas über die Rechte-Bits dieser einen Datei aus. Eigentümer und
Rechte des config/-Verzeichnisses prüft sie nicht, obwohl beide genauso
zählen: systemd liest EnvironmentFile= als root, also auch eine Datei, die
dem Router-User gar nicht gehört, und über ein gruppen- oder weltschreibbares
config/ lässt sich die Datei austauschen, ohne dass sich ihre Rechte-Bits
ändern. Beides bleibt in der Verantwortung des Betreibers. UMask=0027 greift
hier nicht: die Einstellung gilt nur für Dateien, die der Router-Prozess
selbst schreibt. Das Env-File entsteht beim Kopieren aus
share/dicom-router.env.example und trägt die Rechte, die dabei gesetzt
werden.
Docker¶
Das Image (docker/Dockerfile) ist kein Multi-Stage-Build — es erwartet die bereits
gebauten Artefakte unter docker/app/, analog zum CI-Workflow. Lokal bauen:
# 1) Alles bauen
mvn clean install
# 2) Artefakte in den Docker-Build-Kontext stagen
VERSION=$(mvn -q -Dexec.executable=echo -Dexec.args='${project.version}' --non-recursive exec:exec)
mkdir -p docker/app/lib docker/app/bin docker/app/plugins docker/app/share docker/app/config
cp dicom-router-core/target/dicom-router-core-${VERSION}.jar docker/app/dicom-router.jar
cp dicom-router-core/target/lib/*.jar docker/app/lib/
cp bin/dicom-router-ctl docker/app/bin/
cp dicom-router-core/src/main/resources/logback.xml docker/app/config/logback.xml
cp dicom-router-plugins/target/dicom-router-plugins-${VERSION}.jar docker/app/plugins/
cp LICENSE docker/app/
# Referenz-Vorlagen in share/ eingebacken (das Dockerfile COPYt app/share/ → /app/share/).
# share/ ist 1:1 das Distributions-share/ — dieselben 8 Vorlagen wie im tar.gz:
cp dicom-router-core/src/main/resources/logback.xml docker/app/share/logback.xml.example
cp dicom-router-plugins/src/main/resources/example-dicom-router.yaml docker/app/share/dicom-router.yaml.example
cp deploy/systemd/dicom-router.service docker/app/share/dicom-router.service.example
cp deploy/systemd/dicom-router.env.example docker/app/share/
cp deploy/cluster-hosts.example docker/app/share/
cp deploy/haproxy-hosts.example docker/app/share/
cp deploy/sql/audit-retention.sql.example docker/app/share/
cp deploy/sql/audit-timestamp-utc-fix.sql.example docker/app/share/
cp deploy/haproxy/haproxy-example.cfg docker/app/share/
# 3) Bauen und starten (DICOM 11112, Agent-Check 8404, Management 8405)
docker build -t dicom-router docker/
docker run -p 11112:11112 -p 8404:8404 -p 8405:8405 -v ./config:/app/config dicom-router
Ein eigenes logback.xml einhängen, um den eingebackenen Default zu überschreiben:
Umgebungsvariablen¶
| Variable | Beschreibung | Default |
|---|---|---|
DB_ENABLED |
Datenbank-Konnektivität aktivieren/deaktivieren (true/false) |
true |
JDBC_URL |
JDBC-Verbindungs-URL | jdbc:postgresql://localhost:5432/dicomrouter |
JDBC_USER |
Datenbank-Benutzer | aus persistence.xml |
JDBC_PASSWORD |
Datenbank-Passwort | aus persistence.xml |
HIBERNATE_SHOW_SQL |
SQL-Statements loggen | false |
HIBERNATE_FORMAT_SQL |
Geloggtes SQL formatieren | false |
HIKARI_MAX_POOL_SIZE |
Maximale Grösse des Connection-Pools (Minimum 3: eine Schema-Migration hält den Cluster-Lock auf einer eigenen Verbindung, während die Migration selbst auf einer zweiten läuft — Details in docs/database.md im Repository) |
10 |
HIKARI_MIN_IDLE |
Minimale Idle-Grösse des Connection-Pools | 2 |
LOG_DIR |
Verzeichnis für Anwendungslogs (von logback.xml genutzt, nicht von Java) |
/var/log/imconnect |
DICOM_ROUTER_TOKEN_FILE |
Token-Datei für die state-changing Kommandos von dicom-router-ctl (von der CLI genutzt, inkl. systemd ExecStop; nur nötig, wenn management.token-file auf einen nicht-standardmässigen Ort zeigt, siehe management.md) |
/opt/imconnect/config/management.token (falls vorhanden) |
Alle Variablen liegen in
config/dicom-router.env, das sowohl die systemd-Unit (EnvironmentFile=) als auchbin/start.shladen — ein manueller Start verhält sich wie der Dienst. Ein Unterschied: beistart.shgewinnen bereits in der Shell gesetzte Variablen gegenüber der Datei, sodass Ad-hoc-Overrides wieLOG_DIR=/tmp/x bin/start.shfunktionieren; die Werte werden geparst wie systemd es tut (keine Shell-Expansion), nie ge-sourced.Mit
DB_ENABLED=falsestartet der Router ohne jede Datenbankverbindung. DICOM-Routing funktioniert vollständig, aber Connection-Tracking wird nicht persistiert — nützlich für Standalone-Deployments ohne PostgreSQL.
Hinter HAProxy¶
Für rollende Zero-Downtime-Deployments stellt der Router einen Agent-Check-Port und einen
Management-Port bereit und unterstützt drain/reload über dicom-router-ctl. Ein
vollständiges HAProxy-Beispiel liegt der Distribution als share/haproxy-example.cfg bei
(Quelle im Repository: deploy/haproxy/haproxy-example.cfg). Dessen optionale
Statistik-Seite bindet auf 127.0.0.1:8080, weil sie Backend-Adressen, Zustände und
Verkehrszähler offenlegt. Wer sie von aussen erreichen will, öffnet einen SSH-Tunnel
(ssh -L 8080:127.0.0.1:8080 <haproxy-host>) oder ändert die Bind-Adresse bewusst und
stellt Authentifizierung davor.
Die betrieblichen Details (Ports, Zustände, Kommandos, Config-Reload-Regeln) sind in
management.md dokumentiert.
Das Beispiel führt die Strecke mit und die ohne PROXY-Protokoll bewusst auf zwei
getrennte Router-Ports (11112 und 11113). Der Router entscheidet pro Verbindung anhand
der proxyprotocol:-Einträge — Absender-Adresse plus Annahme-Port —, ob ein
PROXY-Header erwartet wird; auf einem gemeinsamen Port sind beide Strecken nicht
unterscheidbar, und eine von beiden bricht. Ein Router-Prozess bindet genau einen
DICOM-Port (router.listener.port), der zweite Port ist also eine zweite Instanz.
Für Geräte, die anrufende IPs whitelisten, unterstützt der Router HAProxy-Rückwege
(haproxy-return + return-port an den Destinations, siehe
configuration.md): der Router wählt automatisch VIP:return-port
an, und die passenden HAProxy-listen-Blöcke generiert er selbst aus der YAML —
--render-haproxy, dokumentiert in management.md.
Die generierte Datei verteilt bin/dicom-router-haproxy-sync auf die
HAProxy-Hosts aus config/haproxy-hosts (Vorlage: share/haproxy-hosts.example)
und reloaded HAProxy unterbrechungsfrei — Ablauf und Modi in
management.md. Voraussetzungen auf jedem HAProxy-Host:
- passwortloser (key-based) ssh-Zugang vom Bearbeitungs-Host (wie bei
dicom-router-sync), - Schreibrecht des ssh-Benutzers auf den Zielpfad (Default
/etc/haproxy/conf.d/dicom-return.cfg), - lesbare
/etc/haproxy/haproxy.cfgfür den Preflight (haproxy -c), - eine sudo-Regel für den Reload, z. B.:
Das Reload-Kommando ist per DICOM_ROUTER_HAPROXY_RELOAD_CMD überschreibbar
(Setups unterscheiden sich, conf.d vs. EXTRAOPTS--f); die
Haupt-haproxy.cfg verwaltet das Skript nie.