# Docker Compose for Oikos development # Usage: docker compose up -d postgres (just the DB) # make dev (full dev stack) # # The control-room SPA lives in its own repo (dtoro/oikos-web) with its own # compose project; the agent runtime (dsh) runs outside this stack and talks # to api's /mcp like any MCP client. services: postgres: image: timescale/timescaledb:2.17.2-pg16 restart: unless-stopped environment: POSTGRES_DB: oikos POSTGRES_USER: oikos POSTGRES_PASSWORD: ${OIKOS_DB_PASSWORD:-oikos_dev} ports: - "5432:5432" volumes: - pg-data:/var/lib/postgresql/data mem_limit: 1g cpus: 2.0 healthcheck: test: ["CMD", "pg_isready", "-U", "oikos"] interval: 5s timeout: 5s retries: 5 # One-shot: run migrations then exit migrate: image: oikos-migrate:${OIKOS_VERSION:-latest} build: context: . dockerfile: compose/oikos/Dockerfile depends_on: postgres: condition: service_healthy environment: OIKOS_DATABASE_URL: postgres://oikos:${OIKOS_DB_PASSWORD:-oikos_dev}@postgres:5432/oikos?sslmode=disable command: ["migrate"] restart: "no" mem_limit: 512m cpus: 1.0 # One-shot: ingest seeds then exit seed: image: oikos-seed:${OIKOS_VERSION:-latest} build: context: . dockerfile: compose/oikos/Dockerfile depends_on: migrate: condition: service_completed_successfully environment: OIKOS_DATABASE_URL: postgres://oikos:${OIKOS_DB_PASSWORD:-oikos_dev}@postgres:5432/oikos?sslmode=disable OIKOS_SEEDS_DIR: /seeds command: ["seed"] restart: "no" mem_limit: 512m cpus: 1.0 # API server (Phase 2) api: image: oikos-api:${OIKOS_VERSION:-latest} build: context: . dockerfile: compose/oikos/Dockerfile restart: unless-stopped profiles: ["dev", "full"] depends_on: seed: condition: service_completed_successfully environment: OIKOS_DATABASE_URL: postgres://oikos:${OIKOS_DB_PASSWORD:-oikos_dev}@postgres:5432/oikos?sslmode=disable OIKOS_API_LISTEN: ":8090" OIKOS_ENV: dev OIKOS_DEBUG: "true" # No dev-open auth bypass (plans/2026-07-12-wails-desktop-app.md 0.4) — # every request needs this token. dsh uses the same value to call # into api's /mcp and /api/v1/approvals/*/decision. OIKOS_MCP_BEARER_TOKEN: ${OIKOS_MCP_BEARER_TOKEN:-dev-token} OIKOS_OIDC_ISSUER: ${OIKOS_OIDC_ISSUER:-https://auth.hubris.network/application/o/oikos/} OIKOS_OIDC_CLIENT_ID: ${OIKOS_OIDC_CLIENT_ID:-otkHBSueHJsYtOHstL6rn5izeGgyOsavp1qA1hod} OIKOS_NOMOS_AGENT_SLUG: ${OIKOS_NOMOS_AGENT_SLUG:-agent:nomos} # Rate limiting (plan D3). Default off; set OIKOS_API_RATE_LIMIT to a # requests/sec value to throttle runaway agent loops per source IP. OIKOS_API_RATE_LIMIT: ${OIKOS_API_RATE_LIMIT:-} OIKOS_API_RATE_BURST: ${OIKOS_API_RATE_BURST:-} # Infisical secret store (Phase 5) OIKOS_INFISICAL_SITE_URL: ${OIKOS_INFISICAL_SITE_URL:-} OIKOS_INFISICAL_CLIENT_ID: ${OIKOS_INFISICAL_CLIENT_ID:-} OIKOS_INFISICAL_CLIENT_SECRET: ${OIKOS_INFISICAL_CLIENT_SECRET:-} OIKOS_INFISICAL_PROJECT_ID: ${OIKOS_INFISICAL_PROJECT_ID:-} OIKOS_INFISICAL_ENV: ${OIKOS_INFISICAL_ENV:-dev} volumes: - ${OIKOS_SSH_KEY_PATH:-~/.ssh/id_ed25519}:/etc/oikos/ssh_key:ro ports: - "8090:8090" command: ["api"] stop_signal: SIGTERM stop_grace_period: 30s mem_limit: 512m cpus: 1.0 # Self-probe so `docker compose ps` reports genuine readiness: /healthz # pings the DB, so "healthy" means actually answering. wget is BusyBox's, # already in the alpine runtime image, so this adds no dependency. healthcheck: test: ["CMD", "wget", "-q", "-O", "-", "http://127.0.0.1:8090/healthz"] interval: 5s timeout: 3s retries: 10 # Migrations and seed run before this container, but the first bind can # still take a moment; failures inside the start period don't count. # The api's NewHandler stalls on TWO unreachable external deps at startup # before binding :8090: Infisical (4x auth retries, ~40s) and OIDC # discovery (auth.hubris.network, ~35s of timeouts). Total ~90-95s, so # the start period must clear it. start_period: 180s # Scheduler (Phase 3) — observe loop scheduler: image: oikos-scheduler:${OIKOS_VERSION:-latest} build: context: . dockerfile: compose/oikos/Dockerfile restart: unless-stopped profiles: ["dev", "full"] depends_on: seed: condition: service_completed_successfully environment: OIKOS_DATABASE_URL: postgres://oikos:${OIKOS_DB_PASSWORD:-oikos_dev}@postgres:5432/oikos?sslmode=disable OIKOS_DEBUG: "true" OIKOS_SCHEDULER_INTERVAL: "30s" OIKOS_SSH_KEY_PATH: /etc/oikos/ssh_key OIKOS_SSH_USER: root # Liveness probe (plan D5): exposes a staleness-aware /healthz inside # the container; the scheduler bumps it each check pass. OIKOS_HEALTH_LISTEN: ":8093" volumes: - ${OIKOS_SSH_KEY_PATH:-~/.ssh/id_ed25519}:/etc/oikos/ssh_key:ro cap_add: - NET_RAW command: ["scheduler"] stop_signal: SIGTERM stop_grace_period: 30s mem_limit: 256m cpus: 1.0 healthcheck: test: ["CMD", "wget", "-q", "-O", "-", "http://127.0.0.1:8093/healthz"] interval: 30s timeout: 5s retries: 3 start_period: 90s # Execution worker (Phase 6) — Postgres-backed job queue execution-worker: image: oikos-execution-worker:${OIKOS_VERSION:-latest} build: context: . dockerfile: compose/oikos/Dockerfile restart: unless-stopped profiles: ["dev", "full"] depends_on: seed: condition: service_completed_successfully environment: OIKOS_DATABASE_URL: postgres://oikos:${OIKOS_DB_PASSWORD:-oikos_dev}@postgres:5432/oikos?sslmode=disable OIKOS_DEBUG: "true" OIKOS_SSH_KEY_PATH: /etc/oikos/ssh_key OIKOS_SSH_USER: root OIKOS_HEALTH_LISTEN: ":8095" volumes: - ${OIKOS_SSH_KEY_PATH:-~/.ssh/id_ed25519}:/etc/oikos/ssh_key:ro command: ["execution-worker"] stop_signal: SIGTERM stop_grace_period: 30s mem_limit: 256m cpus: 1.0 healthcheck: test: ["CMD", "wget", "-q", "-O", "-", "http://127.0.0.1:8095/healthz"] interval: 30s timeout: 5s retries: 3 start_period: 90s # Nomos agent gateway (Phase 4) — decommissioned. Replaced by dsh # (DeepSeek Harness) as the agent runtime; see # plans/2026-08-16-dsh-as-agent-replace-nomos.md section 5. dsh runs # outside this stack (Node.js sidecar, dev at http://127.0.0.1:3080) # and talks to api's /mcp like any MCP client. # The control-room SPA moved to its own repo (dtoro/oikos-web, Phase 1 of # plans/2026-08-15-hexagonal-architecture.md) with its own compose project # publishing the same host port 8091 — the outer Caddy targets the published # port, so nothing here changes for routing. # Redis (required by Infisical — Phase 5) redis: image: redis:7-alpine restart: unless-stopped profiles: ["infisical", "full"] volumes: - redis-data:/data mem_limit: 128m cpus: 0.5 healthcheck: test: ["CMD", "redis-cli", "ping"] interval: 5s timeout: 3s retries: 5 # Infisical self-hosted (Phase 5 secrets management) infisical: image: infisical/infisical:v0.162.19 restart: unless-stopped profiles: ["infisical", "full"] depends_on: postgres: condition: service_healthy redis: condition: service_healthy environment: ENCRYPTION_KEY: ${INFISICAL_ENCRYPTION_KEY:-dev-encryption-key-change-me} JWT_SIGNUP_SECRET: ${INFISICAL_JWT_SIGNUP_SECRET:-dev-jwt-change-me} JWT_REFRESH_SECRET: ${INFISICAL_JWT_REFRESH_SECRET:-dev-refresh-change-me} JWT_AUTH_SECRET: ${INFISICAL_JWT_AUTH_SECRET:-dev-auth-change-me} JWT_SERVICE_SECRET: ${INFISICAL_JWT_SERVICE_SECRET:-dev-service-change-me} JWT_MFA_SECRET: ${INFISICAL_JWT_MFA_SECRET:-dev-mfa-change-me} JWT_PROVIDER_SECRET: ${INFISICAL_JWT_PROVIDER_SECRET:-dev-provider-change-me} SITE_URL: ${INFISICAL_SITE_URL:-http://localhost:8080} SMTP_HOST: "" DB_CONNECTION_URI: postgres://oikos:${OIKOS_DB_PASSWORD:-oikos_dev}@postgres:5432/infisical?sslmode=disable REDIS_URL: redis://redis:6379 ports: - "8080:8080" mem_limit: 512m cpus: 1.0 volumes: pg-data: redis-data: