docker-compose.yml
Rama main · réplica de lectura del repositorio original.
# Self-host / prod stack. Boot the core with: docker compose --profile full up --build
# Convert a model (job): docker compose run --rm converter /data/model.ifc /data/model.frag
#
# Resource limits (opt-in): every service below has a commented-out `deploy.resources.limits`
# block keyed off env vars (AEC_*_MEM_LIMIT / AEC_*_CPUS — see .env.example for sizing guidance).
# They are OFF by default on purpose: a hard memory cap that is too low will OOM-kill a legitimate
# large-IFC conversion mid-run. Uncomment and set values sized for YOUR host before enabling.
# `deploy.resources.limits` is honored by `docker compose up` (v2); no Swarm required.
services:
postgres:
image: postgres:17
environment:
POSTGRES_USER: "${POSTGRES_USER:-bim}"
POSTGRES_PASSWORD: "${POSTGRES_PASSWORD:-bim}"
POSTGRES_DB: "${POSTGRES_DB:-bim}"
ports: ["5432:5432"]
volumes: ["postgres-data:/var/lib/postgresql/data"]
healthcheck:
test: ["CMD-SHELL", "pg_isready -U bim"]
interval: 5s
timeout: 3s
retries: 10
# deploy: # opt-in cap — uncomment + size for your host
# resources:
# limits:
# memory: "${AEC_PG_MEM_LIMIT:-2g}"
minio:
# Pinned. An unpinned `minio/minio` resolves to :latest, so two developers -- or a developer and
# CI -- can silently run different object stores, and a breaking upstream release arrives without
# a commit. Dependabot's docker ecosystem keeps the pins fresh via PRs.
image: minio/minio:RELEASE.2025-04-22T22-12-26Z
command: server /data --console-address ":9001"
environment:
MINIO_ROOT_USER: "${S3_ACCESS_KEY:-minioadmin}"
MINIO_ROOT_PASSWORD: "${S3_SECRET_KEY:-minioadmin}"
ports: ["9000:9000", "9001:9001"]
volumes: ["minio-data:/data"]
healthcheck:
test: ["CMD", "mc", "ready", "local"]
interval: 5s
timeout: 3s
retries: 10
# deploy: # opt-in cap — uncomment + size for your host
# resources:
# limits:
# memory: "${AEC_MINIO_MEM_LIMIT:-1g}"
api:
image: massing-api
build:
context: .
dockerfile: services/api/Dockerfile
depends_on:
postgres: { condition: service_healthy }
minio: { condition: service_healthy }
environment: &api-env
DATABASE_URL: "postgresql+psycopg://${POSTGRES_USER:-bim}:${POSTGRES_PASSWORD:-bim}@postgres:5432/${POSTGRES_DB:-bim}"
S3_ENDPOINT: http://minio:9000
S3_ACCESS_KEY: "${S3_ACCESS_KEY:-minioadmin}"
S3_SECRET_KEY: "${S3_SECRET_KEY:-minioadmin}"
S3_BUCKET: "${S3_BUCKET:-aec-bim}"
AEC_RBAC: "${AEC_RBAC:-0}" # set 1 to enforce project roles
AEC_API_KEY: "${AEC_API_KEY:-}" # bearer token = admin (set in prod)
AEC_AUTH_SECRET: "${AEC_AUTH_SECRET:-}" # signs login tokens (falls back to AEC_API_KEY)
AEC_CORS_ORIGINS: "${AEC_CORS_ORIGINS:-http://localhost:5173}"
UVICORN_WORKERS: "${UVICORN_WORKERS:-4}" # multi-worker (Postgres) so a reconvert can't freeze the API
# JOB-WORKER-SPLIT: the heavy job kinds (COBie export, bundles, generative runs) are full-model
# IFC parses. Run inside this container they compete for CPU and memory with every request it is
# serving, so the compose stack runs them in the `worker` service below instead. Set this to
# `inline` to go back to one container doing both — the code default is inline precisely so a
# deployment that is NOT using this file is unaffected.
AEC_JOB_WORKER: "${AEC_JOB_WORKER:-off}"
APS_CLIENT_ID: "${APS_CLIENT_ID:-}" # optional paid Autodesk RVT bridge
APS_CLIENT_SECRET: "${APS_CLIENT_SECRET:-}"
ports: ["8000:8000"]
volumes: ["ifc-data:/app/ifc"] # uploaded source IFCs persist across restarts
healthcheck:
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')"]
interval: 5s
timeout: 3s
retries: 20
profiles: ["full"]
# deploy: # opt-in cap — uncomment + size for your host
# resources: # keep generous: UVICORN_WORKERS=4 + an in-process reconvert
# limits:
# memory: "${AEC_API_MEM_LIMIT:-4g}"
# cpus: "${AEC_API_CPUS:-2}"
# JOB-WORKER-SPLIT — the durable queue, in its own container. Same image and same environment as
# the API (the `&api-env` anchor above, so the two cannot drift apart), different command.
#
# Scale it independently of request serving, which is the entire point:
# docker compose --profile full up -d --scale worker=3
# That is safe without any further change because job claim is a compare-and-swap and `pid_lock`
# takes a Postgres advisory lock — two workers cannot both win one job, and a mutating job still
# serialises against a concurrent API edit on the same project.
#
# No ports and no healthcheck: it serves nothing, so there is nothing to probe. "Is the queue
# moving?" is a question about the oldest `queued` row, not about this container being up — a
# wedged worker and an idle one look identical from outside.
worker:
image: massing-api
build:
context: .
dockerfile: services/api/Dockerfile
depends_on:
postgres: { condition: service_healthy }
minio: { condition: service_healthy }
environment:
<<: *api-env
AEC_JOB_WORKER: "inline" # this process IS the worker; never `off` or it does nothing at all
command: ["python", "-m", "aec_api.worker"]
# The image bakes a HEALTHCHECK that curls localhost:8000/health. This service binds no port and
# serves nothing, so that probe can never pass and the container sits permanently `unhealthy` —
# which is worse than no signal, because it trains whoever is watching to ignore the column. The
# comment above claimed there was no healthcheck; there was, inherited from the image.
healthcheck: { disable: true }
volumes: ["ifc-data:/app/ifc"] # same uploaded-IFC volume as the API — jobs re-parse those files
profiles: ["full"]
# deploy: # opt-in cap — size for your host; this is where the heavy parses
# resources: # now live, so it wants the generous limit the API no longer needs
# limits:
# memory: "${AEC_WORKER_MEM_LIMIT:-4g}"
# cpus: "${AEC_WORKER_CPUS:-2}"
# one-shot demo seeder (api must be enabled, so pass both profiles):
# docker compose --profile full --profile seed run --rm seed
# DEV/TEST ONLY — never run against production. The script refuses if projects already exist
# (see docs/PRODUCTION_CHECKLIST.md); --force overrides only for labs.
seed:
image: massing-api
depends_on:
api: { condition: service_healthy }
command: ["python", "/app/services/api/seed_demo.py", "--url", "http://api:8000", "--user", "gc"]
profiles: ["seed"]
web:
build:
context: .
dockerfile: apps/web/Dockerfile
depends_on:
api: { condition: service_started }
ports: ["${WEB_PORT:-8080}:80"]
profiles: ["full"]
# deploy: # opt-in cap — uncomment + size for your host
# resources: # nginx serving static assets — modest footprint
# limits:
# memory: "${AEC_WEB_MEM_LIMIT:-256m}"
converter:
build:
context: .
dockerfile: services/converter/Dockerfile
volumes: ["./data:/data"] # drop IFCs here; .frag written here
profiles: ["tools"] # job-style: `docker compose run --rm converter ...`
# deploy: # opt-in cap — uncomment + size for your host
# resources: # IFC→Fragments is memory-hungry: size well above your
# limits: # largest model and against AEC_PUBLISH_WORKERS (PR #70),
# memory: "${AEC_CONVERTER_MEM_LIMIT:-8g}" # since parallel jobs each hold a model in RAM.
# cpus: "${AEC_CONVERTER_CPUS:-4}"
volumes:
postgres-data:
minio-data:
ifc-data: