services: frontend: build: context: . dockerfile: Dockerfile.frontend args: VITE_PROJECTBRAIN_API_URL: ${VITE_PROJECTBRAIN_API_URL:-} VITE_PROJECTBRAIN_CLIENT_MODE: ${VITE_PROJECTBRAIN_CLIENT_MODE:-real} VITE_PROJECTBRAIN_RELEASE_TAG: ${VITE_PROJECTBRAIN_RELEASE_TAG:-} VITE_PROJECTBRAIN_COMMIT: ${VITE_PROJECTBRAIN_COMMIT:-} VITE_PROJECTBRAIN_BRANCH: ${VITE_PROJECTBRAIN_BRANCH:-} VITE_PROJECTBRAIN_BUILD_TIME: ${VITE_PROJECTBRAIN_BUILD_TIME:-} VITE_PROJECTBRAIN_SOURCE: ${VITE_PROJECTBRAIN_SOURCE:-} container_name: projectbrain-frontend depends_on: backend: condition: service_healthy ports: - "${PROJECTBRAIN_WEB_PORT:-1209}:8080" restart: unless-stopped backend: image: projectbrain-api:local build: context: . dockerfile: backend/Dockerfile container_name: projectbrain-api depends_on: postgres: condition: service_healthy environment: NODE_ENV: production HOST: 0.0.0.0 PORT: 4000 LOG_LEVEL: ${LOG_LEVEL:-info} DATABASE_URL: ${DATABASE_URL:?Set DATABASE_URL to the production PostgreSQL connection string} PROJECTBRAIN_DATA_SOURCE: ${PROJECTBRAIN_DATA_SOURCE:-database} PROJECTBRAIN_SEED_ON_START: ${PROJECTBRAIN_SEED_ON_START:-true} PROJECTBRAIN_OWNER_USERNAME: ${PROJECTBRAIN_OWNER_USERNAME:-owner} PROJECTBRAIN_OWNER_PASSWORD: ${PROJECTBRAIN_OWNER_PASSWORD:?Set PROJECTBRAIN_OWNER_PASSWORD to a unique secret} PROJECTBRAIN_AUTH_SECRET: ${PROJECTBRAIN_AUTH_SECRET:?Set PROJECTBRAIN_AUTH_SECRET to at least 32 random characters} PROJECTBRAIN_SESSION_TTL_HOURS: ${PROJECTBRAIN_SESSION_TTL_HOURS:-24} # The API is reachable through the frontend proxy only. Trusting that # hop preserves the real client IP for rate limiting and audit events. PROJECTBRAIN_TRUST_PROXY: ${PROJECTBRAIN_TRUST_PROXY:-true} PROJECTBRAIN_GITEA_URL: ${PROJECTBRAIN_GITEA_URL:-} PROJECTBRAIN_GITEA_TOKEN: ${PROJECTBRAIN_GITEA_TOKEN:-} PROJECTBRAIN_GITEA_WRITE_TOKEN: ${PROJECTBRAIN_GITEA_WRITE_TOKEN:-} PROJECTBRAIN_GITEA_WEBHOOK_SECRET: ${PROJECTBRAIN_GITEA_WEBHOOK_SECRET:-} PROJECTBRAIN_APPOPS_MCP_URL: ${PROJECTBRAIN_APPOPS_MCP_URL:-} PROJECTBRAIN_APPOPS_MCP_TOKEN: ${PROJECTBRAIN_APPOPS_MCP_TOKEN:-} PROJECTBRAIN_MCP_ENABLED: ${PROJECTBRAIN_MCP_ENABLED:-false} PROJECTBRAIN_MCP_RATE_LIMIT_PER_MINUTE: ${PROJECTBRAIN_MCP_RATE_LIMIT_PER_MINUTE:-1200} PROJECTBRAIN_AUTO_ONBOARD_REPOSITORIES: ${PROJECTBRAIN_AUTO_ONBOARD_REPOSITORIES:-true} PROJECTBRAIN_REPOSITORY_RECONCILE_SECONDS: ${PROJECTBRAIN_REPOSITORY_RECONCILE_SECONDS:-60} PROJECTBRAIN_AUTONOMY_EXECUTOR_STALE_SECONDS: ${PROJECTBRAIN_AUTONOMY_EXECUTOR_STALE_SECONDS:-5400} PROJECTBRAIN_RUNNER_CLAIM_STALL_SECONDS: ${PROJECTBRAIN_RUNNER_CLAIM_STALL_SECONDS:-120} PROJECTBRAIN_HUB_SERVICE_TOKEN: ${PROJECTBRAIN_HUB_SERVICE_TOKEN:-} PROJECTBRAIN_PUBLIC_URL: ${PROJECTBRAIN_PUBLIC_URL:-} PROJECTBRAIN_ENTRA_TENANT_ID: ${PROJECTBRAIN_ENTRA_TENANT_ID:-} PROJECTBRAIN_ENTRA_CLIENT_ID: ${PROJECTBRAIN_ENTRA_CLIENT_ID:-} PROJECTBRAIN_ENTRA_CLIENT_SECRET: ${PROJECTBRAIN_ENTRA_CLIENT_SECRET:-} healthcheck: test: ["CMD", "node", "-e", "fetch('http://127.0.0.1:4000/api/system/readiness').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"] interval: 15s timeout: 5s retries: 10 start_period: 30s restart: unless-stopped analysis-worker: image: projectbrain-api:local build: context: . dockerfile: backend/Dockerfile container_name: projectbrain-analysis-worker command: ["node", "dist/workers/analysisWorker.js"] depends_on: backend: condition: service_healthy environment: NODE_ENV: production HOST: 0.0.0.0 DATABASE_URL: ${DATABASE_URL:?Set DATABASE_URL to the production PostgreSQL connection string} PROJECTBRAIN_DATA_SOURCE: database PROJECTBRAIN_OWNER_USERNAME: ${PROJECTBRAIN_OWNER_USERNAME:-owner} PROJECTBRAIN_OWNER_PASSWORD: ${PROJECTBRAIN_OWNER_PASSWORD:?Set PROJECTBRAIN_OWNER_PASSWORD to a unique secret} PROJECTBRAIN_AUTH_SECRET: ${PROJECTBRAIN_AUTH_SECRET:?Set PROJECTBRAIN_AUTH_SECRET to at least 32 random characters} PROJECTBRAIN_GITEA_URL: ${PROJECTBRAIN_GITEA_URL:-} PROJECTBRAIN_GITEA_TOKEN: ${PROJECTBRAIN_GITEA_TOKEN:-} PROJECTBRAIN_GITEA_WRITE_TOKEN: ${PROJECTBRAIN_GITEA_WRITE_TOKEN:-} PROJECTBRAIN_GITEA_WEBHOOK_SECRET: ${PROJECTBRAIN_GITEA_WEBHOOK_SECRET:-} PROJECTBRAIN_MCP_ENABLED: ${PROJECTBRAIN_MCP_ENABLED:-false} PROJECTBRAIN_MCP_RATE_LIMIT_PER_MINUTE: ${PROJECTBRAIN_MCP_RATE_LIMIT_PER_MINUTE:-1200} PROJECTBRAIN_PUBLIC_URL: ${PROJECTBRAIN_PUBLIC_URL:-} PROJECTBRAIN_ANALYSIS_WORKER_PORT: 4001 PROJECTBRAIN_ANALYSIS_POLL_MS: ${PROJECTBRAIN_ANALYSIS_POLL_MS:-1000} PROJECTBRAIN_ANALYSIS_BATCH_SIZE: ${PROJECTBRAIN_ANALYSIS_BATCH_SIZE:-5} PROJECTBRAIN_ANALYSIS_REPOSITORY_CONCURRENCY: ${PROJECTBRAIN_ANALYSIS_REPOSITORY_CONCURRENCY:-2} PROJECTBRAIN_ANALYSIS_JOB_TIMEOUT_SECONDS: ${PROJECTBRAIN_ANALYSIS_JOB_TIMEOUT_SECONDS:-1800} PROJECTBRAIN_ANALYSIS_STALE_SECONDS: ${PROJECTBRAIN_ANALYSIS_STALE_SECONDS:-300} PROJECTBRAIN_ANALYSIS_SCHEDULE_MINUTES: ${PROJECTBRAIN_ANALYSIS_SCHEDULE_MINUTES:-1440} PROJECTBRAIN_AUTO_ONBOARD_REPOSITORIES: ${PROJECTBRAIN_AUTO_ONBOARD_REPOSITORIES:-true} PROJECTBRAIN_REPOSITORY_RECONCILE_SECONDS: ${PROJECTBRAIN_REPOSITORY_RECONCILE_SECONDS:-60} NODE_OPTIONS: --max-old-space-size=${PROJECTBRAIN_ANALYSIS_WORKER_HEAP_MB:-384} healthcheck: test: ["CMD", "node", "-e", "fetch('http://127.0.0.1:4001/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"] interval: 15s timeout: 5s retries: 5 start_period: 20s mem_limit: ${PROJECTBRAIN_ANALYSIS_WORKER_MEMORY_LIMIT:-768m} cpus: ${PROJECTBRAIN_ANALYSIS_WORKER_CPU_LIMIT:-1.0} pids_limit: ${PROJECTBRAIN_ANALYSIS_WORKER_PIDS_LIMIT:-128} restart: unless-stopped postgres: image: postgres:16-alpine container_name: projectbrain-postgres environment: POSTGRES_DB: ${POSTGRES_DB:-projectbrain} POSTGRES_USER: ${POSTGRES_USER:-projectbrain} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?Set POSTGRES_PASSWORD to a unique secret} volumes: - projectbrain-postgres-data:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER:-projectbrain} -d $${POSTGRES_DB:-projectbrain}"] interval: 10s timeout: 5s retries: 5 restart: unless-stopped volumes: projectbrain-postgres-data: