services: postgres: image: pgvector/pgvector:pg17 environment: POSTGRES_DB: osinara POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?POSTGRES_PASSWORD is required} POSTGRES_USER: osinara healthcheck: test: ["CMD-SHELL", "pg_isready -U osinara -d osinara"] interval: 5s timeout: 3s retries: 20 restart: unless-stopped volumes: - postgres-data:/var/lib/postgresql/data agent: build: context: . target: runtime depends_on: migrate: condition: service_completed_successfully memory-embedding: condition: service_healthy sandbox-runner: condition: service_healthy environment: DATABASE_URL: postgresql://osinara:${POSTGRES_PASSWORD}@postgres:5432/osinara ELEVENLABS_API_KEY: ${ELEVENLABS_API_KEY-} GROQ_API_KEY: ${GROQ_API_KEY-} GOOGLE_OAUTH_CLIENT_ID: ${GOOGLE_OAUTH_CLIENT_ID-} GOOGLE_OAUTH_CLIENT_SECRET: ${GOOGLE_OAUTH_CLIENT_SECRET-} INTEGRATION_TOKEN_ENCRYPTION_KEY: ${INTEGRATION_TOKEN_ENCRYPTION_KEY-} INVITATION_SIGNING_SECRET: ${INVITATION_SIGNING_SECRET:?INVITATION_SIGNING_SECRET is required} KLIPY_API_KEY: ${KLIPY_API_KEY-} MEMORY_EMBEDDING_BASE_URL: http://memory-embedding:80 MODEL_API_KEY: ${MODEL_API_KEY:?MODEL_API_KEY is required} PUBLIC_BASE_URL: ${PUBLIC_BASE_URL-} TELEGRAM_BOT_TOKEN: ${TELEGRAM_BOT_TOKEN:?TELEGRAM_BOT_TOKEN is required} TELEGRAM_BOT_USERNAME: ${TELEGRAM_BOT_USERNAME:?TELEGRAM_BOT_USERNAME is required} TELEGRAM_WEBHOOK_SECRET_TOKEN: ${TELEGRAM_WEBHOOK_SECRET_TOKEN:?TELEGRAM_WEBHOOK_SECRET_TOKEN is required} healthcheck: test: ["CMD", "node", "-e", "fetch('http://127.0.0.1:3000/v1/health').then(r=>{if(!r.ok)process.exit(1)}).catch(()=>process.exit(1))"] interval: 5s timeout: 3s retries: 72 restart: unless-stopped networks: - default - sandbox-control volumes: - ./config/agent-model-providers.json:/app/config/agent-model-providers.json:ro - google-workspace-credentials:/app/google-workspace-credentials # A host directory, not a named volume: `npm run dev` on the host writes the same grant file. - ./network-access:/app/network-access - workspace-data:/app/workspaces migrate: build: context: . target: runtime depends_on: postgres: condition: service_healthy entrypoint: ["npm", "run", "migrate:runtime"] environment: DATABASE_URL: postgresql://osinara:${POSTGRES_PASSWORD}@postgres:5432/osinara restart: "no" sandbox-runtime-image: build: context: . target: sandbox-runtime command: ["true"] image: osinara-sandbox-runtime:local network_mode: none restart: "no" sandbox-runner: build: context: . target: sandbox-runner depends_on: sandbox-egress-proxy: condition: service_healthy sandbox-runtime-image: condition: service_completed_successfully healthcheck: test: ["CMD", "node", "-e", "fetch('http://127.0.0.1:8080/health').then(r=>{if(!r.ok)process.exit(1)})"] interval: 5s timeout: 3s retries: 20 environment: SANDBOX_RUNTIME_IMAGE: osinara-sandbox-runtime:local networks: - sandbox-control read_only: true restart: unless-stopped security_opt: - no-new-privileges:true stop_grace_period: 30s tmpfs: - /tmp:size=64m,mode=1777 volumes: - /var/run/docker.sock:/var/run/docker.sock - tool-environments:/runner/tools - workspace-data:/runner/workspaces sandbox-egress-proxy: build: context: . target: sandbox-egress-proxy cap_drop: - ALL healthcheck: test: ["CMD", "node", "-e", "require('node:net').connect(3128,'127.0.0.1').on('connect',function(){this.destroy();process.exit(0)}).on('error',()=>process.exit(1))"] interval: 5s timeout: 3s retries: 20 networks: default: sandbox-egress: # Resolves only on the trusted sandbox network: the proxy finds its own address there and # opens owner-approved IP destinations only to requests that arrive on it. aliases: - sandbox-trusted-egress-proxy read_only: true restart: unless-stopped security_opt: - no-new-privileges:true tmpfs: - /tmp:size=32m,mode=1777 volumes: - ./network-access:/app/network-access:ro memory-embedding: image: ghcr.io/huggingface/text-embeddings-inference:cpu-1.9@sha256:ad950d30878eceb72aaf32024d26fa2b1d04a75304fa0b4776b49aa1941fea07 # Sized by measurement on a six-core host, not by guess. Memory is set by the math threads # alone: three cores cost eleven megabytes more than two, while a third math thread costs four # hundred and does not fit a gigabyte at all — the container is killed while loading the model. # Two threads peak at 870 MB under a saturating load. The CPU budget is separate and cheap: # tokenization and request handling use the third core without touching that peak, and it buys # 45% more indexing throughput and a third off the wait when a search queues behind a batch. cpus: 3.0 mem_limit: 1024m command: - --model-id - intfloat/multilingual-e5-small - --revision - 614241f622f53c4eeff9890bdc4f31cfecc418b3 - --tokenization-workers - "2" - --max-client-batch-size - "8" # One queue slot per input, so a client batch of eight fills a limit of eight and the search # arriving beside it is refused rather than queued — the turn silently loses its semantic # branch. Measured against a saturating indexer: at eight, 15 searches out of 60 were refused; # at sixteen, none, at no cost in memory. - --max-concurrent-requests - "16" - --max-batch-requests - "8" - --max-batch-tokens - "4096" - --auto-truncate=false environment: OMP_NUM_THREADS: "2" healthcheck: test: ["CMD", "curl", "--fail", "http://localhost/health"] interval: 5s timeout: 5s retries: 120 restart: unless-stopped volumes: - memory-embedding-model-e5:/data memory-embedding-worker: build: context: . target: runtime depends_on: migrate: condition: service_completed_successfully memory-embedding: condition: service_healthy entrypoint: ["node", ".runtime/scripts/memory-embedding-worker.js"] environment: DATABASE_URL: postgresql://osinara:${POSTGRES_PASSWORD}@postgres:5432/osinara MEMORY_EMBEDDING_BASE_URL: http://memory-embedding:80 restart: unless-stopped telegram-ingress-worker: build: context: . target: runtime depends_on: - agent entrypoint: ["node", ".runtime/scripts/telegram-ingress-worker.js"] environment: AGENT_INTERNAL_BASE_URL: http://agent:3000 TELEGRAM_WEBHOOK_SECRET_TOKEN: ${TELEGRAM_WEBHOOK_SECRET_TOKEN:?TELEGRAM_WEBHOOK_SECRET_TOKEN is required} restart: unless-stopped edge: image: nginx:1.29-alpine depends_on: - agent ports: - "8080:80" restart: unless-stopped volumes: - ./infra/nginx.conf:/etc/nginx/nginx.conf:ro volumes: google-workspace-credentials: postgres-data: memory-embedding-model-e5: tool-environments: workspace-data: networks: sandbox-control: internal: true sandbox-egress: internal: true