Deploy components at the trust level:
- Orthanc (orthanc)
- Imaging API (imaging-api)
- Data Access API (data-access-api)
- Trust API (trust-api)
- OMOP Database (omop-db)
- XNAT (xnat)
- Observability (observability) — Grafana (host
GRAFANA_PORT, container 3000), Loki (hostLOKI_PORT, container 3100) and an Alloy log collector (internal only, no published port). Each trust runs its own; logs never leave the trust.
See also the dedicated README files under each folder.
Orthanc, Imaging API, Data Access API and Trust API can be started using the Makefile provided at the repository level:
make upDICOMs can be uploaded to Orthanc at http://localhost:8042 — log in with ORTHANC_USERNAME/ORTHANC_PASSWORD from the trust kit file (trust/.env.<CODE>.<env>).
The Trust API polls the Central Hub for tasks. In development, it connects to the hub over HTTP on the internal Docker network.
Use this flow when you want a trust whose identity (keys, FL kit slot) came from the hub at runtime. The trust table on the hub is the sole trust registry — there is no env-slot model. The trust's plaintext keys never live in a hub-side env file; they live only in the trust's gitignored kit file. The Ansible-driven prod flow lives in docs/source/deploy-flip/deploy-flip-node-on-prem.rst.
A trust is registered on the running hub rather than configured via env-file key dicts. The kit files (trust/.env.<CODE>.<env>) ARE the roster. Either:
make register-trusts(from the repo root) — registers the shipped dev roster (everytrust/.env.*.development.example, currently GSTT and KCH; run automatically bymake up), ormake register-trust KIT=<CODE>— registers one trust (aftermake new-trust TRUST_CODE=<CODE> TRUST_NAME="..."scaffolds its kit), or- the Add Trust button on the Connection status page — enter the friendly name, code, and region. This registers the trust on the hub; to produce a deliverable kit file use the
make register-trustflow above.
Registration (register_trust service, POST /admin/trusts):
- mints a
TRUST_API_KEYandTRUST_INTERNAL_SERVICE_KEY(random tokens), - stores only the SHA-256 of the API key on
trust.api_key_hash, - claims the next free
fl_kit_slotrow and binds it to the new trust id.
make register-trust / register-trusts are idempotent and write the resulting credentials straight into the per-trust kit file — the hub never stores either plaintext again.
Each trust stack reads a per-trust kit file trust/.env.<CODE>.<env> (e.g. trust/.env.GSTT.development, trust/.env.<CODE>.prod; gitignored; dev templates trust/.env.<CODE>.development.example for GSTT/KCH, generic base trust/.env.example), auto-included by trust/Makefile. make register-trust KIT=<CODE> writes the managed blocks. It carries:
TRUST_API_KEY=<from kit>
TRUST_INTERNAL_SERVICE_KEY=<from kit>
FL_KIT_SLOT=<from kit>
FL_KIT_SLOT_NUMBER=<from kit>
EXPECTED_TRUST_ID=<from kit>plus the trust's identity (TRUST_NAME / TRUST_CODE / TRUST_REGION, read by register-trust) and its host-local ports and data directories. The optional EXPECTED_TRUST_ID lets trust-api self-check the hub-resolved id at startup.
The kit also carries XNAT's own AE title and the upstream PACS block (see the commented
── Upstream PACS ── section of trust/.env.example, and
xnat/README.md for what each one does):
XNAT_AETITLE=XNAT
PACS_HOST=orthanc
PACS_AETITLE=ORTHANC
PACS_QR_PORT=4242XNAT_AETITLE is applied to XNAT's SCP receiver, its DQR calling AE and the C-MOVE destination it
hands the PACS, so all three agree and the PACS's return-leg association is addressed correctly.
up-trust / up-trust-ec2 run a require-xnat-aetitle guard first: the variable may be absent
(the default XNAT applies) but must not be present-and-empty — that fails the bring-up naming
the kit file, before any data fixture is fetched. The remaining PACS_* and DQR_* variables
(PACS_LABEL, PACS_SUPPORTS_EXTENDED_NEGOTIATIONS, PACS_AVAILABILITY_DAYS/_START/_END,
PACS_THREADS, PACS_UTILIZATION_PERCENT, DQR_MAX_PACS_REQUEST_ATTEMPTS,
DQR_RETRY_WAIT_SECONDS) default to the mocked Orthanc plus an unthrottled retrieval window; a
real trust agrees them with its PACS manager.
The kit also carries the trust's disclosure floor:
COHORT_QUERY_THRESHOLD=10This is the minimum cohort size the trust will release anything about. Cohort statistics below it are privacy-suppressed (a genuine zero and a small count are indistinguishable), and both row-level routes refuse outright — /cohort/dataframe, which supplies FL training data, and /cohort/accession-ids, which decides whose imaging is pulled into XNAT. Raise it to release less. It is the operator's setting, not the hub's: trusts need not agree on a value, and the hub cannot lower it. See data-access-api/README.md. The same schema serves dev trusts (GSTT/KCH against a local hub), on-prem trusts (against a prod hub), and laptop-against-prod testing — operator picks the kit code (trust/.env.<CODE>.<env>) and make -C trust up-trust KIT=<CODE> HUB=<env> handles the rest.
The kit file's Host-local profile can set FL_SITE_PRIVACY_POLICY=percentile (plus optional
FL_SITE_PRIVACY_* parameters — see the commented block in trust/.env.example). The fl-client entrypoint
renders these into the client's FLARE local/privacy.json at container start, so THIS trust's
update-privacy filter is applied to every outgoing model update regardless of the researcher's app config
(site filters run before app filters and a job's configuration cannot opt out or pick a weaker scope). The
bound object is the job's configuration: every FLARE app template runs the Client API in_process, so job
code runs inside the client's job process and is not currently isolated from the filter (FLIP#1572). Unset =
no site policy (app-level filters only, the previous behavior). Invalid values stop the fl-client at startup
(fail closed). Apply changes with make -C trust up-fl-clients-kit KIT=<CODE>
(down-fl-clients-kit KIT=<CODE> stops and removes
just that trust's clients; up-fl-clients / down-fl-clients do the same across every kit for
this env, leaving the rest of each trust stack up); the fl-client log then shows
[site-privacy] site privacy policy ACTIVE: .... Details: docs/source/components/component-fl-privacy.rst
("Site-enforced privacy policy").
The two controls above are the trust's runtime access policy, and both are single settings compiled
into the services. A trust that wants to state them together — or to vary a rule by project — can
write a governance document instead and point ACCESS_POLICY_FILE at it from the kit file. Start
from governance.example.toml, which is a worked example of every
section.
One file, three sections, each read by the service that enforces it:
| Section | Read by | Replaces |
|---|---|---|
[disclosure] |
data-access-api | COHORT_QUERY_THRESHOLD (may raise it, never lower it) |
[access] |
data-access-api | nothing — new: permit/deny rules over project + operation |
[fl_privacy.nvflare] |
FLARE fl-client | FL_SITE_PRIVACY_* — an alternative to them, not an override: set the filter in one place, since both set stops the client |
The document is optional and additive. Unset means the platform defaults apply and behaviour is
exactly as before — the two variables above stay the only controls, so no existing trust has to
change anything. Nothing here can weaken a trust's posture: min_cohort_size may only raise the
threshold, and an action no rule mentions keeps its existing behaviour (which is what lets a trust
adopt one rule without enumerating everything).
Rules are decided the same way whatever their order: for an action the document mentions, any
matching deny denies; otherwise the strictest matching permit permits; otherwise the request is
denied. So a deny list needs a permit for everyone else (the example shows one). Mind what the
actions gate: cohort.dataframe is the FL client's own training-data fetch, so denying it for a
project stops that project's federated training at this trust; cohort.accession_ids decides whose
imaging is pulled into XNAT, and only real accessions from omop.image_occurrence are ever returned.
A cohort.dataframe deny does not by itself stop a caller holding the trust-internal key from
learning about that project's subjects: it can still send predicate queries to
/cohort/accession-ids and compare the accession sets that clear the floor. To close a project off
entirely, deny cohort.accession_ids for it too.
Validation is strict and fails closed. An unknown key, a misspelt action, a project id that is not a
UUID, a rule with no effect, an empty file, or a threshold below the kit's floor stops the service
at startup rather than being ignored — a silently-dropped access rule is worse than no rule, because
the operator believes it is in force. check-governance runs both halves through the loaders the
services themselves use (data-access-api's for [disclosure]/[access], the fl-client's own
site-policy module for [fl_privacy.nvflare]) on the host's own Python, installing nothing. It also
fails a site privacy section on a Flower trust, where nothing enforces it yet (FLIP#852), and a
filter set in both the document and FL_SITE_PRIVACY_*. Validate, then apply:
make -C trust check-governance KIT=<CODE>
make -C trust reload-governance KIT=<CODE>reload-governance is how an edit is applied: it recreates data-access-api and, on FLARE, the
fl-clients (with the step that extracts their section), and touches no data. It then waits for
data-access-api's [governance] policy ACTIVE … sha256=… startup line and checks the digest is the
document's, so an image too old to read the document fails the reload instead of passing it. Do
not apply a policy change with up-trust or restart-trust: both are first-install verbs, and on
a live trust either can destroy data — up-trust's XNAT step runs xnat-reset, wiping the XNAT
archive and database, and its seeding step can replace the data volumes. Recreating the fl-clients
does interrupt any job they are running, so apply between runs.
The document is mounted read-only into data-access-api — a service can never rewrite its own policy —
and is operator-owned: the hub cannot set, read, or override it. The fl-client never mounts it:
researcher code runs there, so a one-shot fl-governance-init step writes only the
[fl_privacy] section into the client's kit local/ directory. On Kubernetes, sync-kit embeds the
document from the kit's ACCESS_POLICY_FILE as the chart's governance.document (see
deploy/helm/README.md). A remotely driven trust (the EC2 path, over an ssh
DOCKER_CONTEXT) cannot use a document: Compose would resolve the path on the workstation, so those
targets refuse one. A denied request is answered with the same fixed refusal as a below-threshold
cohort, so a caller cannot use it to probe the trust's configuration; the rule id that caused the
denial goes to the trust's own log.
make -C trust down-trust KIT=GSTT # if a previous GSTT stack is running
make -C trust up-trust KIT=GSTTOn an on-prem host provisioned by the on-prem playbook, prefix these with sudo -E — the login
user is deliberately not in the docker group (see
trust/deploy/ansible/README.md). Dev workstations are
unaffected.
The trust-api container authenticates with its TRUST_API_KEY and posts a heartbeat to POST /trust/heartbeat (no name segment — the hub resolves the trust's identity from the API key). The Connection status page flips the row online within ~30s.
The plaintext keys aren't recoverable — only the hash is on disk. If you didn't save the kit, the only options are:
- delete the row (
DELETE FROM trust WHERE name='<name>';againstflip-db, after freeing the slot:UPDATE fl_kit_slot SET assigned_to_trust_id = NULL, assigned_at = NULL WHERE assigned_to_trust_id = '<trust-id>';) and re-register, or - re-register with
make register-trust KIT=<CODE>, which mints fresh keys and rewrites the kit file — this also rotates the keys.
All mock trust data — the canonical OMOP tables and the per-project DICOM sets — comes from one
public Hugging Face dataset,
aicentreflip/trust-data, which
holds exactly one copy of every artefact at an unversioned path, one pair per project:
omop-csv/<project>/*.csv omop-csv/<project>/source/… dicom/<project>.tar.gz
Not every project has a dicom/ set. A project cut from open data — spleen_project and
brain_mri_project, both from the Medical Segmentation Decathlon — publishes only its tables and
the metadata table they were built from; its DICOMs are regenerated locally by a deterministic
converter (fl-tutorials/datasets/, FLIP#1221) and seeded from that tree
(make -C fl-tutorials seed-<dataset> KIT=<CODE>, which drives seed-omop … CANONICAL_DIR= and
seed-orthanc … DICOM_SOURCE= TABLES_DIR= here). cxr_project and prostate_project still ship
one, which is why the default PROJECTS a bring-up seeds is cxr_project alone.
A trust is stood up by seeding those into its running omop-db and Orthanc (up-trust does it,
see below); there are no volume snapshots to download. A data version is a git tag on that
dataset, and .data_version in this directory pins one — a single pin for OMOP
and Orthanc together, because a tag describes the whole dataset state. Every consumer (seed-omop /
seed-orthanc, the spleen label uploader, the Ansible plays, the Helm chart) fetches
resolve/<tag>/<path>, so an old version stays reachable at its tag forever and is never
duplicated as a second directory or a suffixed filename. HF_TRUST_DATA_REVISION overrides the
tag everywhere (main to work against files uploaded but not tagged yet; a sha to freeze one).
Publishing a new version is one commit that replaces exactly the artefacts that changed, plus one
tag. An existing tag is never moved, so a published version means one set of bytes for good. The
commit and the tag are separate calls to the Hub: if the second fails, the bytes sit on main with
nothing pinning them and no consumer resolving them — re-run the same command to finish it.
uv run orthanc/publish_dicom.py --project … --revision main --out orthanc/dist/dicom/<project>.tar.gz
make publish-trust-data VERSION=20261001 DRY_RUN=1 \
OMOP_CSV=omop-db/data/canonical DICOM=orthanc/dist/dicom/<project>.tar.gz [CARD=…]
make publish-trust-data VERSION=20261001 … # for real; then set .data_version to 20261001
make publish-trust-data VERSION=20261001 OMOP_CSV=… DELETE=dicom/spleen_project.tar.gz # retire a re-hosted setDELETE=<path in repo> removes a file from main in the same commit (earlier tags keep it) — how
dicom/spleen_project.tar.gz went when spleen moved to local regeneration.
(hf auth login with write access to the dataset is needed.) Bumping .data_version is what
moves a checkout: the next up-trust (its ensure-seeded step) re-seeds the trust's projects at
the new tag — OMOP rows replaced project by project, PACS studies cleared and re-uploaded — and
every seed/enrichment run reads at the new tag.
up-trust starts omop-db and Orthanc on empty, pre-created volumes and then runs
ensure-seeded, which loads PROJECTS (default cxr_project — the projects that publish a DICOM
set, see above) from the dataset at
the pinned version. Each half leaves a marker beside its store — <omop dir>/../.seeded and
<orthanc parent>/.<storage dir>.seeded — recording projects, partition and version; a marker
that matches means nothing to do, so a second up fetches and uploads nothing and the seeded
volumes simply persist on the host. A first bring-up on a fresh host is the one slow step:
roughly 2 GB of DICOM per trust posted through Orthanc's REST API (a few minutes); the OMOP half
takes seconds. The DICOM vocabulary is loaded with the rows; the licensed core vocabulary is still
the separate credentialed make -C trust/omop-db load-omop-vocab step.
make -C trust ensure-seeded KIT=GSTT PROJECTS="cxr_project" # what up-trust runs; safe to repeat
make -C trust seed KIT=GSTT PROJECTS="prostate_project" # add a project, unconditionallyA host seeded before FLIP#1187 re-seeds once. The PACS marker used to live inside the
storage directory (<storage dir>/.seeded) and now sits beside it, because the directory
itself is owned by Orthanc's uid. The new reader does not look at the old path, so the first
up-trust after this change finds no marker and re-seeds: the DICOM cache under
trust/orthanc/volumes/dicom/ is keyed by data version and survives, so nothing is
re-downloaded, but every instance is re-posted (Orthanc answers AlreadyStored) — a few
minutes, once. The stale .seeded inside the storage directory can be deleted.
Recovering a half-loaded DICOM vocabulary: the loader skips itself when its scaffolding
concept is present, and that concept is committed before the bulk load, so a run killed in
between leaves a database that reports itself loaded and is skipped for good.
make -C trust/omop-db load-dicom-vocab FORCE_DICOM_VOCAB=1 reloads over it. The same
variable reaches the Kubernetes hook (trustData.seed.forceDicomVocab) and the EC2 play
(make -C deploy/providers/AWS seed-trust-data KIT=<CODE> HUB=<hub> FORCE_DICOM_VOCAB=1).
make -C trust seed KIT=<CODE> loads the OMOP source_trust partition matching the trust's FL
kit slot — partition 1 into the trust holding Trust_1, and so on. That is a default, not an
invariant: the kit slot and the OMOP partition are separate axes that happen to line up on the
shipped GSTT/KCH roster. Slots are claimed from a pool in registration order, so a re-registered
trust, or a third one, can hold slot 2 while the data meant for it is partition 1.
Seeded the wrong way round nothing complains — the OMOP rows and the PACS studies are selected by
the same column, so they still agree with each other; they just belong to another institution.
Override with SOURCE_TRUST when the two differ:
make -C trust seed KIT=<CODE> SOURCE_TRUST=1 # slot stays as assigned; load partition 1To seed the whole shipped dev roster in one go — seed KIT=GSTT then seed KIT=KCH, same
projects, same partition-by-slot default:
make -C trust seed-trusts PROJECTS="cxr_project"seed itself is just seed-omop + seed-orthanc; run either half alone with
make -C trust seed-omop KIT=<CODE> PROJECTS=… / seed-orthanc KIT=<CODE> PROJECTS=….
SOURCE_TRUST moves only the partition. The trust's volumes, ports and the .seeded marker stay
keyed to its kit slot, which is why it exists as its own variable rather than an override of
TRUST_NUM.
make -C trust unseed KIT=<CODE> PROJECTS="…" takes the listed projects out of a running trust —
OMOP rows by the person ids and PACS studies by the accessions the tables at
HF_TRUST_DATA_REVISION (or CANONICAL_DIR= / TABLES_DIR=) name — and loads nothing; every other
project stays. It exists for a project whose identities were re-cut (spleen at the FLIP#1221 tag:
every person id, accession and UID changed), where seed-omop's default --clean projects would
delete by the new ids and leave the old rows beside them:
make -C trust unseed KIT=GSTT PROJECTS=spleen_project HF_TRUST_DATA_REVISION=20260911 # the cut the trust holds
make -C fl-tutorials seed-spleen KIT=GSTT # the new cut, from the local treeseed-omop CLEAN=all is the blunt alternative (every project's rows go first); populate on the
build stack is the same loader with that mode.
See dedicated README under omop-db/README.md for instructions to populate the database.
make up (and make up-trust KIT=<name>) brings up that trust's XNAT automatically — it is no longer a separate step. See the dedicated README under xnat/README.md for standalone XNAT management and debugging.
MONAI Label adds AI-assisted annotation to the XNAT OHIF viewer: a MONAI Label menu in the viewer's Masks panel that runs a segmentation model over the scan on screen and lets a user correct the result interactively.
Prerequisite: the XNAT OHIF Viewer plugin — the MONAI Label panel lives inside the viewer and registration targets its
/xapi/ohifaiaaendpoint. FLIP ships the plugin by default in every deployment mode (see the plugin table in xnat/README.md), so there is no extra plugin step.
It is off by default — it needs an NVIDIA GPU on the trust host and pulls a large image, so a trust that does not want it is unaffected. Enable it per trust:
make up-trust KIT=<CODE> MONAI_LABEL=true # or set MONAI_LABEL=true in trust/.env.<CODE>.<env>The trust's kit file carries the rest of the settings:
| Variable | Default | Notes |
|---|---|---|
MONAI_LABEL |
false |
Master switch. Requires NUM_AVAILABLE_GPUS>0. |
MONAI_LABEL_PORT |
8030 |
Host port the server listens on. |
MONAI_LABEL_MODELS |
deepedit |
Comma-separated radiology models. all loads nine, each downloading its own weights. |
MONAI_LABEL_PROJECTS |
(empty) | XNAT projects the server may read. Empty means every project on this trust. |
MONAI_LABEL_PUBLIC_URL |
Dev: http://localhost:$MONAI_LABEL_PORT; required in production |
See below — this one matters. |
MONAI_LABEL_SHM_SIZE |
8gb |
Shared memory for dataloader workers. |
MONAI_LABEL_BIND_HOST |
127.0.0.1 |
Host interface MONAI_LABEL_PORT is published on. Loopback by default — see below. |
MONAI_LABEL_AUTH_ENABLE |
false |
MONAI Label's own auth. Upstream default; enabling it needs an OAuth realm FLIP does not run. |
MONAI_LABEL_PUBLIC_URL is the setting people get wrong. XNAT stores this URL and hands it
to the OHIF viewer, which calls it from the clinician's browser — XNAT never proxies the
request. So it must resolve on the clinician's machine: a Docker service name, or 0.0.0.0,
will not work. Production Compose refuses an empty value. The development default only works
when the browser runs on the trust host itself. Two
further consequences on a real trust:
-
If XNAT is served over HTTPS, the browser blocks a plain-
http://MONAI Label URL as mixed content. Terminate TLS in front of MONAI Label, or serve it through the XNAT nginx so it is same-origin. -
The MONAI Label API is unauthenticated and holds this trust's XNAT service-account credentials. Anyone who can reach the port can enumerate every study the server can see, download identifiable DICOM, and write labels back to XNAT as an admin.
MONAI_LABEL_AUTH_ENABLEis upstream's own switch, but turning it on requires an OAuth realm FLIP does not run — so the containment is the network, not the app.Because of that,
MONAI_LABEL_BIND_HOSTdefaults to127.0.0.1: out of the box the port is published on loopback only, and the trust host gains no listener reachable from the network (the trust deployment model is otherwise strictly outbound — see Deployment Architecture). That default is deliberately not browser-usable from another machine. To give clinicians access, pick one:- Serve it through the XNAT nginx (recommended) — same-origin with XNAT, so it also resolves the mixed-content problem above, and it inherits XNAT's TLS.
- Set
MONAI_LABEL_BIND_HOSTto an interface you have explicitly firewalled to the clinical network, and setMONAI_LABEL_PUBLIC_URLto match.
Each user must switch the panel on themselves, once per browser: in the viewer, Options →
Preferences → Experimental, tick MONAILabel Tools. A MONAI Label entry then appears in
the Masks panel. Registering the server is automatic, but this flag is not, and it cannot be
defaulted from the server — the viewer keeps it in the browser's localStorage. Until it is
ticked, a perfectly working server is simply absent from the viewer; that is the first thing to
check when it "isn't showing up", followed by GET /xapi/ohifaiaa/servers returning the URL and
that URL opening in the browser.
Known stock-viewer defects (all client-side — the server returns valid masks throughout, verifiable in the monailabel container's logs):
-
Clicks do nothing in DeepGrow/DeepEdit after the panel first connects: it activates the NVIDIA AIAA click tool instead of MONAI Label's, so left-clicks on the image never reach the server. Pick Mask ▾ → MONAILabel again once the panel shows the server; clicks then prompt the model (Ctrl+click for a negative point).
-
"Empty mask was returned by the model run" from every interactive model (
sam_2d,deepgrow_*) whiledeepedit_segstill works: the tab's segment state has gone stale — reload the tab. -
Interactive (DeepGrow-type) results always land in the first segment, regardless of which segment is selected — the viewer routes the mask through an active-segment index that segment selection does not update (observed in the OHIF viewer frontend 3.7.2 that ships inside XNAT OHIF plugin 3.8.0; the 3.7.0 frontend did not have the reworked segment store — note the frontend and plugin carry separate version numbers). Until fixed upstream, treat SAM/DeepGrow as single-target — rename the first segment afterwards — and use
deepedit_segfor multi-organ work.
A recorded walkthrough of all this (enable, run, export, convert) can be regenerated on any trust with
make -C trust/monailabel-demo demo-video; see monailabel-demo/README.md.
A mask saved in the viewer lands in XNAT as a DICOM-SEG assessor, which FL training cannot
read — it consumes NIfTI. trust/xnat/xnat/config/configure-export-mask.sh closes that gap: it
registers an export_mask container-service command plus a site-wide event subscription on
image-assessor creation, so every saved segmentation is converted to NIfTI and uploaded back to
the session automatically. Nothing upstream does this step.
Two things to know:
- It is registered only when
MONAI_LABEL=true.make xnat-configuregates it (seexnat-configure-export-maskintrust/xnat/Makefile), so a trust without MONAI Label gets neither the converter command nor the subscription. If you enable MONAI Label on a trust that was already up, re-runmake -C trust/xnat xnat-configure KIT=<CODE>(or bring XNAT up again) — otherwise the annotation UI works but masks are never converted. - The converter is FLIP-built:
ghcr.io/londonaicentre/xnat-export-mask:<version>, fromxnat/export-mask/, pinned by an immutable version tag inexport_mask_command.json. It runs on the trust network with the XNAT credentials the container service injects, reads the assessor's DICOM-SEG, resamples the mask onto the session's NIfTI scan when there is exactly one, and uploads the.nii.gzinto the assessor'sSEGresource. Likexnat-dcm2niix, the GHCR package must be public (see xnat/README.md).
Notes:
- SAM is always on, independently of
MONAI_LABEL_MODELS: the radiology app registerssam_2dandsam_3d(interactive click-to-segment) whenever thesam2package is importable. Their checkpoint is a further ~900 MB fetched from HuggingFace on first start, into the same persisted model directory. Upstream's--conf sam2 falsewould disable them, butentrypoint.shpasses only--conf models, so there is currently no way to turn SAM off from the kit file — it would need an entrypoint change. - The server reads DICOM straight off this trust's XNAT archive (mounted read-only), falling back to HTTP downloads only for scans it cannot resolve there.
- Pretrained weights are fetched on first start and persisted in the
monailabel-modelsvolume, so a container recreate does not re-download them. - A deployed trust (
HUB=stag|prod) runs the publishedghcr.io/londonaicentre/monailabelimage (built bydocker_build_monailabel.yml; dev builds locally fromtrust/monailabel/).
If you are operating a trust on a host that does not have the hub's
.env.<env> file (e.g. an on-prem deployment or a third-party trust), you
need only your trust's kit file (trust/.env.<CODE>.<env>).
The root onboard-onprem-trust, up-onprem-trust and upgrade-onprem-trust
wrappers default to HUB=prod. Their readiness checklist checks the same file
as deployment: trust/.env.<KIT>.<env>, resolved by deploy/env_mode.mk's kit_file.
Its heading and fix hints name that selected path. Pass HUB=stag to the checklist and deployment when joining the staging hub.
-
The hub admin scaffolds and registers your kit (
make new-trust TRUST_CODE=<CODE> TRUST_NAME="..." HUB=prodthenmake -C deploy/providers/AWS register-trusts KIT=<CODE> HUB=prodandmake sync-trust-kit KIT=<CODE> HUB=prod). The result is a complete kit file attrust/.env.<CODE>.prodcontaining credentials, the AES key, the hub URL, image tags, and a host-local profile. -
The hub admin transmits the file to you out-of-band (SCP-via-SSM for an EC2 trust; encrypted channel for on-prem).
-
Drop it at
trust/.env.<CODE>.prodin your checkout. -
Fill in the Trust-local credentials block (Orthanc / OMOP / XNAT / Grafana passwords) — these are your secrets, the hub never sees them.
-
Start the stack:
- EC2 trust:
make -C trust up-trust-ec2 KIT=<CODE> HUB=prod(stop it withmake -C trust down-trust-ec2 KIT=<CODE> HUB=prod) - On-prem trust:
sudo -E make -C trust up-trust KIT=<CODE> HUB=prod(sudo: the provisioned login user is deliberately not in the docker group;-Ekeeps$HOMEso root's docker reuses your GHCR login) - Laptop-against-prod:
make -C trust up-trust KIT=<CODE> HUB=prod(no sudo — your workstation isn't provisioned by the on-prem playbook)
HUBnames the hub you are joining, and with it the kit-file suffix the trust Makefiles read:prod→.env.<CODE>.prod,stag→.env.<CODE>.stag. Both run the prod compose and stack files; onlyHUBunset is the development stack.On-prem,
up-trustrunsensure-seededunconditionally, the same way it does in dev: a store with no.seededmarker beside it is seeded with the listed mock projects (anonymous Hugging Face download, no hub credentials) — OMOP via--clean projects, which replaces only those projects' own rows, and Orthanc by posting their studies. A real on-prem operator who pointsOMOP_DATA_DIR/ORTHANC_STORAGE_DIRat real data therefore gets the mock projects loaded alongside it on first bring-up; there is no switch to turn the seed off yet.up-trustis the first-install verb on every path, on-prem included: its XNAT step runsxnat-reset, which wipes the XNAT archive and database, and itsensure-seededstep re-seeds the listed projects whenever the seed markers differ from the kit (a.data_versionbump or changedPROJECTS). A real on-prem operator runsup-trustonce; every later move to a release goes throughupgrade-trust(below), neverup-trustorrestart-trust. - EC2 trust:
git fetch --tags origin && git checkout vX.Y.Z # the checkout first: compose files + this verb come from it
sudo -E make upgrade-onprem-trust KIT=<CODE> # → the release the hub runs
sudo -E make upgrade-onprem-trust KIT=<CODE> TAG=vX.Y.Z # → a named releaseRuns the readiness checklist, resolves the target (the hub's /api/health
version, or TAG=), refuses a release tag unless this checkout is at it
(ALLOW_CHECKOUT_DRIFT=1 overrides — testing a branch), asks you to confirm
site <current> → target <release>,
writes the tag into your kit's Hub-shared block, pulls, recreates what changed,
and upgrades XNAT in place (database dump first, no reset); it never runs
ensure-seeded, so the OMOP / Orthanc stores are left as they are. Before the kit is touched
it asks the registry for every image at the target and refuses a tag any of them was
never built at (each sha- build is path-filtered; a release tag builds them all —
for a sha- move, FL_TAG= holds the FL client at its own build and OMOP_DB_TAG /
ORTHANC_TAG in the kit do the same for those data services). XNAT_TAG pins all three
XNAT images together; their builds no longer share sha- tags. Set it to a release tag
(or stag/prod) published for all three images when moving a site to a sha- tag.
FORCE=1 allows a downgrade, YES=1 skips the prompt. The full runbook — ordering, refreshed kits,
Kubernetes and EC2 variants, rollback — is
docs/source/sys-admin/admin-upgrading-sites.rst.
When the hub admin rotates a shared value (AES key, FL backend, image tag),
they will run make sync-trust-kit KIT=<CODE> HUB=prod on their side. That
produces an updated kit file with the new Hub-shared block; credentials are
preserved. The updated file is transmitted to you using the same out-of-band
channel. Replace only the Hub-shared block in your local copy (your
Host-local profile and Trust-local credentials stay yours), then re-apply:
sudo -E make upgrade-onprem-trust KIT=<CODE> YES=1That recreates only the containers whose configuration changed and leaves the
data alone. restart-trust would also work for the API containers but re-runs
up-trust's XNAT reset — don't. A stale block is what the checklist's
Hub-shared block current row detects: trust-api compares its AES key with the
hub's on every heartbeat and reports the mismatch on its /health, so a rotated
key shows up there before it shows up as every task failing to decrypt.
The trust-api and data-access-api integration suites run against a throwaway Compose stack — vanilla Postgres seeded from a small OMOP fixture plus a freshly-built data-access-api. The stack is defined in deploy/compose/compose.test.yml and brought up by Testcontainers inside session-scoped pytest fixtures, so a single test invocation is enough — no make up first.
# trust-api: drives ``handle_cohort_query`` end-to-end through trust-api → data-access-api → omop-db
make -C trust-api integration_test
# data-access-api: hits ``/cohort`` endpoints directly against the same stack
make -C data-access-api integration_testThe seed data lives in trust-api/tests/integration/fixtures/omop_seed.sql and follows the MI-CDM shape — image_occurrence joined to concept for modality lookups. Counts there match the assertions in test_cohort_query.py and test_cohort_endpoint.py (16 patients, 24 image occurrences). When adjusting the seed, update both. The trust-api side mocks nothing on the HTTP boundary — the only stub is an in-process HTTP server that catches the trust-api → flip-api callback (B3 is intentionally scoped to exclude the hub leg, see issue #369).
Both Make targets are also wired into CI via dedicated jobs in test_trust_trust_api.yml and test_trust_data_access_api.yml.