Skip to main content

Local Clinic Deployment (Docker-in-Docker)

This page explains how to deploy a FLNet clinic node locally using Docker-in-Docker (DinD). This approach packages the entire clinic stack — all services, configuration, and pre-pulled Docker images — into a single self-contained Docker container. It is designed for clinic environments where:

  • Internet access is restricted or unavailable at runtime.
  • A simple, portable deployment is required (one container, one command).
  • The clinic node must connect to an existing global server.

How DinD Works

The clinic stack runs inside a Docker container that itself runs a Docker daemon (Docker-in-Docker). The inner daemon starts a docker compose stack with all the clinic services. This means:

  • All clinic service images are bundled into the DinD image at build time — no internet needed at runtime.
  • The inner compose stack creates its own isolated Docker network inside the DinD container.
  • Only port 80 is exposed from the DinD container to the host.
Host machine
└── DinD container (clinic-dind)
├── dockerd (inner Docker daemon)
└── docker compose stack (inner)
├── local-learning-api
├── local-learning-api-db (PostgreSQL)
├── orch-api
├── controller ──────────────────────────▶ Global Relay Server (TCP 9150)
├── instance-manager-frontend
├── keycloak
├── keycloak-postgres
└── reverse-proxy (Nginx :80) ◀──── Host port

Services at a Glance

ServiceImagePurpose
local-learning-apilocal-learning-apiREST API for the clinic node — manages local data, connects to global server
local-learning-api-dbpostgres:17.5PostgreSQL database for the local learning API
orch-apiorch-apiOrchestration API — spawns federated learning tool containers
controllercontrollerFLNet controller — connects to the global relay server and manages federated runs
instance-manager-frontendlocal-fl-netWeb frontend for the clinic node
keycloakquay.io/keycloak/keycloak:26.5Identity provider (authentication)
keycloak-postgrespostgres:17.5PostgreSQL database for Keycloak
reverse-proxy-unencryptednginxNginx reverse proxy, binds on :80

Prerequisites

On the build machine (internet access required):

  • Docker installed and running
  • Access to the GitLab container registry (gitlab.cosy.bio:5050) — you need valid credentials
  • The clinic-dind/ directory from the deployment repository

On the clinic machine (target deployment):

  • Docker installed
  • No internet access required at runtime
  • Port 80 (or a custom port) available for the clinic frontend

Step 1: Build the DinD Image

The build process pulls all required images, saves them into a .tar archive, and bundles everything into a single Docker image.

From the clinic-dind/ directory, run:

./build-local-dind.sh

This script does the following:

  1. Reads all image: entries from docker-compose.yml
  2. Pulls each image (docker pull)
  3. Saves them all into images.tar (docker save)
  4. Builds the DinD Docker image tagged feddb-dind
  5. Removes the temporary images.tar

The resulting image feddb-dind contains everything needed to run the clinic stack offline.

Tip: If images.tar already exists, the pull and save steps are skipped. Delete it to force a fresh image pull.

To only prepare the image archive (without building the DinD image):

./prepare-local-dind.sh

Step 2: Prepare Clinic Data Files

The clinic node requires two data files that are mounted into the DinD container at runtime:

FilePurpose
clinic.csvThe clinic's patient/measurement dataset in the expected CSV format
us-130-clinics.jsonConnector configuration describing how the ETL process should read the CSV

These are mounted read-only into /connectors/ inside the DinD container. The inner local-learning-api service reads them from there.


Step 3: Run the DinD Container

Use the provided run script:

./run-local-dind.sh

By default this exposes the clinic frontend on port 4601 of the host.

Configurable environment variables

You can override any of these before running:

HOST_PORT=4601 \
DEPLOYED_ON_ADDRESS=http://localhost:4601 \
COMPOSE_PROJECT_NAME=fl-net-clinic1 \
CLINIC_DATASET_PATH=/path/to/your/clinic.csv \
CLINIC_CONNECTOR_PATH=/path/to/us-130-clinics.json \
./run-local-dind.sh
VariableDefaultDescription
HOST_PORT4601The port on the host machine that maps to the clinic frontend (:80 inside DinD).
DEPLOYED_ON_ADDRESShttp://localhost:{HOST_PORT}The full URL the clinic frontend is reachable at. Used by Keycloak for redirect URLs.
COMPOSE_PROJECT_NAMEfl-net-clinic1Docker Compose project name for the inner stack. Change this when running multiple clinics on the same host.
CLINIC_DATASET_PATH{repo}/deployment/example-data/us-130/clinics/clinic_001.csvAbsolute path to the clinic CSV dataset on the host.
CLINIC_CONNECTOR_PATH{script_dir}/us-130-clinics.jsonAbsolute path to the connector config JSON on the host.

What the run script does

The run-local-dind.sh script runs the feddb-dind image with:

  • --privileged — required for the inner Docker daemon
  • --cgroupns host — required for cgroup management inside DinD
  • -v /sys/fs/cgroup:/sys/fs/cgroup:rw — cgroup access for the inner daemon
  • -v clinic-dind-docker:/var/lib/docker — persistent Docker layer cache for the inner daemon (so images survive restarts)
  • -v $CLINIC_DATASET_PATH:/connectors/clinic.csv:ro — mounts clinic data read-only
  • -v $CLINIC_CONNECTOR_PATH:/connectors/us-130-clinics:ro — mounts connector config read-only
  • -p $HOST_PORT:80 — exposes the Nginx reverse proxy

The inner Docker daemon, clinic stack, and all networking happen entirely inside this one container.


Step 4: What Happens at Startup

When the DinD container starts, entrypoint.sh runs automatically:

  1. Starts the inner Docker daemon (dockerd-entrypoint.sh)
  2. Waits up to 60 seconds for the daemon to be ready
  3. Loads pre-saved images from /compose/images.tar into the inner Docker daemon (docker load)
  4. Logs into the registry using the CI_REGISTRY_USER and CI_REGISTRY_PASSWORD environment variables — required so orch-api can later pull federated learning tool images on demand
  5. Resolves host.docker.internal — auto-detects the host IP so the inner compose services can reach the host machine if needed
  6. Writes a runtime .env — merges the baked-in .env with any environment variables passed to the DinD container
  7. Runs docker compose up with the inner compose file, attaching logs from controller, orch-api, and local-learning-api

Step 5: Configure Global Server Connection

The inner controller service connects to the global relay server. These URLs are injected at runtime via environment variables:

VariableDescription
GLOBAL_LEARNING_API_URLHTTP URL of the global learning API (e.g. https://dev.federated-learning.net/api)
GLOBAL_LEARNING_API_WEBSOCKET_URLWebSocket URL of the global learning API (e.g. wss://dev.federated-learning.net/api)
GLOBAL_SCHEMA_API_URLURL of the data modeler API on the global server
GLOBAL_RELAY_HTTP_URLHTTP URL of the relay server on the global server
GLOBAL_RELAY_TCP_ADDRESSTCP address of the relay server (e.g. dev.federated-learning.net:9150)

These are pre-set in .env to point at the staging global server. Pass them as environment variables to docker run to override:

docker run -it \
--name clinic-dind \
--privileged \
--cgroupns host \
--rm \
-e COMPOSE_PROJECT_NAME=fl-net-clinic1 \
-e DEPLOYED_ON_ADDRESS=http://localhost:4601 \
-e GLOBAL_LEARNING_API_URL=https://your-global-server.com/api \
-e GLOBAL_LEARNING_API_WEBSOCKET_URL=wss://your-global-server.com/api \
-e GLOBAL_SCHEMA_API_URL=https://your-global-server.com/data-modeler \
-e GLOBAL_RELAY_HTTP_URL=https://your-global-server.com/relay \
-e GLOBAL_RELAY_TCP_ADDRESS=your-global-server.com:9150 \
-e CI_REGISTRY_USER=your-gitlab-username \
-e CI_REGISTRY_PASSWORD=your-gitlab-pat \
-v clinic-dind-docker:/var/lib/docker \
-v /sys/fs/cgroup:/sys/fs/cgroup:rw \
-v /path/to/clinic.csv:/connectors/clinic.csv:ro \
-v /path/to/us-130-clinics.json:/connectors/us-130-clinics:ro \
-p 4601:80 \
feddb-dind

Step 6: Nginx Routing (Clinic Node)

The inner Nginx reverse proxy routes all traffic inside the DinD container:

Path prefixUpstream serviceNotes
/local-learning-api/local-learning-api:8080Local learning REST API
/auth/keycloak:8080Keycloak authentication
/instance-manager-frontend:80Clinic web frontend (catch-all)

Step 7: Initial Setup

Once the stack is running, access the clinic node at http://localhost:4601 (or your configured host port).

First login (Keycloak admin)

  1. Navigate to http://localhost:4601/auth/
  2. Log in with:
    • Username: admin (or the value of KC_BOOTSTRAP_ADMIN_USERNAME in env/keycloak-secrets.env)
    • Password: password (or the value of KC_BOOTSTRAP_ADMIN_PASSWORD)
  3. Change the admin password immediately.

Create clinic users

  1. In Keycloak, switch to the FLNet-Client realm (left sidebar → Manage Realms).
  2. Go to UsersAdd User.
  3. Make sure to tick Email verified (or configure SMTP, or disable email verification requirement).
  4. Assign the user to a group:
GroupAccess level
AdminFull access — see and edit everything
Data-Access-ManagerRead all patient data, control query and federated learning access, view all logs
Data-AdminRead, edit, and add patient data; view data-related logs

Running Multiple Clinics on the Same Host

To simulate multiple clinic nodes on a single host, run the DinD container multiple times with different project names and ports:

HOST_PORT=4601 COMPOSE_PROJECT_NAME=fl-net-clinic1 ./run-local-dind.sh

# In a second terminal:
HOST_PORT=4602 COMPOSE_PROJECT_NAME=fl-net-clinic2 \
CLINIC_DATASET_PATH=/path/to/clinic2.csv \
./run-local-dind.sh

Each DinD container runs a completely isolated inner Docker stack. The COMPOSE_PROJECT_NAME ensures Docker volume names do not collide between instances.


Persisting the Inner Docker Layer Cache

The run script mounts a named volume clinic-dind-docker at /var/lib/docker inside the DinD container:

-v clinic-dind-docker:/var/lib/docker

This volume stores the inner daemon's image and container layers. On subsequent starts, images do not need to be re-loaded from the .tar file, making restarts significantly faster.

To start fresh (e.g. after a DinD image update):

docker volume rm clinic-dind-docker

Secrets Reference

The secrets files inside the DinD image contain default development passwords. For production deployments, rebuild the DinD image with updated secret files before distributing it.

FileKey variables
env/keycloak-secrets.envPOSTGRES_PASSWORD, KC_DB_PASSWORD, KC_BOOTSTRAP_ADMIN_PASSWORD, LOCAL_LEARNING_SECRET, DATA_IMPORTER_SECRET
env/local-learning-secrets.envPOSTGRES_PASSWORD, QUARKUS_DATASOURCE_PASSWORD, QUARKUS_OIDC_CREDENTIALS_SECRET
env/orch-secrets.envPOSTGRES_PASSWORD, QUARKUS_DATASOURCE_PASSWORD, ORCH_DOCKER__GITLAB__REGISTRY_PASSWORD, PIPELINE_DOCKER_PASSWORD, PIPELINE_REPO_TOKEN

Troubleshooting

DinD container starts but frontend is not reachable

Check that the host port is not already in use and that the inner Nginx started successfully:

docker exec clinic-dind docker compose logs reverse-proxy-unencrypted

Inner Docker daemon fails to start

The host kernel must support the cgroup configuration needed by DinD:

  • Make sure you pass --cgroupns host and -v /sys/fs/cgroup:/sys/fs/cgroup:rw
  • Make sure /lib/modules is readable (-v /lib/modules:/lib/modules:ro)

Clinic does not appear in the global server

Check that the controller service inside DinD can reach the global relay TCP address:

docker exec clinic-dind docker compose logs controller

Verify the GLOBAL_RELAY_TCP_ADDRESS is set correctly and that the relay TCP port is reachable from the clinic host.

Registry login fails / tool images can't be pulled

The CI_REGISTRY_USER and CI_REGISTRY_PASSWORD environment variables must be passed to the DinD container. These are used at startup to authenticate the inner Docker daemon with the GitLab registry (so orch-api can later pull federated learning tool images).

Multiple clinics: port or volume conflicts

Ensure each clinic run uses a unique HOST_PORT and COMPOSE_PROJECT_NAME. The COMPOSE_PROJECT_NAME prefixes all volume names inside Docker to avoid collisions.