# Docker Compose for Oikos development
# Usage: docker compose up -d postgres  (just the DB)
#        make dev                     (full dev stack)
#
# The SPA isn't embedded in the oikos binary (see
# plans/2026-07-12-wails-desktop-app.md 0.1/0.6) but it IS part of this
# stack as its own `web` service (compose/web/Dockerfile), so it deploys
# through the same push-to-main pipeline as everything else. `npm run dev`
# in web/ is still the fast local-iteration path.

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
    healthcheck:
      test: ["CMD", "pg_isready", "-U", "oikos"]
      interval: 5s
      timeout: 5s
      retries: 5

  # One-shot: run migrations then exit
  migrate:
    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"

  # One-shot: ingest seeds then exit
  seed:
    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"

  # API server (Phase 2)
  api:
    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. nomos uses the same value to call
      # back 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}
      NOMOS_PROXY_URL: http://nomos:8092
    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
    # Exists so nomos can wait for the API to actually answer rather than just
    # for its container to exist — see nomos's depends_on below. wget is
    # BusyBox's, already in the alpine runtime image, so this adds no
    # dependency. /healthz pings the DB, so "healthy" means genuinely ready.
    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.
      start_period: 10s

  # Scheduler (Phase 3) — observe loop
  scheduler:
    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
    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

  # Notifier (Phase 3) — Matrix alerts
  notifier:
    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_APPROVAL_HMAC_SECRET: ${OIKOS_APPROVAL_HMAC_SECRET:-dev-secret}
      OIKOS_MATRIX_HOMESERVER: ${OIKOS_MATRIX_HOMESERVER:-https://matrix.hubris.network}
      OIKOS_MATRIX_USER: ${OIKOS_MATRIX_USER:-@hermes:hubris.network}
      OIKOS_MATRIX_TOKEN: ${OIKOS_MATRIX_TOKEN}
      OIKOS_MATRIX_ROOM: ${OIKOS_MATRIX_ROOM:-!alerts:hubris.network}
    command: ["notifier"]
    stop_signal: SIGTERM
    stop_grace_period: 30s

  # Nomos agent gateway (Phase 4) — mesh-published :8092
  nomos:
    build:
      context: .
      dockerfile: compose/nomos/Dockerfile
    restart: unless-stopped
    profiles: ["full"]
    depends_on:
      api:
        # service_started only waits for the container to exist, so nomos came
        # up while the API was still binding :8090, failed its MCP initialize,
        # exited 1, and crash-looped for ~25s on every single deploy. It always
        # recovered, which is exactly why it went unnoticed. service_healthy
        # waits for the API to actually answer.
        condition: service_healthy
    environment:
      NOMOS_MCP_URL: http://api:8090/mcp
      NOMOS_AGENT_SLUG: agent:nomos
      OPENROUTER_API_KEY: ${OPENROUTER_API_KEY}
      NOMOS_MODEL: ${NOMOS_MODEL:-deepseek/deepseek-v4-pro}
      DATABASE_URL: postgres://oikos:${OIKOS_DB_PASSWORD:-oikos_dev}@postgres:5432/oikos?sslmode=disable
      # Must match api's OIKOS_MCP_BEARER_TOKEN above — api's combinedAuth
      # rejects every request without it now (no dev-open bypass).
      OIKOS_MCP_BEARER_TOKEN: ${OIKOS_MCP_BEARER_TOKEN:-dev-token}
    ports:
      - "8092:8092"
    stop_signal: SIGTERM
    stop_grace_period: 10s

  # Control-room SPA — static build served behind Caddy. The outer
  # production Caddy (caddy-conf repo, LXC 121) splits /api/*, /mcp,
  # /agent/* off to api:8090 and sends everything else here; this
  # container only serves static files with SPA-fallback routing.
  web:
    build:
      context: .
      dockerfile: compose/web/Dockerfile
    restart: unless-stopped
    profiles: ["dev", "full"]
    ports:
      - "8091:80"
    stop_signal: SIGTERM

  # Redis (required by Infisical — Phase 5)
  redis:
    image: redis:7-alpine
    restart: unless-stopped
    profiles: ["infisical", "full"]
    volumes:
      - redis-data:/data
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 5s
      timeout: 3s
      retries: 5

  # Infisical self-hosted (Phase 5 secrets management)
  infisical:
    image: infisical/infisical:latest
    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"

volumes:
  pg-data:
  redis-data:
