services: postgres: image: postgres:17-alpine@sha256:18cfe3ef5e6815560c98237d6216d1e5119702fb0f3894c8785dd58b8bbe5d73 environment: POSTGRES_DB: ${MODELFORGE_POSTGRES_DB:-modelforge} POSTGRES_USER: ${MODELFORGE_POSTGRES_ADMIN_USER:-postgres} POSTGRES_PASSWORD: ${MODELFORGE_POSTGRES_ADMIN_PASSWORD:?set a bootstrap-only PostgreSQL admin password} MODELFORGE_MIGRATION_DB_PASSWORD: ${MODELFORGE_MIGRATION_DB_PASSWORD:?set the non-superuser migration role password} MODELFORGE_RUNTIME_DB_PASSWORD: ${MODELFORGE_RUNTIME_DB_PASSWORD:?set the non-owner runtime role password} volumes: - postgres-data:/var/lib/postgresql/data - ./deploy/postgres/init:/docker-entrypoint-initdb.d:ro healthcheck: test: ["CMD-SHELL", "pg_isready -U ${MODELFORGE_POSTGRES_ADMIN_USER:-postgres} -d ${MODELFORGE_POSTGRES_DB:-modelforge}"] interval: 5s timeout: 3s retries: 20 ports: # Loopback only. The control-plane database holds provenance, credential hashes and the # audit trail; publishing it on every interface put all of that on the LAN behind a # development password. Override deliberately if a remote tool genuinely needs it. - "${MODELFORGE_POSTGRES_BIND:-127.0.0.1}:${MODELFORGE_POSTGRES_PORT:-5432}:5432" redis: image: redis:7-alpine@sha256:ff02b58f971e7d7d156a1267e283fcbbeee91773b6aa36c49dac28ecfe28eadf command: ["redis-server", "--appendonly", "yes"] volumes: - redis-data:/data healthcheck: test: ["CMD", "redis-cli", "ping"] interval: 5s timeout: 3s retries: 20 ports: # Loopback only. Redis has no authentication configured and holds transient request payloads. - "${MODELFORGE_REDIS_BIND:-127.0.0.1}:${MODELFORGE_REDIS_PORT:-6379}:6379" api: build: context: ./backend args: # Build identity, so any image built from this file can say where it came from. An argument # that is never passed stays empty and is reported as null rather than guessed. MODELFORGE_VERSION: ${MODELFORGE_VERSION:-0.0.0} MODELFORGE_COMMIT: ${MODELFORGE_COMMIT:-} MODELFORGE_BUILT_AT: ${MODELFORGE_BUILT_AT:-} environment: MODELFORGE_DATABASE_URL: ${MODELFORGE_RUNTIME_DATABASE_URL:?set the modelforge_runtime SQLAlchemy URL} MODELFORGE_REDIS_URL: redis://redis:6379/0 MODELFORGE_HF_HOME: /data/hf-cache MODELFORGE_ARTIFACT_ROOT: /data/artifacts MODELFORGE_QUARANTINE_ROOT: /data/quarantine MODELFORGE_CONFIG_ROOT: /app/config MODELFORGE_NODE_IDENTITY_FILE: /data/state/node-id MODELFORGE_NODE_IDENTITY_MODE: persisted MODELFORGE_HARDWARE_REFRESH_ON_STARTUP: "false" MODELFORGE_NODE_LIVENESS_MONITOR_ENABLED: "true" MODELFORGE_REGISTRY_SEED_ON_STARTUP: "true" MODELFORGE_OPERATOR_API_KEY: ${MODELFORGE_OPERATOR_API_KEY:-} MODELFORGE_HF_TOKEN: ${MODELFORGE_HF_TOKEN:-} MODELFORGE_NODE_STALE_AFTER_SECONDS: ${MODELFORGE_NODE_STALE_AFTER_SECONDS:-30} MODELFORGE_NODE_OFFLINE_AFTER_SECONDS: ${MODELFORGE_NODE_OFFLINE_AFTER_SECONDS:-90} MODELFORGE_LIVENESS_POLL_INTERVAL_SECONDS: ${MODELFORGE_LIVENESS_POLL_INTERVAL_SECONDS:-5} MODELFORGE_SERVING_RECONCILIATION_ENABLED: ${MODELFORGE_SERVING_RECONCILIATION_ENABLED:-true} MODELFORGE_LIFECYCLE_RECONCILIATION_ENABLED: ${MODELFORGE_LIFECYCLE_RECONCILIATION_ENABLED:-true} MODELFORGE_MIGRATION_RECONCILIATION_ENABLED: ${MODELFORGE_MIGRATION_RECONCILIATION_ENABLED:-true} MODELFORGE_OBSERVABILITY_MONITOR_ENABLED: ${MODELFORGE_OBSERVABILITY_MONITOR_ENABLED:-true} MODELFORGE_RECOVERY_RECONCILIATION_ENABLED: ${MODELFORGE_RECOVERY_RECONCILIATION_ENABLED:-true} MODELFORGE_SOURCE_COMMIT: ${MODELFORGE_SOURCE_COMMIT:-} MODELFORGE_SOURCE_REFERENCE: ${MODELFORGE_SOURCE_REFERENCE:-} MODELFORGE_SOURCE_REPOSITORY: ${MODELFORGE_SOURCE_REPOSITORY:-} MODELFORGE_BACKUP_ROOT: /data/backups MODELFORGE_BACKUP_RESTORE_ROOT: /data/restore MODELFORGE_BACKUP_ENCRYPTION_KEY: ${MODELFORGE_BACKUP_ENCRYPTION_KEY:-} MODELFORGE_BACKUP_ENCRYPTION_KEY_ID: ${MODELFORGE_BACKUP_ENCRYPTION_KEY_ID:-modelforge-backup-key-1} MODELFORGE_BACKUP_STALE_AFTER_SECONDS: ${MODELFORGE_BACKUP_STALE_AFTER_SECONDS:-93600} MODELFORGE_RESTORE_ALLOW_PRODUCTION_TARGET: ${MODELFORGE_RESTORE_ALLOW_PRODUCTION_TARGET:-false} MODELFORGE_OBSERVABILITY_POLL_INTERVAL_SECONDS: ${MODELFORGE_OBSERVABILITY_POLL_INTERVAL_SECONDS:-60} MODELFORGE_SERVING_RECONCILIATION_INTERVAL_SECONDS: ${MODELFORGE_SERVING_RECONCILIATION_INTERVAL_SECONDS:-5} MODELFORGE_SCHEDULER_SAFETY_RESERVE_BYTES: ${MODELFORGE_SCHEDULER_SAFETY_RESERVE_BYTES:-1073741824} volumes: - ./config:/app/config:ro - hf-cache:/data/hf-cache - model-artifacts:/data/artifacts - quarantine:/data/quarantine - modelforge-state:/data/state - ${MODELFORGE_BACKUP_VOLUME:-modelforge-backups}:/data/backups - ${MODELFORGE_RESTORE_VOLUME:-modelforge-restore}:/data/restore ports: - "${MODELFORGE_API_BIND:-0.0.0.0}:${MODELFORGE_API_PUBLISHED_PORT:-8000}:8000" cap_drop: - ALL security_opt: - no-new-privileges:true depends_on: migrate: condition: service_completed_successfully redis: condition: service_healthy healthcheck: test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/api/v1/health/live')"] interval: 10s timeout: 3s retries: 10 migrate: build: context: ./backend args: MODELFORGE_VERSION: ${MODELFORGE_VERSION:-0.0.0} MODELFORGE_COMMIT: ${MODELFORGE_COMMIT:-} MODELFORGE_BUILT_AT: ${MODELFORGE_BUILT_AT:-} command: ["alembic", "upgrade", "head"] environment: MODELFORGE_MIGRATION_DATABASE_URL: ${MODELFORGE_MIGRATION_DATABASE_URL:?set the non-superuser modelforge owner SQLAlchemy URL} depends_on: postgres: condition: service_healthy restart: "no" cap_drop: - ALL security_opt: - no-new-privileges:true web: build: context: ./frontend args: VITE_API_BASE_URL: ${VITE_API_BASE_URL:-http://localhost:8000} MODELFORGE_VERSION: ${MODELFORGE_VERSION:-0.0.0} MODELFORGE_COMMIT: ${MODELFORGE_COMMIT:-} MODELFORGE_BUILT_AT: ${MODELFORGE_BUILT_AT:-} ports: - "${MODELFORGE_WEB_BIND:-127.0.0.1}:${MODELFORGE_WEB_PORT:-3000}:3000" read_only: true tmpfs: - /tmp:size=16m,mode=1777 cap_drop: - ALL security_opt: - no-new-privileges:true depends_on: api: condition: service_healthy volumes: postgres-data: modelforge-backups: modelforge-restore: redis-data: hf-cache: model-artifacts: quarantine: modelforge-state: