|
Documentation: Install a production instance

Deploy

Install a production instance

Single-host Docker Compose on a customer Linux VM. Kubernetes is out of scope.

The supported deployment model is a single-host Docker Compose install on a Linux VM provided by the customer, with optional separate GPU host(s) for inference. Multi-host Kubernetes is not covered. Air-gapped installs are partially covered by a compose override (see Configuration).

1. Get the code and prepare environment files

mkdir pacs-ai && cd pacs-ai
git clone https://github.com/HeartWise-AI/pacs-ai-backend.git
git clone https://github.com/HeartWise-AI/PACS-AI.git

# execution plane (private repository; access is granted per engagement)
cd pacs-ai-backend && git clone https://github.com/HeartWise-AI/cardio-agent.git && cd ..

cp pacs-ai-backend/.env.example                 pacs-ai-backend/.env
cp PACS-AI/platform/app/.env.example            PACS-AI/platform/app/.env
cp pacs-ai-backend/api-pacs/.env.example        pacs-ai-backend/api-pacs/.env
cp pacs-ai-backend/orthanc/.env.example         pacs-ai-backend/orthanc/.env
cp pacs-ai-backend/nginx/.env.example           pacs-ai-backend/nginx/.env
cp pacs-ai-backend/cardio-agent/.env.example    pacs-ai-backend/cardio-agent/.env
cp pacs-ai-backend/postgresql/.env.example      pacs-ai-backend/postgresql/.env
# redis has no .env.example (the README still lists one); its password lives in redis/redis.conf

docker network create pacs-net

The root .env holds deployment-wide values such as APP_TIMEZONE=America/Toronto. Service-specific .env files (api-pacs/.env, postgresql/.env, orthanc/.env, nginx/.env) hold runtime settings and secrets for that service. When a variable appears both in a service env_file and an explicit compose environment block, the compose value wins. Before first boot, replace every password, key and secret with a strong unique value (openssl rand -hex 32). Keep .env files, the Firebase service-account JSON and TLS material in the customer secrets vault; they are customer-owned.

2. Identity: Google Cloud and Firebase

  1. Create a GCP project (the README uses pacs-ai-prod). Enable Identity Platform, then under Settings, Security, enable Multi Tenancy and create a tenant (for example prod). Note the generated tenant ID: the viewer, the login route and the owner bootstrap all require it.
  2. Under Providers, scope to the tenant and enable the sign-in methods the customer requires. Email/password is the one the viewer uses; Identity Platform also offers Google, SAML and OIDC. Firebase Authentication must have Email/Password enabled as well.
  3. Configure the authorized origins and redirect URIs to include the API hostname and any frontend origins; sign-in fails without them.
  4. Create a Firestore database in production mode. Do not change the Database Id. The README uses northamerica-northeast1 (Montreal); Firebase data residency follows the project you provision.
  5. Create a collection named tenants with one document whose ID is the tenant ID (see Admin access for the fields).
  6. Register a web app (pacs-ai-webapp) and copy the Web SDK configuration into PACS-AI/platform/app/.env.
  7. Generate a service-account private key and rename it pacs-ai-firebase-admin.json. For development it lives at api-pacs/configs/firebase/. For production either use the README path inside the container, FIREBASE_CONFIG_FILE_PATH=/app/build/configs/firebase/pacs-ai-firebase-admin.json, or stage it outside the repo (/etc/pacs-ai/secrets/firebase-sa.json, mode 600), mount it read-only into api-pacs through docker-compose.override.yml and point FIREBASE_CONFIG_FILE_PATH at the mounted path. Never commit it.
  8. Generate a Web API key. Set FIREBASE_PROJECT_ID, FIREBASE_WEB_API_KEY (allow the Identity Toolkit API; restrict the key to the API host IP or referrer where possible, and test its restrictions from the api-pacs host before rollout) and a strong FIREBASE_SUPERUSER_KEY.
If Identity Platform reCAPTCHA Enterprise enforcement is enabled on the project, signInWithPassword also requires Google reCAPTCHA fields; the Turnstile integration does not replace that provider-specific requirement and login will fail. Check the setting before rollout.

3. Email, DICOM and network configuration

  • Mailgun: connect your domain, then set MAILGUN_API_KEY, MAILGUN_DOMAIN and MAILGUN_SENDER_EMAIL in api-pacs/.env (the README sends you to nginx/.env for the key; that file only holds SERVER_NAME and body-size limits, so the api-pacs file is the right place).
  • DICOM: set ORTHANC_DICOM_AET and ORTHANC_DICOM_PORT in orthanc/.env (the example ships the port empty and the compose file has no fallback, so it must be set) and ORTHANC_AET in api-pacs/.env. The AE title must be unique on the hospital DICOM network; consult the PACS administrator.
  • nginx: set SERVER_NAME (domain or IP) in nginx/.env and place the certificate and key at nginx/ssl/nginx.crt and nginx/ssl/nginx.key. Rotate a certificate by replacing the files and running docker compose restart nginx; expect a few seconds of dropped connections, so schedule it in a maintenance window.
  • Timezone: set APP_TIMEZONE per deployment region (for example America/Toronto or Asia/Dubai) instead of changing code.
  • Proxies: if Cloudflare fronts the deployment, set its origin SSL mode to Full or Full (strict); Flexible mode is not supported. A different TLS-terminating proxy requires an explicit compose override; the bundled nginx is the intended public boundary.

4. Service-to-service tokens

api-pacs and study-service authenticate each other with three distinct bearer tokens. Generate one value per token and set all three in both api-pacs/.env and cardio-agent/study-service/.env. Use a different set per environment, never commit populated .env files, and rotate all three together by restarting both services.

openssl rand -hex 32   # STUDY_SERVICE_INGEST_TOKEN    api-pacs signs POST /ingest/study
openssl rand -hex 32   # STUDY_SERVICE_OPERATOR_TOKEN  /jobs, /jobs/stream, /settings, /health/detailed, /metrics
openssl rand -hex 32   # STUDY_SERVICE_CALLBACK_TOKEN  study-service signs processing callbacks to api-pacs

5. Start the stack

cd pacs-ai-backend
make up-prod        # docker compose up -d --build (builds the viewer container too)
docker compose ps   # every container should report healthy
make down-prod      # stop

Initial startup takes several minutes while images build and containers initialize. The API documentation is then served at https://<your-domain>/api/docs, gated by OPENAPI_DOCS_PASSWORD when set.

6. Database bootstrap

api-pacs stores the ingestion control plane in PostgreSQL (port 5433) and all IAM data in Firebase. Apply the SQL migrations with psql (no extra tooling; the README shows the first three) or with golang-migrate (documented in postgresql/README.md); apply cardio-agent migrations with Alembic. The tables created are ingestion_jobs, ingestion_candidates, ingestion_processing_jobs and, from migration 000004, ingestion_processing_runs; migrations 000005 to 000007 add the run rollup, reconciliation tracking and the manual-run requester.

# psql, no third-party tool (repeat for every 00000N_*.up.sql in order)
for f in api-pacs/infrastructures/database/postgresql/migrations/*.up.sql; do
  docker exec -i postgresql psql -U $POSTGRES_DB_USERNAME -d $POSTGRES_DB_DATABASE < "$f"
done
docker exec postgresql psql -U $POSTGRES_DB_USERNAME -d $POSTGRES_DB_DATABASE -c '\dt'

# golang-migrate (POSTGRES_DB_* exported in the shell, migrate binary installed)
cd api-pacs && make migrate-up
make migrate-version            # current schema version
STEPS=1 make migrate-down       # roll back one step (all seven current migrations ship down SQL; check before relying on it)
STEPS=<v> make migrate-force    # recovery only

# cardio-agent (Alembic)
docker compose exec study-service alembic upgrade head
The real-time worklist requires Go migrations 000004 through 000006, migration 000007 for user-attributed manual runs and quotas, and study-service Alembic revision 20260814_0012 or later. There is no SQL seed and no make seed-admin: tenants are Firestore documents and the first owner is created through the superuser endpoint described under Admin access.

7. Smoke tests

# 1. C-ECHO from the api-pacs host to the remote PACS (dcmtk)
echoscu -aet $ORTHANC_AET -aec <REMOTE_AE> <REMOTE_HOST> <REMOTE_PORT>

# 2. Orthanc REST reachable (the shipped compose disables Orthanc authentication, so no credentials are needed)
curl http://localhost:8042/system

# 3. api-pacs reachable (root status route)
curl https://api.<customer-domain>/
  1. Log in to the viewer as the tenant owner.
  2. Open Inference, Ingestion jobs, Add. Create a job for the customer modality with a short recent window (30 to 60 minutes) and the Orthanc destination AE.
  3. Watch the candidate status progress through DISCOVERED, STABLE, RETRIEVAL_QUEUED and RETRIEVED, then its processing_status rollup through queued, running and completed.
  4. Confirm the study is visible in the viewer and an inference result row landed in the cardio-agent database: docker compose exec postgres psql -U cardio -d cardio_agent -c "SELECT id, study_instance_uid, status, created_at FROM pipeline_jobs ORDER BY created_at DESC LIMIT 5;".
  5. If a candidate sits in one state for more than about 5 minutes, follow the stuck-candidate runbook under Operations.
PACS AI Logo status Status Terms and Conditions Privacy Policy Privacy Impact Assessment Document (EFVP)

© 2026 HeartWise AI Lab, Montreal Heart Institute. All rights reserved.