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
- 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 exampleprod). Note the generated tenant ID: the viewer, the login route and the owner bootstrap all require it. - 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.
- Configure the authorized origins and redirect URIs to include the API hostname and any frontend origins; sign-in fails without them.
- 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. - Create a collection named
tenantswith one document whose ID is the tenant ID (see Admin access for the fields). - Register a web app (
pacs-ai-webapp) and copy the Web SDK configuration intoPACS-AI/platform/app/.env. - Generate a service-account private key and rename it
pacs-ai-firebase-admin.json. For development it lives atapi-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 throughdocker-compose.override.ymland pointFIREBASE_CONFIG_FILE_PATHat the mounted path. Never commit it. - 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 strongFIREBASE_SUPERUSER_KEY.
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_DOMAINandMAILGUN_SENDER_EMAILinapi-pacs/.env(the README sends you tonginx/.envfor the key; that file only holdsSERVER_NAMEand body-size limits, so the api-pacs file is the right place). - DICOM: set
ORTHANC_DICOM_AETandORTHANC_DICOM_PORTinorthanc/.env(the example ships the port empty and the compose file has no fallback, so it must be set) andORTHANC_AETinapi-pacs/.env. The AE title must be unique on the hospital DICOM network; consult the PACS administrator. - nginx: set
SERVER_NAME(domain or IP) innginx/.envand place the certificate and key atnginx/ssl/nginx.crtandnginx/ssl/nginx.key. Rotate a certificate by replacing the files and runningdocker compose restart nginx; expect a few seconds of dropped connections, so schedule it in a maintenance window. - Timezone: set
APP_TIMEZONEper deployment region (for exampleAmerica/TorontoorAsia/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 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>/ - Log in to the viewer as the tenant owner.
- 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.
- Watch the candidate
statusprogress throughDISCOVERED,STABLE,RETRIEVAL_QUEUEDandRETRIEVED, then itsprocessing_statusrollup throughqueued,runningandcompleted. - 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;". - If a candidate sits in one state for more than about 5 minutes, follow the stuck-candidate runbook under Operations.