.. _deployment-scenarios:
=====================
Deployment Scenarios
=====================
The container architecture described in :doc:`ms-architecture` does not change from
one installation to the next. What changes is *where* those containers run, and
which of the arrows between them become network hops that leave a machine, a
building or an organisation.
Three scenarios are relevant to ScanHub. They differ in how much is distributed,
not in what is deployed:
* **All-in-one on a single device** — everything on the scanner workstation.
This is what runs today.
* **Distributed in a clinic** — the backend moves to a hospital server; the UI
and the scanner become network clients.
* **Fully distributed** — the backend moves to a Kubernetes cloud platform and
integrates with hospital IT systems. This is the target vision.
Read them as a progression: each step takes a boundary that was internal and
turns it into a network boundary that has to be secured.
.. list-table::
:header-rows: 1
:widths: 20 27 27 26
* -
- All-in-one *(today)*
- Clinic
- Fully distributed *(vision)*
* - Backend runs on
- The scanner-embedded workstation
- One on-prem Docker host or cluster
- Kubernetes cloud platform
* - Brought up with
- ``docker compose up``
- ``docker compose`` on the server
- Node pools, CPU and GPU
* - UI reached at
- ``localhost:8443``
- Hospital network, over HTTPS
- Internet, over HTTPS
* - Device link
- WebSocket over localhost
- WebSocket over the hospital network
- WebSocket from the imaging site
* - Data lake
- Bind-mounted host directory
- Shared volume on the server
- Shared object storage (PVC)
* - Image archive
- Local XNAT
- XNAT on the same server
- Hospital PACS
* - Local IT needed
- None
- Hospital IT operates the server
- None at the imaging site
All-in-one on a single device (today)
=====================================
.. mermaid::
%%{init: {"flowchart": {"curve": "basis", "nodeSpacing": 40, "rankSpacing": 60}} }%%
flowchart LR
subgraph HOST[" Single scanner-embedded workstation · e.g. Nexus console "]
direction LR
subgraph BROWSER[" Browser "]
ui["ScanHub UI
localhost:8443"]
end
subgraph BACKEND[" Docker containers · docker compose up "]
direction TB
gw{{"nginx api-gateway
TLS · reverse proxy"}}
svc(["4 microservices
user · patient · device · protocol"])
dag["Dagster engine
webserver + daemon"]
db[("Databases
Postgres x2 · MongoDB")]
lake[["Data lake + XNAT
shared volume · DICOM store"]]
gw --> svc
svc --> db
svc -->|"orchestration"| dag
svc -.-> lake
dag -.-> lake
end
subgraph SCANNER[" Attached scanner "]
dc["Device client
localhost"]
end
end
ui <-->|"HTTPS · TLS
localhost"| gw
dc <-.->|"WebSocket · device certificate
localhost"| gw
classDef client fill:#e8f1fb,stroke:#4a7fb5,color:#1b3a5c
classDef gateway fill:#12354f,stroke:#0d2537,color:#ffffff
classDef service fill:#e5f4f1,stroke:#3f9e8c,color:#12433a
classDef store fill:#e7eff7,stroke:#4a7fb5,color:#1b3a5c
classDef orch fill:#efe7fb,stroke:#7a5cb5,color:#33215c
classDef external fill:#fdf0e2,stroke:#c8752e,stroke-dasharray:4 3,color:#7a4212
class ui client
class gw gateway
class svc service
class db,lake store
class dag orch
class dc external
style HOST fill:none,stroke:#12354f
style BROWSER fill:none,stroke:#c8cdd3,stroke-dasharray:5 4
style BACKEND fill:none,stroke:#c8cdd3,stroke-dasharray:5 4
style SCANNER fill:none,stroke:#c8752e,stroke-dasharray:5 4
This is the deployment used today, and the one :file:`docker-compose.yml`
describes directly. A single workstation embedded in the scanner — a Nexus
console, for example — runs the browser, every backend container and the device
client. ``docker compose up`` brings up the whole system, and every arrow in the
diagram stays on ``localhost``.
**The UI** is opened in the local browser at ``https://localhost:8443``. Because
the gateway publishes its ports on the host, other machines on the same network
can reach the same UI through port forwarding — convenient for a demo, and the
reason the ``8080``/``8443`` mappings are labelled *for development* in the
compose file.
**The device client** runs on the same host and connects to the ``device-manager``
over the local WebSocket. The attached scanner is served locally: no network in
between, no certificates crossing a boundary that matters.
**XNAT and the data lake** live on this machine too — the data lake as a
bind-mounted directory, XNAT as a locally reachable DICOM store.
The appeal of this setup is that it has no infrastructure requirements at all. A
scanner and a workstation are enough, which is precisely what makes a portable
low-field system usable outside a hospital — or outside the lab entirely. The
trade-off is equally clear: nothing is shared, nothing is backed up by
institutional IT, and the machine's CPU is the only compute the reconstruction
will ever get.
Distributed deployment in a clinic
==================================
.. mermaid::
%%{init: {"flowchart": {"curve": "basis", "nodeSpacing": 40, "rankSpacing": 70}} }%%
flowchart LR
subgraph USER[" User device · nothing installed "]
ui["Web browser
ScanHub UI (React)"]
end
subgraph HOSPITAL[" Hospital IT · on-prem server "]
direction TB
gw{{"nginx api-gateway
TLS · reverse proxy"}}
svc(["4 microservices
user · patient · device · protocol"])
dag["Dagster engine
webserver + daemon"]
db[("Databases
Postgres x2 · MongoDB")]
lake[["Data lake + XNAT
shared volume · DICOM store"]]
gw --> svc
svc --> db
svc -->|"orchestration"| dag
svc -.-> lake
dag -.-> lake
end
subgraph MRI[" MRI device "]
dc["Device client
agent on / near the scanner"]
end
ui <-->|"HTTPS · TLS"| gw
dc <-.->|"WebSocket · device certificate"| gw
classDef client fill:#e8f1fb,stroke:#4a7fb5,color:#1b3a5c
classDef gateway fill:#12354f,stroke:#0d2537,color:#ffffff
classDef service fill:#e5f4f1,stroke:#3f9e8c,color:#12433a
classDef store fill:#e7eff7,stroke:#4a7fb5,color:#1b3a5c
classDef orch fill:#efe7fb,stroke:#7a5cb5,color:#33215c
classDef external fill:#fdf0e2,stroke:#c8752e,stroke-dasharray:4 3,color:#7a4212
class ui client
class gw gateway
class svc service
class db,lake store
class dag orch
class dc external
style USER fill:none,stroke:#c8cdd3
style HOSPITAL fill:none,stroke:#12354f
style MRI fill:none,stroke:#c8752e
The same containers, moved off the scanner and onto a server that hospital IT
operates. One Docker host — or a small cluster — runs the entire backend: every
container from :file:`docker-compose.yml`, unchanged.
**Nothing is installed on the user side.** Any clinician workstation, laptop or
tablet on the hospital network opens the app in a browser and talks to the
gateway over HTTPS. This is the practical payoff of keeping the UI a pure client:
rolling out ScanHub to a department is a URL, not a software deployment.
**The scanner keeps only the device client** — a lightweight agent on or near the
machine that holds a WebSocket connection to the ``device-manager``, receives
scan commands and streams raw data back. It is the only ScanHub software running
at the machine, and the only component that has to be certified with the device.
Two things change compared to the all-in-one setup. Data now lives on hospital
infrastructure, with the backup, access control and physical security that
implies. And the two links leaving the server — browser to gateway, device client
to gateway — cross a real network, so TLS and device certificates stop being a
formality.
Fully distributed setup (vision)
================================
.. mermaid::
%%{init: {"flowchart": {"curve": "basis", "nodeSpacing": 35, "rankSpacing": 65}} }%%
flowchart LR
subgraph EDGE[" Edge · imaging sites "]
direction TB
tablet["Portable device
ScanHub UI"]
mri["MRI device
+ device client"]
end
subgraph CLOUD[" Cloud platform · Kubernetes "]
direction TB
gw{{"nginx api-gateway
TLS · reverse proxy"}}
subgraph CPU[" CPU node pool "]
direction TB
svc(["4 microservices
user · patient · device · protocol"])
db[("Databases
Postgres x2 · MongoDB")]
end
subgraph GPU[" GPU node pool "]
dag["Dagster engine
webserver + daemon"]
end
lake[["Data lake
shared object storage (PVC)"]]
gw --> svc
svc --> db
svc -->|"orchestration"| dag
svc -.-> lake
dag -.-> lake
end
subgraph HOSP[" Hospital IT "]
direction TB
hl7["HL7 / FHIR engine
interface · integration"]
mwl["Modality Worklist
DICOM worklist"]
ris["RIS
orders · worklist · reports"]
pacs["PACS
patient data + image archive"]
end
tablet <-->|"HTTPS · TLS"| gw
mri <-.->|"WebSocket"| gw
svc <-.->|"patient · study · worklist"| hl7
hl7 --- ris
hl7 --- mwl
svc -.->|"archive DICOM"| pacs
classDef client fill:#e8f1fb,stroke:#4a7fb5,color:#1b3a5c
classDef gateway fill:#12354f,stroke:#0d2537,color:#ffffff
classDef service fill:#e5f4f1,stroke:#3f9e8c,color:#12433a
classDef store fill:#e7eff7,stroke:#4a7fb5,color:#1b3a5c
classDef orch fill:#efe7fb,stroke:#7a5cb5,color:#33215c
classDef external fill:#fdf0e2,stroke:#c8752e,stroke-dasharray:4 3,color:#7a4212
classDef hospital fill:#fbeaee,stroke:#a8324a,color:#5c1425
class tablet client
class gw gateway
class svc service
class db,lake store
class dag orch
class mri external
class hl7,mwl,ris,pacs hospital
style EDGE fill:none,stroke:#c8cdd3
style CLOUD fill:none,stroke:#1a6fc4
style CPU fill:none,stroke:#1a6fc4,stroke-dasharray:5 4
style GPU fill:none,stroke:#7a5cb5,stroke-dasharray:5 4
style HOSP fill:none,stroke:#a8324a
The target architecture, in which the backend leaves the hospital entirely and
the imaging site keeps almost nothing.
**At the edge**, an imaging site has no local IT: only the scanner, running the
device client, and a tablet or PC that opens the ScanHub UI. Sites become cheap
to add, which is the point — a low-field scanner in a clinic, in a van or in a
remote location is just another WebSocket connection.
**In the cloud**, the same containers run on Kubernetes, but the deployment can
finally exploit the fact that they were always separable. The microservices and
databases sit in a CPU node pool and scale horizontally; the Dagster engine sits
in a GPU node pool, where reconstruction and AI processing actually belong. The
data lake becomes shared object storage backed by a persistent volume claim
rather than a bind-mounted directory.
**Hospital IT** is integrated rather than replaced. ScanHub reads patient, study
and worklist data from PACS and RIS through the HL7/FHIR interface engine and the
DICOM Modality Worklist, and archives reconstructed DICOM images back into the
hospital PACS. The clinical record stays where clinicians and existing systems
expect it; ScanHub holds the working data.
**Both boundaries are trust boundaries.** Edge to cloud and cloud to hospital
each cross an organisational border, and each carries patient data. Everything
that is a local convenience in the all-in-one setup — self-signed certificates,
localhost WebSockets, an open ``5432`` — becomes a requirement here.
What stays the same
===================
Across all three scenarios:
* **The compose stack is the unit of deployment.** The same images and the same
inter-container contracts are used whether the target is a scanner console or
a node pool.
* **The gateway is the only ingress.** Every client reaches the backend through
nginx, so relocating the backend changes a hostname, not the application.
* **The device client is the only software at the machine.** Everything else the
scanner needs is on the other end of a WebSocket.
* **The UI is a browser away.** Nothing is installed on a user device in any
scenario.
* **The data lake is a mount point, not a service.** A bind mount, a shared
volume or a PVC — the containers see ``/data`` either way.
What changes is the length of the arrows, and how much of what runs on them has
to be secured, replicated and operated.