MSV-med is a FastAPI service for working with DICOM studies in Orthanc. It includes asynchronous Celery jobs, PostgreSQL metadata and vector search, and Prometheus request metrics.
- Features
- Architecture
- API & Services
- AI Indexing and Search
- DICOM Integration, Privacy, and Security
- Getting Started
- Configuration
- Running the Stack
- Desktop GUI
- Service Logs
- Publishing
- Requirements
- JWT login, role-based access control, and legacy bearer-token compatibility.
- Redis-backed request rate limiting.
- DICOM upload validation with file-count and request-size limits.
- Asynchronous DICOM upload, PACS forwarding, retry, and failed-instance workflows.
- Three isolated Celery queues for DICOM uploads, PACS forwarding, and demo jobs.
- Orthanc study, series, and instance browsing through the API and GUI.
- PACS configuration management and administrator connectivity checks.
- DICOM anonymization with identifying-tag removal, private-tag removal, and UID remapping.
- PostgreSQL metadata storage with pgvector embeddings.
- Cosine, Euclidean, PostgreSQL full-text, and hybrid search strategies.
- Idempotent study ingestion into the metadata and vector index.
- Persistent jobs, progress tracking, retries, failed-instance storage, audit logging, and worker inspection.
- Liveness, readiness, dependency health checks, and Prometheus request metrics.
- Dedicated Alembic migration job for repeatable Docker startup.
- Versioned
/api/v1REST routes with compatibility aliases for existing clients. - OpenAPI, Swagger, ReDoc, environment templates, Docker development overrides, and GUI container support.
flowchart TD
Client(["Client\ngui.py · Swagger · curl"])
subgraph API ["FastAPI — api:8000"]
Auth["JWT Auth\n+ Rate Limiting"]
Router["Routers\n/api/v1/studies\n/api/v1/jobs\n/api/v1/query\n/api/v1/admin"]
Health["Health\n/health · /metrics"]
Auth --> Router
end
subgraph Queues ["Celery Workers"]
W1["worker\nDICOM upload"]
W2["worker-forward\nPACS forwarding"]
W3["worker-demo\ndemo jobs"]
end
subgraph Storage ["Storage"]
PG[("PostgreSQL\nmetadata · jobs\nembeddings · audit")]
RD[("Redis\nbroker · rate limits\njob results")]
end
subgraph PACS ["Orthanc — orthanc:8042"]
ORT["DICOM Storage\nREST API · port 4242"]
end
Client -->|"HTTPS + Bearer / JWT"| Auth
Router -->|"enqueue job"| RD
Router -->|"read metadata"| PG
Router -->|"GET studies / instances"| ORT
RD -->|"pull task"| W1
RD -->|"pull task"| W2
RD -->|"pull task"| W3
W1 -->|"POST /instances"| ORT
W2 -->|"POST /instances"| ORT
W1 -->|"update job · save failed"| PG
W2 -->|"update job · save failed"| PG
Router -->|"results"| Client
style API fill:#1e3a5f,color:#fff,stroke:#3b82f6
style Queues fill:#3b1f5e,color:#fff,stroke:#8b5cf6
style Storage fill:#1a3a2a,color:#fff,stroke:#10b981
style PACS fill:#3a2a1a,color:#fff,stroke:#f59e0b
The normal flow is:
- A client authenticates and submits a request.
- The API validates request fields and DICOM headers.
- Long-running upload and forwarding work is persisted as a job and sent to the appropriate Celery queue.
- Workers update job progress in PostgreSQL and retain retryable failures in
failed_instances. - Orthanc stores DICOM objects; PostgreSQL stores indexed metadata and optional 384-dimensional embeddings.
- Query endpoints read PACS metadata or indexed records, depending on the operation.
PostgreSQL data is stored in the postgres_data Docker volume. Orthanc data is stored in the orthanc_data volume. docker compose down preserves both; docker compose down -v permanently deletes them. Failed DICOM instances are stored as binary data for retry, so protect the database volume as PHI.
The recommended installation is Docker Compose. It starts the complete stack:
| Service | Address |
|---|---|
| FastAPI API | http://localhost:8000 |
| Versioned API routes | http://localhost:8000/api/v1 |
| Swagger documentation | http://localhost:8000/docs |
| ReDoc documentation | http://localhost:8000/redoc |
| Orthanc web UI/API | http://localhost:8042 |
| Orthanc DICOM port | 4242 |
| PostgreSQL with pgvector | host port 5433 |
| Redis | host port 6379 |
Domain API routes are available under the stable /api/v1 prefix, for example /api/v1/auth/login and /api/v1/studies; the original unversioned paths remain available for backward compatibility.
After studies are present in Orthanc, index one study or all studies through Swagger or curl:
curl -X POST "http://localhost:8000/api/v1/query/ingestions" \
-H "Authorization: Bearer $TOKEN"
curl "http://localhost:8000/api/v1/query/search?q=chest%20CT&strategy=cosine" \
-H "Authorization: Bearer $TOKEN"The first indexing request downloads or initializes the all-MiniLM-L6-v2 embedding model. Search strategies include cosine, euclidean, fulltext, and hybrid.
Uploads require valid DICOM files with SOP Instance, Study Instance, and Series Instance UIDs. When anonymization is enabled, identifying tags and private tags are removed and UIDs are remapped consistently across the job. The current anonymization is intended for controlled development and staging workflows; validate it against your organization's approved DICOM confidentiality profile before using it with clinical data in production.
Implemented as an atomic Lua script executed inside Redis. Each request's timestamp is stored in a sorted set; on every incoming request, entries older than the window are pruned and the remaining count checked before the request is admitted. Running this logic as a single Lua script prevents race conditions where two concurrent requests could both read a count below the limit and both be allowed through. Clients are identified by a hash of their token, or by IP address if no token is present, and limits are tracked independently per endpoint.
Request 1 — allowed
allowed, remaining = rate_limit("ratelimit:/api/v1/studies:abc123", max_requests=10, window_seconds=60)
# (True, 9)Request 11 — rejected
allowed, remaining = rate_limit("ratelimit:/api/v1/studies:abc123", max_requests=10, window_seconds=60)
# (False, 0)FastAPI raises:
HTTP 429 Too Many Requests
Retry-After: 60
{"detail": "Rate limit exceeded"}Redis sorted set at the point of rejection
ZRANGE ratelimit:/api/v1/studies:abc123 0 -1 WITHSCORES
1) "a1b2c3..." 1720310400123 <- request 1, timestamp ms
2) "d4e5f6..." 1720310400891 <- request 2
...
10) "z9y8x7..." 1720310405999 <- request 10after 60s all entries expire and the window resets
- Only valid DICOM files with SOP Instance, Study Instance, and Series Instance UIDs are accepted by the API upload path.
- Uploads are limited to 100 files and 50 MiB per request.
- Forwarding and upload options support PACS credentials, anonymization, an optional examination result, and an optional webhook URL.
- Anonymization removes configured identifying tags, private tags, and remaps common UIDs consistently within a job.
- The anonymizer is a custom implementation. Not checked against legal policy. Use at your own risk.
- Audit records are stored in PostgreSQL and include request ID, actor, action, resource, outcome, and JSON details.
- Production mode requires strong API/JWT secrets and an administrator password of at least 12 characters. Network access to Orthanc, Redis, and PostgreSQL should be private.
export TOKEN=$(curl -sS -X POST http://localhost:8000/api/v1/auth/login \
-H 'Content-Type: application/json' \
-d '{"username":"admin","password":"admin123"}' \
| python3 -c 'import sys, json; print(json.load(sys.stdin)["access_token"])')
curl -i http://localhost:8000/ \
-H "Authorization: Bearer $TOKEN"
curl -s http://localhost:8000/health \
-H "Authorization: Bearer $TOKEN"
curl -s http://localhost:8000/metricsThe health response should contain "api":"ok" and "pacs_reachable":true. The metrics endpoint is intended for an internal Prometheus scrape and should not be exposed directly to the public internet.
The Docker Compose file includes development defaults, so a fresh clone can start without creating an environment file:
docker compose up --build -dFor a local test run, the built-in defaults use admin / admin123 and changeme for the legacy API secret. Change these values before exposing the service to another machine or network. You can either export variables in your shell or create an untracked .env file to override them. Do not commit .env.
The important settings are:
API_SECRET=replace_this_with_a_long_random_value
JWT_SECRET=replace_this_with_a_different_long_random_value
JWT_EXPIRE_MINUTES=60
ADMIN_USERNAME=admin
ADMIN_PASSWORD=change_this_demo_password
ORTHANC_USER=orthanc
ORTHANC_PASS=orthanc
DATABASE_URL=postgresql://msvmed:msvmed@postgres:5432/msvmed
REDIS_URL=redis://redis:6379/0
ORTHANC_URL=http://orthanc:8042Configuration reference:
| Variable | Purpose | Local default |
|---|---|---|
APP_ENV |
Runtime mode; production enables secret validation | development in Compose |
API_SECRET |
Legacy bearer-token compatibility secret | changeme |
JWT_SECRET |
JWT signing secret | changeme |
JWT_EXPIRE_MINUTES |
JWT lifetime | 60 |
ADMIN_USERNAME |
Bootstrap administrator username | admin |
ADMIN_PASSWORD |
Bootstrap administrator password | admin123 |
DATABASE_URL |
SQLAlchemy PostgreSQL connection URL | Docker service URL |
REDIS_URL |
Celery broker/result backend URL | redis://redis:6379/0 |
ORTHANC_URL |
PACS HTTP API URL | http://orthanc:8042 in Docker |
ORTHANC_USER / ORTHANC_PASS |
PACS credentials | orthanc / orthanc |
API_URL |
GUI API URL | http://localhost:8000 |
API_TOKEN |
Optional GUI bearer token; takes precedence over login | unset |
API_USERNAME / API_PASSWORD |
GUI automatic login credentials | admin / admin123 |
UI_FONT |
Optional GUI font path | platform-dependent |
DATABASE_URL, REDIS_URL, and ORTHANC_URL must use Docker service names inside containers and localhost when the process runs directly on the host.
Generate a secret on Linux with:
openssl rand -hex 32On first API startup, an administrator is created automatically if ADMIN_USERNAME does not already exist. Existing users and passwords are never overwritten.
The Compose file supplies the internal Docker addresses for DATABASE_URL, REDIS_URL, and ORTHANC_URL. Do not replace those with localhost while running the API and worker in Docker: inside a container, localhost means that same container.
From the project directory:
docker compose up --build -d
docker compose up --waitWatch startup logs:
docker compose logs -f migrate api worker worker-forward worker-demo postgres orthanc redisThe first build downloads Python packages and the embedding model, so it can take several minutes. Check container status:
docker compose psThe migration job runs once and must complete successfully before the API and workers start. The stack then runs PostgreSQL, Redis, Orthanc, the API, and three queue-specific Celery workers:
worker: DICOM upload processingworker-forward: PACS forwardingworker-demo: UI/demo jobs used for testing concurrency and job representation in the database during development.
PostgreSQL is initialized with pgvector from docker/initdb/01-enable-pgvector.sql. The migration job applies files in alembic/versions/ and exits after success; an exited migration container is expected.
To stop the stack while preserving data:
docker compose downTo stop it and delete the PostgreSQL and Orthanc data volumes:
docker compose down -vThe second command permanently deletes imported studies and indexed metadata.
The create_all() method from sql-alchemy ignores creating databases once schemas changes.After the first restart,if it sees the tables already exist, it does nothing, and the mismatch is silent until it crashes the production.
@app.on_event("startup")
def init_db():
Base.metadata.create_all(engine)Thus, for the purpose of keeping the development and eventual expansion easier I have integrated alembic as a docker migration job to prevent manual interventions (deleting the databases from Postgres) on every deploy.
The easiest Fedora/Linux option is to run the GUI on the host and the backend in Docker. Install the GUI dependencies with uv:
uv sync --extra gui
API_URL=http://localhost:8000 \
API_USERNAME=admin \
API_PASSWORD=admin123 \
uv run python gui.pyThe GUI logs in automatically with API_USERNAME and API_PASSWORD when API_TOKEN is not set. For a non-default installation, use the credentials you configured in .env.
If Fedora reports missing graphical libraries, install them with:
sudo dnf install -y mesa-libGL libX11 libXext libXrender libXcursor libXinerama libXi libXrandr libXxf86vm glfwThe GUI connects to the API, so the API and worker containers must already be running. The Docker GUI profile is also available on an X11 Linux desktop:
xhost +local:docker
docker compose -f docker-compose.yml -f docker-compose.gui.yml --profile gui up --buildIf using Wayland, running gui.py directly on the host is usually simpler than forwarding X11 from a container. The only requirement is the dearpygui library and a font. If the latter is missing the GUI will default to the system's font.
For development with live source reload:
docker compose -f docker-compose.yml -f docker-compose.dev.yml up --buildThe development override mounts src/ into the API container and enables Uvicorn reload. It is not intended for production.
For troubleshooting see:
docker compose logs api
docker compose logs worker
docker compose logs worker-forward
docker compose logs worker-demo
docker compose logs migrate
docker compose logs postgresBefore publishing, confirm that:
git status --shortcontains no.env, credential, key, or generated data files.- Production deployments set
APP_ENV=production, long uniqueAPI_SECRETandJWT_SECRETvalues, and anADMIN_PASSWORDof at least 12 characters. - Default Docker credentials are changed before exposing API, Orthanc, PostgreSQL, or Redis outside the local machine.
docker compose psshows the API, PostgreSQL, Redis, Orthanc, and all three workers as running and healthy where health checks are configured. The migration service should showExited (0).
The Docker defaults are for local development only. They are deliberately convenient for a fresh clone and are not suitable for an internet-facing deployment.
Install Docker Engine and the Docker Compose plugin. On Fedora (the distro that the program was developed on), you can use Docker's official repository:
sudo dnf -y install dnf-plugins-core
sudo dnf config-manager --add-repo https://download.docker.com/linux/fedora/docker-ce.repo
sudo dnf -y install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
sudo systemctl enable --now docker
sudo usermod -aG docker "$USER"Log out and back in once after adding yourself to the docker group. Verify the installation:
docker run --rm hello-world
docker compose versionOn another Linux distribution, install Docker Engine and Docker Compose from the Docker documentation, then run the same verification commands.
The manual setup requires:
- Python 3.11 or newer
- uv for locked dependency installation
- PostgreSQL 16 with the
vectorextension - Redis 7 or newer
- An Orthanc server
Docker is easier because it supplies PostgreSQL, pgvector, Redis, and Orthanc with the correct networking and credentials.