First-time setup for running Yggdrasil locally with Docker Compose.
- Docker and Docker Compose
- A Docker network shared by everyone running this stack on the host (see below)
Two infrastructure pieces sit outside the Django app's own code and are easy to overlook:
- Redis — started as the
redisservice in docker-compose.yml (sodocker compose upbrings it up for you), but it isn't just an internal cache: it's the Celery broker the runner worker consumes jobs from (see docs/runners.md). It is published on loopback only; keep it that way. If you changeREDIS_PASSWORD, updateCELERY_BROKER_URLin.env.workertoo. - Garage (or any S3-compatible object store, e.g. MinIO) — not part of
docker-compose.ymlat all. It's a fully external service that must already be running and reachable from theyggdrasil-web-$DOCKER_SUFFIXcontainer atOBJECT_STORAGE_ENDPOINT_URL(.env.exampledefaults tohttp://garage:3900). Make sure that container can actually resolve/reach thegaragehost — either join the same Docker network Garage is on, or pointOBJECT_STORAGE_ENDPOINT_URLat a routable address — otherwise uploads/exports will fail with object storage errors (check/api/processing/health/, see docs/running.md).
Every container, network, and project name in docker-compose.yml is suffixed with $DOCKER_SUFFIX, e.g. yggdrasil-web-$DOCKER_SUFFIX, app-net-$DOCKER_SUFFIX. This lets multiple stacks (your dev instance, a teammate's, production) run on the same host without colliding.
Pick something unique to you, e.g. dev-yourname, or prod for the production deployment.
cp .env.example .envEdit .env and set at minimum:
DOCKER_SUFFIX— the value you picked aboveUID/GID— your host user/group id (id -u/id -g), so files written by the container are owned by youSECRET_KEY— any random string for DjangoMYSQL_DATABASE,MYSQL_USER,MYSQL_PASSWORD,MYSQL_ROOT_PASSWORDREDIS_PASSWORDRUNNER_API_TOKENS— token(s) the external runners will use
docker-compose.yml always reads from .env in the repo root (env_file: .env), so make sure that's the file you edited.
The app-net-$DOCKER_SUFFIX network is declared as external: true, meaning Compose expects it to already exist — it won't create it for you:
docker network create app-net-$DOCKER_SUFFIXYou also need the proxy-net external network (shared reverse proxy network), if it doesn't already exist on the host:
docker network create proxy-netSee running.md for day-to-day commands. The short version:
docker compose --env-file .env up -d --buildThis builds the web image, then starts the app services (web, beat,
maintenance-worker, db, and redis). The runner-worker is also defined in
the compose file, but it is deliberately not attached to app-net-$DOCKER_SUFFIX;
configure .env.worker, the SSH key, and the known-hosts mount before relying on
it for SLURM dispatch.
entrypoint.sh runs migrate --noinput on container start by default. If your stack shares a database with another stack that owns migrations, opt out with AUTO_MIGRATE=0 in .env and run migrations manually:
export DOCKER_SUFFIX=YOUR-DOCKER-SUFFIX
docker exec -it yggdrasil-web-$DOCKER_SUFFIX python manage.py migrateThe container serves the Django ASGI application with Uvicorn (ASGI_WORKERS and
ASGI_KEEP_ALIVE are tunable via .env). For local development with auto-reload,
set RUN_DEV_SERVER=1; the entrypoint will run Uvicorn with --reload so the live
transcription WebSocket remains available.
Each project app ships a management command that creates its Project row and registers its Modality rows (file types it accepts). The database is empty without this — uploads will fail until it's run, since Patient.modalities and upload forms validate against existing Modality records.
For local development, run seed_dev. This single command seeds all projects and modalities, creates a superuser account (admin / admin), configures the required ProjectAccess roles, and creates initial demo patients:
docker exec -it yggdrasil-web-$DOCKER_SUFFIX python manage.py seed_devYou can then log in at http://localhost:8000 with:
- Username:
admin - Password:
admin
For production deployments or fine-grained seeding, run the individual commands:
# Maxillo: CBCT, IOS, intraoral photos, teleradiography, panoramic, raw zip
docker exec -it yggdrasil-web-$DOCKER_SUFFIX python manage.py create_maxillo_modalities
# Brain: reuses the same Patient/Modality model under its own project namespace
docker exec -it yggdrasil-web-$DOCKER_SUFFIX python manage.py setup_brain_modalities
# Laparoscopy: video modality
docker exec -it yggdrasil-web-$DOCKER_SUFFIX python manage.py setup_laparoscopy_modalities
# Urology: MRI, WSI digital pathology, confocal
docker exec -it yggdrasil-web-$DOCKER_SUFFIX python manage.py setup_urology_modalitiesThese are idempotent (get_or_create + update) — safe to re-run after upgrades that add/change modalities.
To create an admin superuser manually (e.g., in production where seed_dev refuses to run when DEBUG=False):
docker exec -it yggdrasil-web-$DOCKER_SUFFIX python manage.py createsuperuserMicrophone captions stream through an authenticated Yggdrasil WebSocket relay.
Configure the upstream shared token in .env; it is never exposed to browsers:
WHISPER_WS_URL=wss://155.185.48.254:9097/ws
WHISPER_API_TOKEN=replace-with-the-server-token
WHISPER_CA_CERT=/app/certs/whisper-live.pemThe tracked certificate pins the current self-signed Live Whisper certificate.
Verify its SHA-256 fingerprint before deployment:
C8:34:A6:FC:93:65:E8:76:B6:11:83:0F:38:C6:12:D7:3E:E9:45:D4:07:F3:48:CB:EC:30:E0:21:50:F3:70:F8.
It expires on 23 July 2027 and must be replaced when the upstream certificate is
rotated. Connect by 155.185.48.254, not pdor.ing.unimore.it, because the
current certificate contains the IP address but not that DNS name.
The public reverse proxy must pass WebSocket upgrade requests for
/ws/live-transcription/ to the web container. The standard nginx-proxy setup
used by docker-compose.yml handles WebSocket upgrades automatically.
The Assist tool in laparoscopy Annotation Mode turns clicked points into masks using a
model on the DjangoWorker GPU worker. The browser never reaches the worker. Its WebSocket
(/ws/assist/…) ends at Yggdrasil, which opens the worker session itself. Leave both
values empty to hide the tool.
# Base URL, no path. Yggdrasil calls <url>/api/session/... and opens <url>/ws/session/...
ASSIST_WORKER_URL=https://gpu-worker.example.org
# The worker's WORKER_API_TOKEN
ASSIST_WORKER_TOKEN=replace-with-the-worker-token
# ASSIST_CONNECT_TIMEOUT=10- Where the worker is. Behind a reverse proxy (which must route
/api/sessionand/ws/sessionto it), use its publichttps://URL; the socket then useswss://. If it shares a Docker network withweb, use its service address there, e.g.http://assist-worker:8000. Onlyhttp(s)URLs are accepted, and certificates are checked against the system CAs. - The worker gets a link, not storage credentials. For each session Yggdrasil signs a
10-minute download link to that one video, against its own
OBJECT_STORAGE_ENDPOINT_URL. So that endpoint must be reachable from the worker, and the worker'sVIDEO_SOURCE_ORIGINSmust list its origin (scheme://host[:port], e.g.https://s3.example.org). The worker refuses links to any other origin and does not follow redirects. One worker can serve several deployments by listing each one's storage origin. - The project. The tool also needs the
assisted_segmentationannotation method on the project. Migration0063enables it on every laparoscopy project, andsetup_laparoscopy_modalitiesdoes the same for a new one. - After changing
.env, recreateweb(docker compose up -d web); a restart does not re-readenv_file. The page shows the tool as Unavailable if the worker cannot be reached.