# opendevops service-mode stack. # # The self-hosted LangGraph Server (Postgres queue+checkpointer, Redis) fronted by Caddy # (bearer-token gate), with the audit trail shipped by Vector to a durable local spool and # Prometheus + Grafana for the operability dashboards/alerts. # # IMPORTANT (blast-radius rule): this stack MUST NOT run on a Kubernetes cluster the agent itself manages # — a blast-radius rule. Run it on a dedicated ops VM (or a separate ops cluster). See # docs/DEPLOY.md. # # The released stack pulls a prebuilt, multi-architecture server image. Contributors can # override LANGGRAPH_IMAGE with a locally built tag. # # Validate this file without pulling/running anything: # docker compose -f docker-compose.yml config -q # # Secrets are read from the environment (or a sibling .env). Security-sensitive service # credentials are required at Compose interpolation time; there are no deployable default values. name: opendevops volumes: postgres-data: redis-data: agent-state: # OIDC/control-plane sessions, grants, revisions and action attribution agent-audit: # the per-run .jsonl chain files (writer: server; reader: vector) audit-spool: # Vector's durable merged sink (default: local spool dir) prometheus-data: grafana-data: caddy-data: postgres-backups: # scheduler-created dumps; no automatic retention/deletion networks: agent-net: services: # -- the self-hosted LangGraph Server (our graph + webhook app) ------------------------------ langgraph-server: image: ${LANGGRAPH_IMAGE:-ghcr.io/skundu42/opendevops:latest} working_dir: /deploy # so config's relative ./audit and ./config/policy resolve under /deploy restart: unless-stopped expose: - "8000" # internal only — Caddy is the sole ingress (no host port published) depends_on: postgres: condition: service_healthy redis: condition: service_healthy environment: # The platform's own persistence + queue (NOT our LocalGateway saver — the server injects it). DATABASE_URI: postgres://postgres:${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD}@postgres:5432/postgres?sslmode=disable REDIS_URI: redis://redis:6379/1 # LangSmith licensing / tracing key — passed through from the environment (never baked). LANGSMITH_API_KEY: ${LANGSMITH_API_KEY:-} LANGSMITH_TRACING: ${LANGSMITH_TRACING:-false} # Our config: OPENDEVOPS_CONFIG names the config.yaml; load_config reads its sibling # models.yaml / budgets.yaml + policy dir from the mounted ./config (grandparent = /deploy). OPENDEVOPS_CONFIG: /deploy/config/config.yaml # The webhook app runs INSIDE this container; its ServerGateway must reach the LOCAL server # API directly on the loopback port (8000) — NOT the external Caddy URL (config.yaml # server.url = :8123, a DIFFERENT container). This bypasses Caddy + its bearer on the # in-process path (no api_key needed); external clients still go through Caddy. See DEPLOY.md. OPENDEVOPS_SELF_URL: http://localhost:8000 # Webhook-app + server-client secrets (names are configured in config.yaml; values live here). ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY:-} # Static dashboard auth is for local development. Production config uses OIDC. DASHBOARD_TOKEN: ${DASHBOARD_TOKEN:-} OIDC_CLIENT_ID: ${OIDC_CLIENT_ID:-} OIDC_CLIENT_SECRET: ${OIDC_CLIENT_SECRET:-} OTEL_EXPORTER_OTLP_ENDPOINT: ${OTEL_EXPORTER_OTLP_ENDPOINT:-} volumes: - ./config:/deploy/config:ro # config.yaml + models.yaml + budgets.yaml + policy/ - agent-audit:/deploy/audit # per-run audit chains (shared read-only with vector) - agent-state:/deploy/state # capability grants, control events and local state healthcheck: test: ["CMD", "curl", "--fail", "--silent", "http://localhost:8000/readyz"] interval: 10s timeout: 3s retries: 6 networks: - agent-net # -- Postgres: the server's durable queue + checkpointer store ------------------------------- postgres: image: postgres:16 restart: unless-stopped environment: POSTGRES_DB: postgres POSTGRES_USER: postgres POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD} volumes: - postgres-data:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U postgres"] interval: 5s timeout: 5s retries: 5 networks: - agent-net # -- Redis: the server's task queue AND the shared RedisDailyCounter (budgets.daily) ---------- redis: image: redis:7 restart: unless-stopped command: ["redis-server", "--save", "60", "1", "--appendonly", "yes"] volumes: - redis-data:/data healthcheck: test: ["CMD", "redis-cli", "ping"] interval: 5s timeout: 3s retries: 5 networks: - agent-net # -- Caddy: bearer-token HTTP ingress gate in front of the server ---------------------------- # Production deployments must terminate TLS before this port (or configure a hostname/certs in # the Caddyfile); the shipped :8123 listener is intentionally plain HTTP for local/VM wiring. caddy: image: caddy:2 restart: unless-stopped depends_on: - langgraph-server ports: - "8123:8123" # the public API port (matches config.yaml server.url) environment: GATEWAY_TOKEN: ${GATEWAY_TOKEN:?set GATEWAY_TOKEN} volumes: - ./ops/caddy/Caddyfile:/etc/caddy/Caddyfile:ro - caddy-data:/data networks: - agent-net # -- Optional Slack Socket-Mode adapter ----------------------------------------------------- slack: profiles: ["slack"] image: ${LANGGRAPH_IMAGE:-ghcr.io/skundu42/opendevops:latest} working_dir: /deploy restart: unless-stopped command: ["opendevops", "slack", "--server-url", "http://caddy:8123"] depends_on: - caddy environment: OPENDEVOPS_CONFIG: /deploy/config/config.yaml LANGGRAPH_API_KEY: ${GATEWAY_TOKEN:?set GATEWAY_TOKEN} OPENDEVOPS_SLACK_BOT_TOKEN: ${OPENDEVOPS_SLACK_BOT_TOKEN:-} OPENDEVOPS_SLACK_APP_TOKEN: ${OPENDEVOPS_SLACK_APP_TOKEN:-} OTEL_EXPORTER_OTLP_ENDPOINT: ${OTEL_EXPORTER_OTLP_ENDPOINT:-} volumes: - ./config:/deploy/config:ro networks: - agent-net # -- Optional scheduler: agent jobs, timeout sweep, and backup-first daily hygiene ---------- scheduler: profiles: ["scheduler"] image: ${LANGGRAPH_IMAGE:-ghcr.io/skundu42/opendevops:latest} working_dir: /deploy restart: unless-stopped command: ["opendevops", "scheduler", "--server-url", "http://caddy:8123", "--jobs-file", "/deploy/scheduler/jobs.yaml"] depends_on: caddy: condition: service_started postgres: condition: service_healthy redis: condition: service_healthy environment: OPENDEVOPS_CONFIG: /deploy/config/config.yaml LANGGRAPH_API_KEY: ${GATEWAY_TOKEN:?set GATEWAY_TOKEN} REDIS_URI: redis://redis:6379/1 OPENDEVOPS_BACKUP_DATABASE_URI: ${OPENDEVOPS_BACKUP_DATABASE_URI:-postgresql://postgres@postgres:5432/postgres?sslmode=disable} PGPASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD} OPENDEVOPS_BACKUP_DIR: ${OPENDEVOPS_BACKUP_DIR:-/backups} OTEL_EXPORTER_OTLP_ENDPOINT: ${OTEL_EXPORTER_OTLP_ENDPOINT:-} volumes: - ./config:/deploy/config:ro - ./scheduler:/deploy/scheduler:ro - postgres-backups:/backups networks: - agent-net # -- Vector: ship the per-run audit chains to the durable spool sink (default sink) ----------- vector: image: timberio/vector:0.41.1-debian restart: unless-stopped depends_on: - langgraph-server expose: - "9598" # prometheus_exporter (Vector's own health/lag metrics) volumes: - ./ops/vector/vector.yaml:/etc/vector/vector.yaml:ro - agent-audit:/audit:ro # read the per-run chain files - audit-spool:/spool:rw # write the merged durable stream command: ["--config", "/etc/vector/vector.yaml"] networks: - agent-net # -- Prometheus: scrape the server /metrics + Vector lag metrics ------------------------------ prometheus: image: prom/prometheus:v2.54.1 restart: unless-stopped depends_on: - langgraph-server - vector expose: - "9090" volumes: - ./ops/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro - ./ops/prometheus/alerts.yml:/etc/prometheus/alerts.yml:ro - prometheus-data:/prometheus command: - "--config.file=/etc/prometheus/prometheus.yml" - "--storage.tsdb.path=/prometheus" networks: - agent-net # -- Grafana: provisioned Prometheus datasource + the ops dashboard --------------------------- grafana: image: grafana/grafana:11.2.0 restart: unless-stopped depends_on: - prometheus ports: - "3000:3000" environment: GF_SECURITY_ADMIN_PASSWORD: ${GRAFANA_ADMIN_PASSWORD:?set GRAFANA_ADMIN_PASSWORD} GF_USERS_ALLOW_SIGN_UP: "false" volumes: - ./ops/grafana/provisioning:/etc/grafana/provisioning:ro - ./ops/grafana/dashboards:/var/lib/grafana/dashboards:ro - grafana-data:/var/lib/grafana networks: - agent-net