Files
model-platform/docker-compose.yml
T
2026-08-24 13:57:46 +08:00

233 lines
8.7 KiB
YAML

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:
- ${PWD}/frontend:/usr/share/nginx/html
- ./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:<workspace_bucket>") 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