services: migrate: build: context: . dockerfile: backend/Dockerfile image: model-development-backend:latest logging: driver: json-file options: max-size: "200m" max-file: "10" restart: "no" command: - uv - run - --frozen - --no-dev - --package - backend - alembic - upgrade - head environment: DATABASE_URL: ${DATABASE_URL:?DATABASE_URL is required} INITIAL_ADMIN_PASSWORD: ${INITIAL_ADMIN_PASSWORD:-admin12345} UV_OFFLINE: "1" UV_NO_SYNC: "1" APP_CONFIG_SECRET_KEY: ${APP_CONFIG_SECRET_KEY:?APP_CONFIG_SECRET_KEY is required} web: build: context: . dockerfile: frontend/Dockerfile image: model-development-web:latest logging: driver: json-file options: max-size: "200m" max-file: "10" restart: unless-stopped # Architecture §2.2: this is the only service exposed to the host. The # default.conf file is mounted as a template; scripts/nginx-entrypoint.sh # parses ${S3_ENDPOINT} and writes the rendered config to # /etc/nginx/conf.d/default.conf before exec'ing nginx. ports: - "${GATEWAY_PORT:-8888}:80" environment: S3_ENDPOINT: ${S3_ENDPOINT:?S3_ENDPOINT is required} depends_on: backend: condition: service_healthy runtime: condition: service_healthy volumes: - ./default.conf:/etc/nginx/conf.d/default.conf.template:ro healthcheck: test: [ "CMD-SHELL", "wget -qO- http://127.0.0.1/ >/dev/null" ] interval: 10s timeout: 3s retries: 12 start_period: 10s backend: build: context: . dockerfile: backend/Dockerfile image: model-development-backend:latest logging: driver: json-file options: max-size: "200m" max-file: "10" restart: unless-stopped # No host port: architecture §2.2 — only Nginx is externally reachable. # The previous ``8891:8000`` mapping (P0-1) was removed: the # ``/internal/v1/*`` storage control plane is now guarded by a # shared ``INTERNAL_SERVICE_TOKEN`` instead of network isolation. # No local-FS volume: backend stores everything in S3 (S3_*). environment: DATABASE_URL: ${DATABASE_URL:?DATABASE_URL is required} SERVICE_NAME: model-platform-backend SCHEDULE_EVENT_NAMESPACE: ${SCHEDULE_EVENT_NAMESPACE:-model-platform-local} JWT_SECRET: ${JWT_SECRET:?JWT_SECRET is required} INITIAL_ADMIN_PASSWORD: ${INITIAL_ADMIN_PASSWORD:-admin12345} RUNTIME_API_URL: http://runtime:8000 # P0-1 fix: shared secret required by /internal/v1/* routes. INTERNAL_SERVICE_TOKEN: ${INTERNAL_SERVICE_TOKEN:?INTERNAL_SERVICE_TOKEN is required} STORAGE_BACKEND: ${STORAGE_BACKEND:-s3} LOCAL_STORAGE_BASE_DIR: ${LOCAL_STORAGE_BASE_DIR:-/data} # S3_* only matter when STORAGE_BACKEND=s3. Defaults are kept so local # mode boots without them; override in .env when switching to s3. S3_ENDPOINT: ${S3_ENDPOINT:-http://s3:9000} S3_ACCESS_KEY: ${S3_ACCESS_KEY:-} S3_SECRET_KEY: ${S3_SECRET_KEY:-} S3_WORKSPACE_BUCKET: ${S3_WORKSPACE_BUCKET:-workspace} S3_VERSION_BUCKET: ${S3_VERSION_BUCKET:-version} S3_RUN_LOG_BUCKET: ${S3_RUN_LOG_BUCKET:-run-log} S3_TRASH_BUCKET: ${S3_TRASH_BUCKET:-trash} S3_TRASH_RETENTION_DAYS: ${S3_TRASH_RETENTION_DAYS:-30} READINESS_TARGETS: ${MYSQL_HOST:?MYSQL_HOST is required}:${MYSQL_PORT:-3306},runtime:8000 UV_OFFLINE: "1" UV_NO_SYNC: "1" APP_CONFIG_SECRET_KEY: ${APP_CONFIG_SECRET_KEY:?APP_CONFIG_SECRET_KEY is required} depends_on: migrate: condition: service_completed_successfully runtime: condition: service_healthy volumes: - ${PWD}:/app - ./data:/data - /app/.venv healthcheck: test: [ "CMD-SHELL", "curl -fsS http://127.0.0.1:8000/health/ready >/dev/null" ] interval: 10s timeout: 5s retries: 18 start_period: 20s runtime: build: context: . dockerfile: runtime/Dockerfile image: model-development-runtime:latest logging: driver: json-file options: max-size: "200m" max-file: "10" restart: unless-stopped # No host port: architecture §2.2 — only Nginx is externally reachable. # The previous ``8892:8000`` mapping (P0-1) was removed: the runtime # container is reachable only from the Docker internal network and # Nginx-authenticated Jupyter paths. cap_add: - SYS_ADMIN devices: - /dev/fuse:/dev/fuse security_opt: - apparmor:unconfined # No host port: architecture §2.2 — only Nginx is externally reachable. environment: DATABASE_URL: ${DATABASE_URL:?DATABASE_URL is required} SERVICE_NAME: runtime-manager # P0-1 fix: runtime's /api/v1/jupyter is token-guarded. The same # ``INTERNAL_SERVICE_TOKEN`` value backend uses for /internal/v1/* # auth — see ``require_internal_service`` in runtime/main.py. INTERNAL_SERVICE_TOKEN: ${INTERNAL_SERVICE_TOKEN:?INTERNAL_SERVICE_TOKEN is required} STORAGE_BACKEND: ${STORAGE_BACKEND:-s3} LOCAL_STORAGE_BASE_DIR: ${LOCAL_STORAGE_BASE_DIR:-/data} # WORKSPACES_ROOT defaults to /data/workspace (settings.workspaces_root); # in local mode runtime skips the rclone mount and reads directly from # ${LOCAL_STORAGE_BASE_DIR}/workspace instead. PUBLIC_BASE_URL: http://runtime # rclone config only used when STORAGE_BACKEND=s3 (mount skipped in local mode). # The remote spec ("s3:") is derived in # common.storage.rclone_remote_spec(); no REMOTE_BUCKET env needed. RCLONE_CONFIG_S3_TYPE: s3 RCLONE_CONFIG_S3_PROVIDER: Other RCLONE_CONFIG_S3_ACCESS_KEY_ID: ${S3_ACCESS_KEY:-} RCLONE_CONFIG_S3_SECRET_ACCESS_KEY: ${S3_SECRET_KEY:-} RCLONE_CONFIG_S3_ENDPOINT: ${S3_ENDPOINT:-http://s3:9000} RCLONE_CONFIG_S3_ENV_AUTH: "false" RCLONE_CONFIG_S3_FORCE_PATH_STYLE: "true" RCLONE_CONFIG_S3_REGION: other UV_OFFLINE: "1" UV_NO_SYNC: "1" APP_CONFIG_SECRET_KEY: ${APP_CONFIG_SECRET_KEY:?APP_CONFIG_SECRET_KEY is required} volumes: - ${PWD}:/app - ./data:/data - /app/.venv healthcheck: # Two-mode: s3 needs rclone FUSE mount; local skips rclone and uses a # bind mount instead. In s3 mode the grep succeeds (mount must be up or # startup would have crashed). In local mode the grep fails (no fuse.rclone) # so the || branch runs: just verify /data/workspace is a directory. test: [ "CMD-SHELL", "curl -fsS http://127.0.0.1:8000/api/v1/health >/dev/null" ] interval: 10s timeout: 5s retries: 18 start_period: 30s schedule: build: context: . dockerfile: schedule/Dockerfile image: model-development-schedule:latest logging: driver: json-file options: max-size: "200m" max-file: "10" restart: unless-stopped # No host port: architecture §2.2 — only Nginx is externally reachable. # No local-FS volume: schedule executes nodes via tempfile.TemporaryDirectory # under Python's default temp dir (cleaned per-run); artifacts live in S3. environment: DATABASE_URL: ${DATABASE_URL:?DATABASE_URL is required} SERVICE_NAME: schedule-executor SCHEDULE_EVENT_NAMESPACE: ${SCHEDULE_EVENT_NAMESPACE:-model-platform-local} BACKEND_API_URL: http://backend:8000 # P0-1 fix: must match the backend's INTERNAL_SERVICE_TOKEN exactly. INTERNAL_SERVICE_TOKEN: ${INTERNAL_SERVICE_TOKEN:?INTERNAL_SERVICE_TOKEN is required} STORAGE_BACKEND: ${STORAGE_BACKEND:-s3} LOCAL_STORAGE_BASE_DIR: ${LOCAL_STORAGE_BASE_DIR:-/data} S3_ENDPOINT: ${S3_ENDPOINT:-http://s3:9000} S3_ACCESS_KEY: ${S3_ACCESS_KEY:-} S3_SECRET_KEY: ${S3_SECRET_KEY:-} S3_WORKSPACE_BUCKET: ${S3_WORKSPACE_BUCKET:-workspace} S3_VERSION_BUCKET: ${S3_VERSION_BUCKET:-version} S3_RUN_LOG_BUCKET: ${S3_RUN_LOG_BUCKET:-run-log} # Backend's health endpoint already verifies MySQL; the scheduler only # needs MySQL and Backend to be ready in either local or S3 mode. READINESS_TARGETS: ${MYSQL_HOST:?MYSQL_HOST is required}:${MYSQL_PORT:-3306},backend:8000 UV_OFFLINE: "1" UV_NO_SYNC: "1" APP_CONFIG_SECRET_KEY: ${APP_CONFIG_SECRET_KEY:?APP_CONFIG_SECRET_KEY is required} depends_on: backend: condition: service_healthy volumes: - ${PWD}:/app - ./data:/data - /app/.venv healthcheck: test: [ "CMD-SHELL", "curl -fsS http://127.0.0.1:8000/health/ready >/dev/null" ] interval: 10s timeout: 5s retries: 18 start_period: 20s