name: roomote # One shared application image ships web, api, controller, bullmq, # preview-proxy, and the db-migrate one-shot. db-migrate owns the build (it # runs first); every other service runs the same image with a different # entrypoint-dispatcher command (see .docker/app/entrypoint.sh). x-roomote-app-image: &roomote-app-image roomote-app:self-host x-roomote-env: &roomote-env NODE_ENV: development R_APP_ENV: development ROOMOTE_DOCKER_LOAD_ENV_FILE: 'false' HOST: 0.0.0.0 DATABASE_URL: postgres://postgres:password@postgres:5432/roomote_development REDIS_URL: redis://redis:6379 S3_ENDPOINT: ${S3_ENDPOINT:-http://minio:9000} S3_PRESIGN_ENDPOINT: ${S3_PRESIGN_ENDPOINT:-http://minio:9000} S3_REGION: ${S3_REGION:-us-east-1} S3_ACCESS_KEY_ID: ${S3_ACCESS_KEY_ID:-roomote} S3_SECRET_ACCESS_KEY: ${S3_SECRET_ACCESS_KEY:-roomote-local-artifacts-password} S3_BUCKET_ARTIFACTS: ${S3_BUCKET_ARTIFACTS:-roomote-artifacts} R_APP_URL: ${R_PUBLIC_URL:-http://localhost:13000} R_INSTANCE_ID: ${R_INSTANCE_ID:-} R_CURATED_INTEGRATIONS_DISABLED: ${R_CURATED_INTEGRATIONS_DISABLED:-false} # The Brain: optional provider keys for the shared memory agents consult. # They power the brain service's embeddings and synthesis; an enabled Brain # otherwise falls back to the deployment's regular provider key. Admins turn # Memory on or off in Settings > Memory. Until that toggle has been set, a # key here also turns Memory on (legacy behavior). R_BRAIN_OPENROUTER_API_KEY: ${R_BRAIN_OPENROUTER_API_KEY:-} R_BRAIN_OPENAI_API_KEY: ${R_BRAIN_OPENAI_API_KEY:-} # Shared secret the Brain presents to /api/brain/inference. REQUIRED to # enable the Brain: a provider key alone leaves it unable to embed. The # installer generates one; set any 32+ random characters by hand. R_BRAIN_GATEWAY_TOKEN: ${R_BRAIN_GATEWAY_TOKEN:-} # Which models the Brain runs, in the configured provider's own naming. # The synthesis model is free to change; the embedding pair is create-time. R_BRAIN_MODEL: ${R_BRAIN_MODEL:-} R_BRAIN_EMBEDDING_MODEL: ${R_BRAIN_EMBEDDING_MODEL:-} R_BRAIN_EMBEDDING_DIMENSIONS: ${R_BRAIN_EMBEDDING_DIMENSIONS:-} R_GBRAIN_URL: ${R_GBRAIN_URL:-http://gbrain:8931} # Roomote reads the brain's bootstrap token once to register its own # scoped clients; api and bullmq mount the brain volume read-only. R_GBRAIN_ADMIN_TOKEN_FILE: /gbrain-data/admin-bootstrap-token # Written by the Brain on first boot when no token was supplied, so a stack # started by hand needs only a provider key. R_BRAIN_GATEWAY_TOKEN_FILE: /gbrain-data/gateway-token TRPC_URL: http://api:3001 PREVIEW_PROXY_BASE_URL: ${PREVIEW_PROXY_BASE_URL:-http://localhost:18081} PREVIEW_DOMAINS: ${PREVIEW_DOMAINS:-localhost,127.0.0.1,roomotepreview.localhost} R_PUBLIC_URL: ${R_PUBLIC_URL:-} R_ALLOWED_EMAILS: ${R_ALLOWED_EMAILS:-} R_LICENSE_KEY: ${R_LICENSE_KEY:-} R_MODEL: ${R_MODEL:-} # Decision model. Unset, a deployment uses Jev when a TypeSafe key is # configured and otherwise none. R_JUDGMENT_UPSTREAM_URL points at any # server that speaks the decision contract, such as the `judgment` profile's # sidecar below (http://judgment:8080). R_JUDGMENT_MODEL: ${R_JUDGMENT_MODEL:-} R_JUDGMENT_UPSTREAM_URL: ${R_JUDGMENT_UPSTREAM_URL:-} R_JUDGMENT_UPSTREAM_API_KEY: ${R_JUDGMENT_UPSTREAM_API_KEY:-} R_SMALL_MODEL: ${R_SMALL_MODEL:-} R_CODE_REVIEW_MODEL: ${R_CODE_REVIEW_MODEL:-} R_EXPLORE_MODEL: ${R_EXPLORE_MODEL:-} R_MODEL_ENV_KEYS: ${R_MODEL_ENV_KEYS:-} CUSTOM_PROVIDER_API_KEY: ${CUSTOM_PROVIDER_API_KEY:-} OPENROUTER_API_KEY: ${OPENROUTER_API_KEY:-} AI_GATEWAY_API_KEY: ${AI_GATEWAY_API_KEY:-} OPENAI_API_KEY: ${OPENAI_API_KEY:-} ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY:-} DEEPSEEK_API_KEY: ${DEEPSEEK_API_KEY:-} GEMINI_API_KEY: ${GEMINI_API_KEY:-} GOOGLE_GENERATIVE_AI_API_KEY: ${GOOGLE_GENERATIVE_AI_API_KEY:-} AWS_BEARER_TOKEN_BEDROCK: ${AWS_BEARER_TOKEN_BEDROCK:-} AWS_REGION: ${AWS_REGION:-} MOONSHOT_API_KEY: ${MOONSHOT_API_KEY:-} KIMI_API_KEY: ${KIMI_API_KEY:-} MINIMAX_API_KEY: ${MINIMAX_API_KEY:-} ZAI_API_KEY: ${ZAI_API_KEY:-} ZAI_REGION: ${ZAI_REGION:-} ZAI_CODING_PLAN_API_KEY: ${ZAI_CODING_PLAN_API_KEY:-} ZAI_CODING_PLAN_REGION: ${ZAI_CODING_PLAN_REGION:-} OPENCODE_API_KEY: ${OPENCODE_API_KEY:-} OPENCODE_GO_API_KEY: ${OPENCODE_GO_API_KEY:-} BASETEN_API_KEY: ${BASETEN_API_KEY:-} TOGETHER_API_KEY: ${TOGETHER_API_KEY:-} GITHUB_TOKEN: ${GITHUB_TOKEN:-} R_GITHUB_APP_ID: ${R_GITHUB_APP_ID:-} R_GITHUB_APP_PRIVATE_KEY: ${R_GITHUB_APP_PRIVATE_KEY:-} R_GITHUB_CLIENT_ID: ${R_GITHUB_CLIENT_ID:-} R_GITHUB_CLIENT_SECRET: ${R_GITHUB_CLIENT_SECRET:-} R_GITHUB_WEBHOOK_SECRET: ${R_GITHUB_WEBHOOK_SECRET:-} SLACK_APP_ID: ${SLACK_APP_ID:-} R_SLACK_CLIENT_ID: ${R_SLACK_CLIENT_ID:-} R_SLACK_CLIENT_SECRET: ${R_SLACK_CLIENT_SECRET:-} R_SLACK_SIGNING_SECRET: ${R_SLACK_SIGNING_SECRET:-} R_TELEGRAM_BOT_TOKEN: ${R_TELEGRAM_BOT_TOKEN:-} R_TELEGRAM_WEBHOOK_SECRET: ${R_TELEGRAM_WEBHOOK_SECRET:-} R_DISCORD_BOT_TOKEN: ${R_DISCORD_BOT_TOKEN:-} R_DISCORD_GATEWAY_SECRET: ${R_DISCORD_GATEWAY_SECRET:-} R_LINEAR_CLIENT_ID: ${R_LINEAR_CLIENT_ID:-} R_LINEAR_CLIENT_SECRET: ${R_LINEAR_CLIENT_SECRET:-} R_LINEAR_WEBHOOK_SECRET: ${R_LINEAR_WEBHOOK_SECRET:-} R_GITHUB_APP_SLUG: ${R_GITHUB_APP_SLUG:-} R_GITHUB_ADDITIONAL_APP_SLUGS: ${R_GITHUB_ADDITIONAL_APP_SLUGS:-} SETUP_TOKEN: ${SETUP_TOKEN:-} DEFAULT_COMPUTE_PROVIDER: ${DEFAULT_COMPUTE_PROVIDER:-docker} BL_API_KEY: ${BL_API_KEY:-} BL_WORKSPACE: ${BL_WORKSPACE:-} BLAXEL_IMAGE: ${BLAXEL_IMAGE:-} BLAXEL_REGION: ${BLAXEL_REGION:-} BLAXEL_STANDBY_MAX_COUNT: ${BLAXEL_STANDBY_MAX_COUNT:-25} BLAXEL_STANDBY_MAX_AGE_HOURS: ${BLAXEL_STANDBY_MAX_AGE_HOURS:-168} BOX_API_KEY: ${BOX_API_KEY:-} BOX_API_BASE_URL: ${BOX_API_BASE_URL:-} BOX_MACHINE_TYPE: ${BOX_MACHINE_TYPE:-} BOX_TIMEOUT_MS: ${BOX_TIMEOUT_MS:-} BOX_STANDBY_MAX_COUNT: ${BOX_STANDBY_MAX_COUNT:-25} BOX_STANDBY_MAX_AGE_HOURS: ${BOX_STANDBY_MAX_AGE_HOURS:-168} AZURE_SUBSCRIPTION_ID: ${AZURE_SUBSCRIPTION_ID:-} AZURE_RESOURCE_GROUP: ${AZURE_RESOURCE_GROUP:-} AZURE_SANDBOX_GROUP: ${AZURE_SANDBOX_GROUP:-} AZURE_SANDBOX_REGION: ${AZURE_SANDBOX_REGION:-} AZURE_SANDBOX_DISK_IMAGE: ${AZURE_SANDBOX_DISK_IMAGE:-} AZURE_CLIENT_ID: ${AZURE_CLIENT_ID:-} AZURE_TENANT_ID: ${AZURE_TENANT_ID:-} AZURE_CLIENT_SECRET: ${AZURE_CLIENT_SECRET:-} AZURE_SANDBOX_REGISTRY_USERNAME: ${AZURE_SANDBOX_REGISTRY_USERNAME:-} AZURE_SANDBOX_REGISTRY_TOKEN: ${AZURE_SANDBOX_REGISTRY_TOKEN:-} AZURE_SANDBOX_SIZE: ${AZURE_SANDBOX_SIZE:-} AZURE_SANDBOX_EGRESS_INSPECTION: ${AZURE_SANDBOX_EGRESS_INSPECTION:-} AZURE_HTTP_DEBUG: ${AZURE_HTTP_DEBUG:-} DOCKER_STANDBY_MAX_COUNT: ${DOCKER_STANDBY_MAX_COUNT:-10} DOCKER_STANDBY_MAX_AGE_HOURS: ${DOCKER_STANDBY_MAX_AGE_HOURS:-24} # Declarative environment provisioning: point ROOMOTE_ENVIRONMENTS_DIR at a # directory of environment YAML files mounted into the api container (for # example `- ./environments:/roomote/environments:ro` on the api service), # or put inline multi-document YAML in ROOMOTE_ENVIRONMENTS_YAML. Only the # api service reads these; see SELF_HOSTING.md. ROOMOTE_ENVIRONMENTS_DIR: ${ROOMOTE_ENVIRONMENTS_DIR:-} ROOMOTE_ENVIRONMENTS_YAML: ${ROOMOTE_ENVIRONMENTS_YAML:-} x-roomote-service: &roomote-service restart: unless-stopped depends_on: postgres: condition: service_healthy redis: condition: service_started minio-init: condition: service_completed_successfully db-migrate: condition: service_completed_successfully environment: <<: *roomote-env services: db-migrate: restart: 'no' container_name: roomote-db-migrate image: *roomote-app-image build: context: . dockerfile: .docker/app/Dockerfile args: APP_ENV: development RELEASE_VERSION: ${RELEASE_VERSION:-self-host-local} environment: <<: *roomote-env depends_on: postgres: condition: service_healthy command: ['db-migrate'] web: <<: *roomote-service container_name: roomote-web image: *roomote-app-image command: ['web'] # Publish app ports to loopback only. Standalone self-host is meant to sit # behind a reverse proxy on the same host (or the production Caddy overlay, # which resets these to none); binding to all interfaces would expose the # web app, API, queue dashboard, and preview proxy directly. ports: - '127.0.0.1:13000:3000' depends_on: postgres: condition: service_healthy redis: condition: service_started minio-init: condition: service_completed_successfully db-migrate: condition: service_completed_successfully api: condition: service_started environment: <<: *roomote-env NODE_ENV: production api: <<: *roomote-service container_name: roomote-api image: *roomote-app-image command: ['api'] # Reachable from task worker containers (worker -> API over TRPC_URL). networks: [default, worker] environment: <<: *roomote-env PORT: 3001 volumes: - gbrain_data:/gbrain-data:ro ports: - '127.0.0.1:13001:3001' controller: <<: *roomote-service container_name: roomote-controller image: *roomote-app-image command: ['controller'] environment: <<: *roomote-env SANDBOX_OPENROUTER_API_KEY: ${SANDBOX_OPENROUTER_API_KEY:-} # Spawns and reaches task worker containers on the worker network. networks: [default, worker] stop_grace_period: 7m depends_on: postgres: condition: service_healthy redis: condition: service_started minio-init: condition: service_completed_successfully db-migrate: condition: service_completed_successfully api: condition: service_started healthcheck: test: [ 'CMD-SHELL', 'curl -fsS --max-time 5 http://api:3001/health/controller >/dev/null', ] interval: 5s timeout: 6s retries: 12 start_period: 20s bullmq: <<: *roomote-service container_name: roomote-bullmq image: *roomote-app-image command: ['bullmq'] environment: <<: *roomote-env PORT: 3002 volumes: - gbrain_data:/gbrain-data:ro ports: - '127.0.0.1:13002:3002' healthcheck: test: [ 'CMD-SHELL', 'curl -fsS --max-time 5 http://127.0.0.1:3002/admin/health >/dev/null', ] interval: 5s timeout: 6s retries: 12 start_period: 20s preview-proxy: <<: *roomote-service container_name: roomote-preview-proxy image: *roomote-app-image command: ['preview-proxy'] # Proxies preview traffic into task worker containers. networks: [default, worker] environment: <<: *roomote-env PORT: 8081 ports: - '127.0.0.1:18081:8081' # Brain (deployment-hosted gbrain). Opt-in via # COMPOSE_PROFILES=brain; deployments that never enable the # profile see no infrastructure change. Internal-only: no published # ports, reachable solely by the API service, which fronts it at # /api/mcp/gbrain with the read-only agent credential. Wire static tokens # into api/bullmq (operator-pinned), or the normal path: api/bullmq already # point R_GBRAIN_URL at http://gbrain:8931 and read the admin bootstrap token # from the shared volume, so Roomote registers its own scoped clients via # the admin API once Memory is enabled in Settings > Memory. Set # GBRAIN_ADMIN_BOOTSTRAP_TOKEN below (>= 32 chars of [A-Za-z0-9_-]) to pin # the token; if left unset, the entrypoint generates one at # /data/admin-bootstrap-token on the volume. gbrain: profiles: [brain] container_name: roomote-gbrain build: .docker/gbrain restart: unless-stopped depends_on: postgres: condition: service_healthy networks: [default] environment: GBRAIN_DATABASE_URL: postgres://postgres:password@postgres:5432/roomote_development GBRAIN_ADMIN_BOOTSTRAP_TOKEN: ${GBRAIN_ADMIN_BOOTSTRAP_TOKEN:-} # The Brain holds no provider key. It calls back into this deployment, # which injects whichever provider key an admin configured in Settings, # so rotating or switching that key needs no restart here. Setting a # provider key below instead makes the Brain call the provider directly. OPENAI_BASE_URL: ${GBRAIN_OPENAI_BASE_URL:-http://api:3001/api/brain/inference} OPENAI_API_KEY: ${R_BRAIN_GATEWAY_TOKEN:-} # Create-time, and it has to be the container that receives these: the # embedding model and its width are decided when the Brain is created, # and the gateway must never substitute a different one afterwards. # Set both before the first boot, or neither. The synthesis model is # the opposite (R_BRAIN_MODEL, applied by the gateway per request). GBRAIN_EMBEDDING_MODEL: ${R_BRAIN_EMBEDDING_MODEL:-} GBRAIN_EMBEDDING_DIMENSIONS: ${R_BRAIN_EMBEDDING_DIMENSIONS:-} volumes: - gbrain_data:/data # Optional CPU decision model (judgment sidecar): the Memory check, reply # addressing, and Roomote's other typed decisions without a Jev key or a # GPU. Enable with COMPOSE_PROFILES=judgment and point Roomote at it with # R_JUDGMENT_UPSTREAM_URL=http://judgment:8080. It serves # roomote/roomote-judgment-gliner unless JUDGMENT_MODEL names another # GLiNER 2.5 checkpoint. The memory and CPU caps bound what inference can # take from the host. judgment: profiles: [judgment] container_name: roomote-judgment build: .docker/judgment restart: unless-stopped networks: [default] environment: JUDGMENT_MODEL: ${JUDGMENT_MODEL:-roomote/roomote-judgment-gliner} HF_TOKEN: ${JUDGMENT_HF_TOKEN:-} JUDGMENT_API_KEY: ${R_JUDGMENT_UPSTREAM_API_KEY:-} JUDGMENT_THREADS: ${JUDGMENT_CPUS:-4} mem_limit: ${JUDGMENT_MEMORY_LIMIT:-4g} cpus: ${JUDGMENT_CPUS:-4} volumes: - judgment_models:/models networks: # The controller uses this network to discover the API and optional preview # proxy. Each task gets a separate bridge network; only the API and an # enabled preview proxy are attached to it, so task sandboxes never share a # network with one another or with Postgres, Redis, or MinIO. The default # network is implicit (roomote_default) and carries the datastores. worker: name: roomote_worker driver: bridge volumes: gbrain_data: name: roomote_gbrain_data judgment_models: name: roomote_judgment_models