Deploy
Configuration reference
Every environment variable that matters, grouped by concern, plus the compose overrides for common topologies.
api-pacs: core
| Variable | Default | Purpose |
|---|---|---|
API_NAME, API_URL_REST_PORT | api-pacs, 8000 | Do not change unless compose and nginx wiring change too |
APP_URL | none | Public URL used in emails and links |
API_BASE_URL | http://api-pacs:8000/v1 | Internal base URL of the API |
APP_TIMEZONE | America/Toronto | IANA timezone for logs, ingestion windows and Postgres sessions |
DOCKER_NETWORK | pacs-net | External network attached to inference containers |
DOCKER_USERNAME, DOCKER_PASSWORD | none | Registry credentials for private inference images (not the Docker socket, which compose mounts read-only) |
POSTGRES_DB_HOST, POSTGRES_DB_PORT, POSTGRES_DB_DATABASE, POSTGRES_DB_USERNAME, POSTGRES_DB_PASSWORD | postgresql, 5432, db_pacs, pacs, dev password | Control-plane database; replace the dev password before any non-dev install |
REDIS_HOST, REDIS_PORT, REDIS_PASSWORD, REDIS_IAM_DB | redis, 6379, dev password, 1 | IAM cache and rate-limit counters; the password must match redis/redis.conf and the cardio-agent REDIS_URL |
ELASTICSEARCH_URL, KIBANA_BASE_URL | http://elasticsearch:9200, http://kibana:5601 | Study index and log deep links |
FIREBASE_PROJECT_ID, FIREBASE_CONFIG_FILE_PATH, FIREBASE_WEB_API_KEY, FIREBASE_SUPERUSER_KEY | none | Firebase Admin SDK, tenant-aware password sign-in, and the superuser key that gates first-owner bootstrap and privileged endpoints |
ORTHANC_AET, ORTHANC_BASE_URL, ORTHANC_LOCAL_CACHE_EXPIRATION_IN_HOURS | PACS_AI, http://orthanc:8042, 24 | Calling AE title, Orthanc REST URL, DICOM cache TTL |
MAILGUN_API_KEY, MAILGUN_DOMAIN, MAILGUN_SENDER_EMAIL | none | Transactional email |
MAILCHIMP_API_KEY, MAILCHIMP_BASE_URL, MAILCHIMP_LIST_ID | none | Optional marketing list sync |
DOCUSIGN_INTEGRATION_KEY, DOCUSIGN_USER_ID, DOCUSIGN_ACCOUNT_BASE_URI, DOCUSIGN_AUTH_SERVER, DOCUSIGN_PRIVATE_KEY, DOCUSIGN_ACCOUNT_ID | none | Optional DocuSign JWT auth for e-signature flows |
OPENAPI_DOCS_PASSWORD | none | Basic-auth password for the /docs OpenAPI UI; empty disables the gate |
ORCHESTRATOR_API_URL | http://medical-agent:8585 | URL of the optional orchestrator service |
api-pacs: ingestion, reconciliation and study-service handoff
| Variable | Default | Purpose |
|---|---|---|
INFERENCE_INGESTION_RUNNER_INTERVAL_MINUTES | 1 | Discovery (C-FIND) loop period |
INFERENCE_INGESTION_RETRIEVAL_WORKER_INTERVAL_MINUTES | 1 | C-MOVE retrieval loop period |
INFERENCE_INGESTION_RECONCILIATION_INTERVAL_MINUTES | 5 | Reconciliation loop period |
INFERENCE_INGESTION_RECONCILIATION_PENDING_MINUTES, INFERENCE_INGESTION_RECONCILIATION_QUEUED_MINUTES, INFERENCE_INGESTION_RECONCILIATION_RUNNING_MINUTES | 2, 10, 65 | Per-state stale thresholds for processing executions |
INFERENCE_INGESTION_RECONCILIATION_MODEL_RUNNING_MINUTES | none | JSON object of per-model running-stale overrides in minutes |
INFERENCE_INGESTION_RECONCILIATION_FAILURE_THRESHOLD | 3 | Consecutive reconciliation failures before a run is flagged RECONCILIATION_FAILED |
INFERENCE_INGESTION_RECONCILIATION_STALE_MINUTES | unset | Legacy single threshold, used only as a fallback for the three per-state values |
INFERENCE_INGESTION_DEFAULT_RECENT_WINDOW_MINUTES | 240 | C-FIND lookback window for jobs that do not set recent_window_minutes |
STUDY_SERVICE_BASE_URL | http://study-service:8600 | Execution plane URL |
STUDY_SERVICE_INGEST_TOKEN, STUDY_SERVICE_OPERATOR_TOKEN, STUDY_SERVICE_CALLBACK_TOKEN | none | The three shared bearer tokens |
STUDY_SERVICE_DISPATCH_CONCURRENCY | none | Parallel dispatch fan-out; the only bound on automated ingestion, which is exempt from user quotas |
INFERENCE_REQUIRE_PROCESSING_RUN_ID | true | Strict processing-run correlation; set false only during a controlled compatibility or rollback window |
api-pacs: public registration, login protection and quotas
| Variable | Default | Purpose |
|---|---|---|
CLOUDFLARE_SECRET_KEY, CLOUDFLARE_TURNSTILE_BASE_URL | none, https://challenges.cloudflare.com/turnstile/v0 | Server-side Turnstile verification; required when public registration or adaptive login is enabled |
REGISTRATION_RATE_LIMIT_WINDOW_SECONDS | 600 | Registration throttle window |
REGISTRATION_RATE_LIMIT_TENANT_ATTEMPTS, REGISTRATION_RATE_LIMIT_EMAIL_ATTEMPTS, REGISTRATION_RATE_LIMIT_IP_ATTEMPTS | 100, 5, 10 | Attempts per tenant, per normalized email, per trusted client IP within the window; throttled requests return HTTP 429 REGISTRATION_RATE_LIMITED with Retry-After |
REGISTRATION_TRUSTED_PROXY_CIDRS, LOGIN_TRUSTED_PROXY_CIDRS | empty | Direct-proxy CIDRs allowed to supply X-Real-IP; use the exact pacs-net subnet (docker network inspect pacs-net) behind the bundled nginx. When empty, or when the peer is outside those networks, api-pacs ignores X-Real-IP and uses the socket peer address |
POLICY_TERMS_VERSION, POLICY_TERMS_URL, POLICY_TERMS_EFFECTIVE_AT, POLICY_PRIVACY_VERSION, POLICY_PRIVACY_URL, POLICY_PRIVACY_EFFECTIVE_AT | 2026-08-15, https://pacsai.co/terms-of-service, 2026-08-15, 2026-08-15, https://pacsai.co/privacy-policy, 2026-08-15 | Versioned Terms of Service and Privacy Policy that registration must accept; publish these exact versions at the canonical HTTPS URLs before enabling public registration |
POLICY_EXISTING_USER_GRACE_UNTIL | empty | Optional RFC3339 cutoff before existing users must accept current policies; empty means immediate enforcement |
LOGIN_ABUSE_PROTECTION_ENABLED | true | Redis-backed adaptive login protection; disabling is emergency-only, logs a security warning, and still keeps password verification server-side |
LOGIN_FAILURE_WINDOW_SECONDS | 600 | Fixed login failure window |
LOGIN_ACCOUNT_CHALLENGE_FAILURES | 3 | Account failures before Turnstile is required; the account scope never hard-locks |
LOGIN_IP_CHALLENGE_FAILURES / LOGIN_IP_MAX_FAILURES | 5 / 30 | Client-IP challenge and hard-limit thresholds |
LOGIN_TENANT_CHALLENGE_FAILURES / LOGIN_TENANT_MAX_FAILURES | 50 / 500 | Tenant challenge and hard-limit thresholds |
LOGIN_TURNSTILE_ALLOWED_HOSTNAMES | none | Exact frontend hostnames accepted for the login Turnstile action; production must not include localhost |
INFERENCE_USER_QUOTA_WINDOW_SECONDS, INFERENCE_USER_QUOTA_ALLOWANCE | 86400, 50 | Per tenant and user inference units per window; one direct prediction or one manual reprocess consumes one unit |
INFERENCE_USER_MAX_CONCURRENT_EXECUTIONS, INFERENCE_USER_RESERVATION_TTL_SECONDS | 2, 7200 | Active runs per user and the recovery TTL for a lost callback |
INFERENCE_USER_QUOTA_TENANT_OVERRIDES_JSON | empty | JSON keyed by tenant ID; fields windowSeconds, allowance, maxConcurrentExecutions, reservationTtlSeconds |
Quota semantics: input or plan validation failures and dispatch failures are refunded; work accepted by a model stays charged even if the model later fails; manual-run concurrency is held until the run reaches a terminal state, with the reservation TTL acting only as recovery for lost callbacks. Rejections return HTTP 429 with INFERENCE_QUOTA_EXCEEDED or INFERENCE_CONCURRENCY_EXCEEDED; Redis unavailability fails closed with HTTP 503 INFERENCE_QUOTA_UNAVAILABLE. Clients read their allowance at GET /v1/inference/quota. Owners and admins are subject to the same quota unless their tenant has an override.
api-pacs: request and input safety
| Variable | Default | Purpose |
|---|---|---|
API_MAX_REQUEST_BODY_BYTES | 16777216 | Hard body limit for regular /v1 requests (HTTP 413 REQUEST_BODY_TOO_LARGE) |
INFERENCE_PREDICT_MAX_REQUEST_BODY_BYTES | 1048576 | Stricter JSON limit for direct model predictions |
DICOMWEB_MAX_REQUEST_BODY_BYTES, DICOMWEB_READ_TIMEOUT_SECONDS | 6442450944, 7200 | The only bundled large-upload exception (authenticated DICOMweb); keep consistent with nginx and do not raise the general API limits to accommodate DICOM uploads |
INFERENCE_MAX_SERIES_UIDS, INFERENCE_MAX_METADATA_BYTES, INFERENCE_MAX_METADATA_DEPTH, INFERENCE_MAX_METADATA_ENTRIES | 256, 65536, 8, 256 | Prediction input ceilings, validated before DICOM retrieval; the model dicomUploadMin / dicomUploadMax are authoritative and may be stricter, and an invalid model range fails closed with HTTP 503 |
ORCHESTRATOR_MAX_REQUEST_BODY_BYTES | 8388608 | Body limit for orchestrator thread, chat and DICOM-selection JSON |
ORCHESTRATOR_MAX_STUDIES_PER_REQUEST, ORCHESTRATOR_MAX_SERIES_UIDS_PER_STUDY | 20, 256 | Studies per orchestrator request and series UIDs per study |
ORCHESTRATOR_MAX_PREVIEW_BASE64_BYTES | 2097152 | Maximum encoded preview or image string |
ORCHESTRATOR_MAX_METADATA_BYTES, ORCHESTRATOR_MAX_METADATA_DEPTH, ORCHESTRATOR_MAX_METADATA_ENTRIES | 65536, 8, 256 | Orchestrator metadata size and structural limits |
ORCHESTRATOR_MAX_MESSAGE_BYTES | 32768 | Maximum chat message length |
HTTP_SERVER_READ_HEADER_TIMEOUT_SECONDS, HTTP_SERVER_READ_TIMEOUT_SECONDS, HTTP_SERVER_IDLE_TIMEOUT_SECONDS | 10, 30, 120 | Server timeouts; the write timeout stays disabled because the worklist SSE stream is long-lived |
After changing nginx limits, rebuild the nginx service. After changing api-pacs limits or timeouts, recreate api-pacs so it receives the new environment.
nginx and Orthanc
| File | Variables |
|---|---|
nginx/.env | SERVER_NAME; NGINX_FRONTEND_MAX_BODY_SIZE=1m, NGINX_API_MAX_BODY_SIZE=16m, NGINX_DICOMWEB_MAX_BODY_SIZE=6g (must be positive; zero or malformed values fail startup) |
orthanc/.env | ORTHANC_DICOM_AET, ORTHANC_DICOM_PORT (no shipped default; 4242 in the development compose) |
root .env | APP_TIMEZONE |
study-service (cardio-agent/study-service/.env)
| Variable | Default | Purpose |
|---|---|---|
HOST, PORT, RELOAD, LOG_LEVEL, LOG_FORMAT | 0.0.0.0, 8600, false, info, text | Server basics |
DATABASE_URL | none | Postgres URI for the cardio-agent database (5434); the repo default cardio:cardio is dev-only |
REDIS_URL | none | Redis URI with password; integrated mode shares the root Redis on DB 2 |
ORTHANC_URL, ORTHANC_CHANGES_LIMIT | http://orthanc:8042, 100 | Orthanc REST and polling page size |
ENABLE_ORTHANC_POLLING, ENABLE_GO_CALLBACKS | false, true | Exactly one must be true. The integrated topology uses callbacks (api-pacs hands off studies); standalone study-service uses polling |
ORTHANC_POLL_START_MODE, ORTHANC_POLL_START_TIMESTAMP, ORTHANC_POLL_INTERVAL, ORTHANC_POLL_LOCK_TTL | latest | Polling-mode settings (latest, backlog, since_timestamp) |
GO_CALLBACK_BASE_URL, GO_CALLBACK_URL, GO_CALLBACK_TIMEOUT_SECONDS, GO_CALLBACK_MAX_RETRIES, GO_CALLBACK_TOKEN | none, none, 5, 4, none | Callback target and auth (mirror of STUDY_SERVICE_CALLBACK_TOKEN) |
STUDY_SERVICE_INGEST_TOKEN, STUDY_SERVICE_OPERATOR_TOKEN, STUDY_SERVICE_CALLBACK_TOKEN | none | Mirrors of the api-pacs values |
ALLOW_UNAUTHENTICATED_INGEST, ALLOW_UNAUTHENTICATED_OPERATOR_ROUTES | false | Dev-only auth bypasses |
CELERY_WORKER_CONCURRENCY | 2 | Worker pool size |
PROMETHEUS_MULTIPROC_DIR | /tmp/prometheus-multiproc | Shared metrics directory so worker counters aggregate |
CORS_ORIGINS, SHARED_DOCKER_NETWORK / CARDIO_AGENT_SHARED_NETWORK | none, pacs-net | CORS and the external network |
Compose overrides for common topologies
Compose merges a docker-compose.override.yml placed next to the root compose file.
- No GPU host: set
study-celery-workertoreplicas: 0with adisabledprofile (andollamaif enabled), then pointREDIS_URLin study-service at the GPU host Redis so a remote worker consumes the queue. - Managed PostgreSQL (RDS, Cloud SQL): remove
postgresql/docker-compose.ymlfrom the rootinclude:list (and the inlinepostgres:block if cardio-agent moves too), then overridePOSTGRES_DB_*on api-pacs andDATABASE_URLon study-service and the worker. Run migrations from a host with themigratebinary. - Managed Redis (ElastiCache, Memorystore): remove
redis/docker-compose.ymlfrominclude:and overrideREDIS_HOST,REDIS_PORT,REDIS_PASSWORD,REDIS_IAM_DBon api-pacs andREDIS_URL(DB 2) on study-service and the worker. - Air-gapped: on a connected host run
docker compose pullanddocker save -o pacs-ai-images.tar $(docker compose config --images); on the air-gapped hostdocker load -i pacs-ai-images.tarand setpull_policy: neverfor every service. Firebase still needs egress to*.googleapis.com; agree with the customer on a self-hosted IdP or a tunneled allow-list for that endpoint.