services: # ── Garage (S3-compatible blob storage) ────────────────────────────────────── # Used for file uploads (images, audio, attachments). Same API as AWS S3 / R2. # Replaces MinIO (EOL — upstream repo archived April 2026). The entrypoint # self-provisions layout + bucket + key, importing S3_ACCESS_KEY/S3_SECRET_KEY # so the S3_* env contract on web/worker is unchanged. Host port 9000 is kept # (Garage's S3 API listens on 3900 internally) so derived public endpoints # keep working. Override S3_* env to use external storage (e.g. R2) instead. garage: image: dxflrs/garage:v2.3.0 container_name: pear-garage ports: - "9000:3900" # S3 API (host 9000 preserved from the MinIO era) environment: # Secrets read by Garage when absent from garage.toml. Compose-network # internal (RPC/admin ports are not published); defaults are fine for # local self-hosting, override in .env for anything exposed. GARAGE_RPC_SECRET: ${GARAGE_RPC_SECRET:-9dd7325b40c1f0ab926e0a35e6592f9026fda9ec7d5c0d1fa1c4c05dfa79dc44} GARAGE_ADMIN_TOKEN: ${GARAGE_ADMIN_TOKEN:-pear-garage-admin} volumes: - garage-meta:/var/lib/garage/meta - garage-data:/var/lib/garage/data - ./server/docker/garage.toml:/etc/garage.toml:ro restart: unless-stopped networks: - pear # One-shot provisioner: layout + bucket + key import via the admin API. garage-init: image: alpine:3.20 container_name: pear-garage-init depends_on: - garage entrypoint: ["/bin/sh", "/garage-init.sh"] environment: GARAGE_ADMIN_TOKEN: ${GARAGE_ADMIN_TOKEN:-pear-garage-admin} S3_BUCKET: ${S3_BUCKET:-pear-attachments} S3_ACCESS_KEY: ${S3_ACCESS_KEY:-pear-access-key} S3_SECRET_KEY: ${S3_SECRET_KEY:-pear-secret-key-change-me} volumes: - ./server/docker/garage-init.sh:/garage-init.sh:ro restart: "no" networks: - pear # ── SpacetimeDB ──────────────────────────────────────────────────────────── # Pre-build the module before starting: cd server && spacetime build spacetimedb: image: clockworklabs/spacetime:v2.0.3 container_name: pear-spacetimedb ports: - "3000:3000" volumes: - spacetimedb-data:/stdb/data # Persist the spacetime CLI identity across container restarts. # Without this, every restart generates a new identity that isn't the # database owner, causing a 403 on publish. - spacetimedb-config:/root/.config/spacetime # Entrypoint script (starts SpacetimeDB then publishes the module) - ./server/docker/entrypoint.sh:/entrypoint.sh:ro # Pre-built module WASM — run `cd server && spacetime build` first - ./server/spacetimedb/target/wasm32-unknown-unknown/release/server.wasm:/module/server.wasm:ro entrypoint: ["/bin/sh", "/entrypoint.sh"] environment: STDB_DB_NAME: ${SPACETIMEDB_DB_NAME:-pear-dev} user: root restart: unless-stopped networks: - pear # ── Next.js web client ────────────────────────────────────────────────────── web: # Image pulled from the Forgejo container registry on the remote. # Set WEB_IMAGE in your .env to the full registry path, e.g.: # WEB_IMAGE=forgejo.yourdomain.com/kara/pear-web:latest # Falls back to a local tag for dev builds. image: ${WEB_IMAGE:-pear-web:latest} build: context: . dockerfile: web/Dockerfile args: # NEXT_PUBLIC_* vars are baked into the browser bundle at build time. # The browser must reach these URLs directly, so use the public-facing # addresses (Cloudflare Tunnel hostname or exposed host port). NEXT_PUBLIC_SPACETIMEDB_URI: ${SPACETIMEDB_PUBLIC_URI-ws://localhost:3000} NEXT_PUBLIC_SPACETIMEDB_DB_NAME: ${SPACETIMEDB_DB_NAME:-pear-dev} NEXT_PUBLIC_SPACETIMEAUTH_CLIENT_ID: ${SPACETIMEAUTH_CLIENT_ID:-} NEXT_PUBLIC_SPACETIMEAUTH_AUTHORITY: ${SPACETIMEAUTH_AUTHORITY:-} # Optional same-origin proxy to SpacetimeDB via Next.js rewrites. # Set SPACETIMEDB_PROXY_ORIGIN=http://spacetimedb:3000 and leave # SPACETIMEDB_PUBLIC_URI empty to let the browser use ws(s)://. # The web app proxies /v1/* to the internal SpacetimeDB service. SPACETIMEDB_PROXY_ORIGIN: ${SPACETIMEDB_PROXY_ORIGIN:-} container_name: pear-web ports: - "3001:3001" depends_on: - spacetimedb - garage environment: # S3-compatible blob storage (default: Garage in this stack) # Override to use external storage (e.g. R2, S3): set S3_ENDPOINT, S3_ACCESS_KEY, S3_SECRET_KEY. S3_ENDPOINT: ${S3_ENDPOINT:-http://garage:3900} # Optional. Browser upload URL (presigned URLs). If unset, derived from request host + :9000 (zero config when app and Garage are on the same host — Garage's S3 API is published on host port 9000). Set only when storage is on another host/port. S3_PUBLIC_ENDPOINT: ${S3_PUBLIC_ENDPOINT:-} S3_BUCKET: ${S3_BUCKET:-pear-attachments} S3_ACCESS_KEY: ${S3_ACCESS_KEY:-pear-access-key} S3_SECRET_KEY: ${S3_SECRET_KEY:-pear-secret-key-change-me} S3_REGION: ${S3_REGION:-us-east-1} restart: unless-stopped networks: - pear # ── Pear LLM Worker ──────────────────────────────────────────────────────── # Orcha agent: connects to SpacetimeDB, claims "orchestrate" and "llm" tasks, # calls Anthropic Claude, and submits results back to the coordination layer. # Set ANTHROPIC_API_KEY in your .env to enable. # Scale with: docker compose up --scale worker=3 worker: image: ${WORKER_IMAGE:-pear-worker:latest} build: context: . dockerfile: worker/Dockerfile container_name: pear-worker depends_on: - spacetimedb environment: SPACETIMEDB_URI: ws://spacetimedb:3000 SPACETIMEDB_DB_NAME: ${SPACETIMEDB_DB_NAME:-pear-dev} ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY} SPACETIMEDB_TOKEN: ${SPACETIMEDB_TOKEN:-} PEAR_MAIL_PRIVATE_KEY_B64: ${PEAR_MAIL_PRIVATE_KEY_B64:-} PEAR_MAIL_ALLOW_PRIVATE_HOSTS: ${PEAR_MAIL_ALLOW_PRIVATE_HOSTS:-false} ANTHROPIC_MODEL: ${ANTHROPIC_MODEL:-claude-haiku-4-5-20251001} ORCHA_AGENT_ID: ${ORCHA_AGENT_ID:-pear-llm-worker} # Same storage as the web service — the worker fetches image attachments # to hand to AI providers as base64. S3_ENDPOINT: ${S3_ENDPOINT:-http://garage:3900} S3_BUCKET: ${S3_BUCKET:-pear-attachments} S3_ACCESS_KEY: ${S3_ACCESS_KEY:-pear-access-key} S3_SECRET_KEY: ${S3_SECRET_KEY:-pear-secret-key-change-me} S3_REGION: ${S3_REGION:-us-east-1} restart: unless-stopped networks: - pear # ── Pear MCP Server (optional) ───────────────────────────────────────────── # Streamable-HTTP MCP endpoint so external AI models (Claude Code, Cursor, …) # can use Pear as a memory store / notes backend. Clients authenticate with # an AI user's worker token as a Bearer token (mint one: `pnpm mcp:provision`). # Opt-in: docker compose --profile mcp up mcp: image: ${WORKER_IMAGE:-pear-worker:latest} build: context: . dockerfile: worker/Dockerfile container_name: pear-mcp command: ["pnpm", "--filter", "@pear/worker", "mcp:http"] profiles: ["mcp"] depends_on: - spacetimedb ports: - "${PEAR_MCP_HTTP_PORT:-3888}:3888" environment: SPACETIMEDB_URI: ws://spacetimedb:3000 SPACETIMEDB_DB_NAME: ${SPACETIMEDB_DB_NAME:-pear-dev} PEAR_MCP_HTTP_HOST: 0.0.0.0 PEAR_MCP_HTTP_PORT: 3888 # Host-header allowlist for DNS-rebinding protection. Add your public # hostname:port when exposing this beyond localhost. PEAR_MCP_ALLOWED_HOSTS: ${PEAR_MCP_ALLOWED_HOSTS:-localhost:3888,127.0.0.1:3888} restart: unless-stopped networks: - pear networks: pear: driver: bridge volumes: spacetimedb-data: spacetimedb-config: garage-meta: garage-data: