name: stint services: postgres: image: postgres:15-alpine restart: unless-stopped environment: POSTGRES_DB: stint POSTGRES_USER: stint POSTGRES_PASSWORD: stint ports: - "127.0.0.1:5432:5432" volumes: - pgdata:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U stint -d stint"] interval: 5s timeout: 5s retries: 10 redis: image: redis:7-alpine restart: unless-stopped ports: - "127.0.0.1:6379:6379" volumes: - redisdata:/data healthcheck: test: ["CMD-SHELL", "redis-cli ping | grep PONG"] interval: 5s timeout: 3s retries: 10 api: image: stint-api restart: unless-stopped build: context: . dockerfile: Dockerfile target: api environment: PORT: ${PORT:-8080} BASE_URL: ${BASE_URL:-http://localhost:8080} WEB_BASE_URL: ${WEB_BASE_URL:-http://localhost:3001} DATABASE_URL: ${DATABASE_URL:-postgres://stint:stint@postgres:5432/stint?sslmode=disable} REDIS_URL: ${REDIS_URL:-redis://redis:6379} GITHUB_CLIENT_ID: ${GITHUB_CLIENT_ID:-} GITHUB_CLIENT_SECRET: ${GITHUB_CLIENT_SECRET:-} SESSION_SECRET: ${SESSION_SECRET:-dev-session-secret-change-me} STORAGE_TYPE: ${STORAGE_TYPE:-local} STORAGE_PATH: ${STORAGE_PATH:-/data/dumps} S3_BUCKET: ${S3_BUCKET:-} S3_REGION: ${S3_REGION:-us-east-1} AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID:-} AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY:-} SMTP_HOST: ${SMTP_HOST:-} SMTP_PORT: ${SMTP_PORT:-587} SMTP_USER: ${SMTP_USER:-} SMTP_PASS: ${SMTP_PASS:-} EMAIL_FROM: ${EMAIL_FROM:-noreply@example.com} ENABLE_PUBLIC_LEADERBOARD: ${ENABLE_PUBLIC_LEADERBOARD:-true} ENABLE_REGISTRATION: ${ENABLE_REGISTRATION:-true} MAX_USERS: ${MAX_USERS:-0} DEV_SEED_ENABLED: ${DEV_SEED_ENABLED:-true} HEARTBEAT_RETENTION_DAYS: ${HEARTBEAT_RETENTION_DAYS:-365} ports: - "8080:8080" volumes: - dumpdata:/data/dumps healthcheck: test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:8080/healthz >/dev/null || exit 1"] interval: 30s timeout: 3s start_period: 10s retries: 3 depends_on: postgres: condition: service_healthy redis: condition: service_healthy worker: image: stint-worker restart: unless-stopped build: context: . dockerfile: Dockerfile target: api command: ["worker"] environment: PORT: ${PORT:-8080} BASE_URL: ${BASE_URL:-http://localhost:8080} WEB_BASE_URL: ${WEB_BASE_URL:-http://localhost:3001} DATABASE_URL: ${DATABASE_URL:-postgres://stint:stint@postgres:5432/stint?sslmode=disable} REDIS_URL: ${REDIS_URL:-redis://redis:6379} GITHUB_CLIENT_ID: ${GITHUB_CLIENT_ID:-} GITHUB_CLIENT_SECRET: ${GITHUB_CLIENT_SECRET:-} SESSION_SECRET: ${SESSION_SECRET:-dev-session-secret-change-me} STORAGE_TYPE: ${STORAGE_TYPE:-local} STORAGE_PATH: ${STORAGE_PATH:-/data/dumps} S3_BUCKET: ${S3_BUCKET:-} S3_REGION: ${S3_REGION:-us-east-1} AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID:-} AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY:-} SMTP_HOST: ${SMTP_HOST:-} SMTP_PORT: ${SMTP_PORT:-587} SMTP_USER: ${SMTP_USER:-} SMTP_PASS: ${SMTP_PASS:-} EMAIL_FROM: ${EMAIL_FROM:-noreply@example.com} ENABLE_PUBLIC_LEADERBOARD: ${ENABLE_PUBLIC_LEADERBOARD:-true} ENABLE_REGISTRATION: ${ENABLE_REGISTRATION:-true} MAX_USERS: ${MAX_USERS:-0} DEV_SEED_ENABLED: "false" HEARTBEAT_RETENTION_DAYS: ${HEARTBEAT_RETENTION_DAYS:-365} volumes: - dumpdata:/data/dumps healthcheck: test: ["CMD-SHELL", "test -r /proc/1/stat"] interval: 30s timeout: 3s start_period: 10s retries: 3 depends_on: postgres: condition: service_healthy redis: condition: service_healthy web: image: stint-web restart: unless-stopped build: context: ./web dockerfile: Dockerfile args: API_BASE_URL: ${API_BASE_URL:-http://api:8080} environment: API_BASE_URL: ${API_BASE_URL:-http://api:8080} NEXT_PUBLIC_API_BASE_URL: ${NEXT_PUBLIC_API_BASE_URL:-} ports: - "3001:3000" depends_on: api: condition: service_healthy # Continuous local AI-usage ingestion. Opt-in: # docker compose --profile collector up -d collector # Reads the dev's agent data from read-only mounts of just the per-agent data # roots under the host home (HOME=/host-home, so adapters' ~/. default # paths resolve), normalizes it, and posts to the api service on a --watch loop. # Configure via STINT_COLLECT_* in .env (api key, host path, uid/gid, interval). # # STINT_COLLECT_HOME MUST be set to your absolute home path in .env. The # ${STINT_COLLECT_HOME:-${HOME}} shell-default below only fills in when HOME is # exported in the shell that invokes `docker compose`; .env file values are NOT # shell-expanded by Compose, so a .env-only setup must set STINT_COLLECT_HOME. # # Each agent dir is mounted individually (instead of the whole home) so secrets # like ~/.ssh, ~/.aws and ~/.netrc are never exposed to the collector. Only the # dirs the *implemented* adapters read are mounted by default; uncomment a line # as its adapter lands (and the dir exists — a missing bind source breaks `up`). # See cmd/collect/README.md and AI_COST.md for the per-agent path table. collector: image: stint-collector restart: unless-stopped build: context: . dockerfile: Dockerfile target: collector profiles: - collector command: ["--watch", "--interval", "${STINT_COLLECT_INTERVAL:-5m}"] user: "${STINT_COLLECT_UID:-1000}:${STINT_COLLECT_GID:-1000}" environment: HOME: /host-home STINT_API_URL: ${STINT_COLLECT_API_URL:-http://api:8080/api/v1} STINT_API_KEY: ${STINT_COLLECT_API_KEY:-} STINT_STATE_PATH: /state/collector-state.json volumes: # Per-agent data roots, read-only. Narrow on purpose (no whole-home mount). # Dirs for the implemented adapters (Claude/Codex/Gemini, and OpenCode/Goose/ # Zed under .local/share). All must exist on the host or `up` fails. - ${STINT_COLLECT_HOME:-${HOME}}/.claude:/host-home/.claude:ro - ${STINT_COLLECT_HOME:-${HOME}}/.codex:/host-home/.codex:ro - ${STINT_COLLECT_HOME:-${HOME}}/.gemini:/host-home/.gemini:ro - ${STINT_COLLECT_HOME:-${HOME}}/.local/share:/host-home/.local/share:ro # Uncomment per agent as its adapter ships AND the dir exists on your host # (.config/Code and .local/share are broader than the dot-dirs above): # - ${STINT_COLLECT_HOME:-${HOME}}/.cursor:/host-home/.cursor:ro # - ${STINT_COLLECT_HOME:-${HOME}}/.copilot:/host-home/.copilot:ro # - ${STINT_COLLECT_HOME:-${HOME}}/.qwen:/host-home/.qwen:ro # - ${STINT_COLLECT_HOME:-${HOME}}/.kimi:/host-home/.kimi:ro # - ${STINT_COLLECT_HOME:-${HOME}}/.kiro:/host-home/.kiro:ro # - ${STINT_COLLECT_HOME:-${HOME}}/.factory:/host-home/.factory:ro # - ${STINT_COLLECT_HOME:-${HOME}}/.hermes:/host-home/.hermes:ro # - ${STINT_COLLECT_HOME:-${HOME}}/.openclaw:/host-home/.openclaw:ro # - ${STINT_COLLECT_HOME:-${HOME}}/.pi:/host-home/.pi:ro # - ${STINT_COLLECT_HOME:-${HOME}}/.config/Code:/host-home/.config/Code:ro # State dir MUST be writable by STINT_COLLECT_UID (the collector runs as that # uid). The committed ./.collector-state dir avoids Docker auto-creating it as # root; otherwise state never persists and every restart re-scans/re-posts. - ${STINT_COLLECT_STATE_DIR:-./.collector-state}:/state depends_on: api: condition: service_healthy caddy: image: caddy:2-alpine restart: unless-stopped profiles: - proxy environment: STINT_API_SITE: ${STINT_API_SITE:-:8081} STINT_WEB_SITE: ${STINT_WEB_SITE:-:3002} ports: - "${STINT_API_PROXY_PORT:-8081}:8081" - "${STINT_WEB_PROXY_PORT:-3002}:3002" volumes: - ./Caddyfile:/etc/caddy/Caddyfile:ro - caddydata:/data - caddyconfig:/config depends_on: api: condition: service_healthy web: condition: service_started volumes: pgdata: redisdata: dumpdata: caddydata: caddyconfig: