# Echodo production stack — shared CT 102 Postgres/Redis, multi-service build. # # Phase A (default): web, collab, mcp, orchestrator, worker # Phase B (optional coding harness): # Preferred: OpenCode — COMPOSE_PROFILES=opencode, WORKER_SESSION_TYPE=opencode # Legacy: OpenHands — COMPOSE_PROFILES=agent-server, WORKER_SESSION_TYPE=openhands # # Deploy targets: # - Dokploy (CT 112): compose file path `docker/docker-compose.prod.yml`, project dir = repo root # - Standalone LXC: `docker compose -f docker/docker-compose.prod.yml up -d` # # Domains (Dokploy UI or CT 100 Traefik): # web → echodo.stillwell.cloud (port 3000) # collab → collab.echodo.stillwell.cloud (port 1234) # opencode (optional) → opencode.stillwell.cloud (port 4096) # # Build (required on ≤8 GB hosts — parallel export OOMs with exit 255): # bash scripts/build-prod.sh # Dokploy → Advanced → Custom Build Command: bash scripts/build-prod.sh # # Phase B OpenCode: # export COMPOSE_PROFILES=opencode # Set WORKER_SESSION_TYPE=opencode, OPENCODE_SERVER_PASSWORD, OMNIROUTE_API_KEY / BASE_URL. # # Phase B OpenHands (legacy): # export COMPOSE_PROFILES=agent-server # Set WORKER_SESSION_TYPE=openhands and matching SESSION_API_KEY / LOCAL_BACKEND_API_KEY. services: web: build: context: . dockerfile: docker/Dockerfile target: web args: NEXT_PUBLIC_COLLAB_SERVER_URL: ${NEXT_PUBLIC_COLLAB_SERVER_URL:-ws://localhost:1234} NEXT_PUBLIC_UMAMI_SCRIPT: ${NEXT_PUBLIC_UMAMI_SCRIPT:-} NEXT_PUBLIC_UMAMI_WEBSITE_ID: ${NEXT_PUBLIC_UMAMI_WEBSITE_ID:-} NEXT_PUBLIC_LIBREDESK_URL: ${NEXT_PUBLIC_LIBREDESK_URL:-} NEXT_PUBLIC_LIBREDESK_WIDGET_ID: ${NEXT_PUBLIC_LIBREDESK_WIDGET_ID:-} NEXT_PUBLIC_DIRECTUS_URL: ${NEXT_PUBLIC_DIRECTUS_URL:-} depends_on: collab: condition: service_started mcp: condition: service_started restart: unless-stopped environment: - DATABASE_URL=${DATABASE_URL} - REDIS_URL=${REDIS_URL} - NEXTAUTH_URL=${NEXTAUTH_URL} - NEXTAUTH_SECRET=${NEXTAUTH_SECRET} - AUTH_SECRET=${AUTH_SECRET} - AUTH_DEV_PASSWORD=${AUTH_DEV_PASSWORD} - AUTH_GITHUB_ID=${AUTH_GITHUB_ID} - AUTH_GITHUB_SECRET=${AUTH_GITHUB_SECRET} - AUTH_GOOGLE_ID=${AUTH_GOOGLE_ID} - AUTH_GOOGLE_SECRET=${AUTH_GOOGLE_SECRET} - AUTH_AUTHENTIK_ID=${AUTH_AUTHENTIK_ID} - AUTH_AUTHENTIK_SECRET=${AUTH_AUTHENTIK_SECRET} - AUTH_AUTHENTIK_ISSUER=${AUTH_AUTHENTIK_ISSUER} - COLLAB_SERVER_URL=${COLLAB_SERVER_URL} - NEXT_PUBLIC_COLLAB_SERVER_URL=${NEXT_PUBLIC_COLLAB_SERVER_URL} - NEXT_PUBLIC_UMAMI_SCRIPT=${NEXT_PUBLIC_UMAMI_SCRIPT} - NEXT_PUBLIC_UMAMI_WEBSITE_ID=${NEXT_PUBLIC_UMAMI_WEBSITE_ID} - NEXT_PUBLIC_LIBREDESK_URL=${NEXT_PUBLIC_LIBREDESK_URL} - NEXT_PUBLIC_LIBREDESK_WIDGET_ID=${NEXT_PUBLIC_LIBREDESK_WIDGET_ID} - NEXT_PUBLIC_DIRECTUS_URL=${NEXT_PUBLIC_DIRECTUS_URL} - DIRECTUS_FEEDBACK_TOKEN=${DIRECTUS_FEEDBACK_TOKEN} - OPENAI_API_KEY=${OPENAI_API_KEY} - OPENAI_BASE_URL=${OPENAI_BASE_URL} - ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY} - OMNIROUTE_BASE_URL=${OMNIROUTE_BASE_URL:-} - OMNIROUTE_API_KEY=${OMNIROUTE_API_KEY:-} - GITHUB_TOKEN=${GITHUB_TOKEN:-} - FORGEJO_TOKEN=${FORGEJO_TOKEN:-} - FORGE_WEBHOOK_SECRET=${FORGE_WEBHOOK_SECRET:-} - NODE_ENV=production - NEXT_TELEMETRY_DISABLED=1 expose: - "3000" healthcheck: test: - CMD-SHELL - "wget -qO- --timeout=3 http://127.0.0.1:3000/ >/dev/null || exit 1" interval: 30s timeout: 5s retries: 3 start_period: 90s networks: - echodo collab: build: context: . dockerfile: docker/Dockerfile target: collab restart: unless-stopped environment: - DATABASE_URL=${DATABASE_URL} - REDIS_URL=${REDIS_URL} - PORT=1234 - NODE_ENV=production expose: - "1234" healthcheck: test: - CMD-SHELL - 'node -e "const p=+(process.env.PORT||1234);require(''net'').createConnection(p,''127.0.0.1'').on(''connect'',()=>process.exit(0)).on(''error'',()=>process.exit(1))"' interval: 30s timeout: 5s retries: 3 start_period: 25s networks: - echodo mcp: build: context: . dockerfile: docker/Dockerfile target: mcp args: MCP_SERVER_PORT: ${MCP_SERVER_PORT:-3001} restart: unless-stopped environment: - DATABASE_URL=${DATABASE_URL} - MCP_SERVER_PORT=${MCP_SERVER_PORT:-3001} - NODE_ENV=production stdin_open: true expose: - "${MCP_SERVER_PORT:-3001}" healthcheck: test: - CMD-SHELL - "pgrep -f 'node.*dist/index\\.js' > /dev/null || exit 1" interval: 30s timeout: 5s retries: 3 start_period: 25s networks: - echodo orchestrator: build: context: . dockerfile: docker/Dockerfile target: orchestrator restart: unless-stopped environment: - DATABASE_URL=${DATABASE_URL} - REDIS_URL=${REDIS_URL} - ORCHESTRATOR_DRY_RUN=${ORCHESTRATOR_DRY_RUN:-0} - ORCHESTRATOR_POLL_MS=${ORCHESTRATOR_POLL_MS:-5000} - ORCHESTRATOR_STALL_MINUTES=${ORCHESTRATOR_STALL_MINUTES:-120} - ORCHESTRATOR_MAX_IMPLEMENTERS=${ORCHESTRATOR_MAX_IMPLEMENTERS:-2} - NODE_ENV=production healthcheck: test: - CMD-SHELL - "pgrep -f 'node.*dist/index\\.js' > /dev/null || exit 1" interval: 30s timeout: 5s retries: 3 start_period: 25s networks: - echodo worker: build: context: . dockerfile: docker/Dockerfile target: worker depends_on: orchestrator: condition: service_started restart: unless-stopped environment: - DATABASE_URL=${DATABASE_URL} - REDIS_URL=${REDIS_URL} - WORKER_DRY_RUN=${WORKER_DRY_RUN:-0} - WORKER_BLPOP_TIMEOUT_S=${WORKER_BLPOP_TIMEOUT_S:-5} - WORKER_MAX_TOOL_STEPS=${WORKER_MAX_TOOL_STEPS:-12} - WORKER_REPO_ROOT=/app/repo - WORKER_WORKTREE_BASE=/tmp/echodo-worker-worktrees - WORKER_SESSION_TYPE=${WORKER_SESSION_TYPE:-minimal} - WORKER_OPENHANDS_WORKSPACE=${WORKER_OPENHANDS_WORKSPACE:-/workspace/repo} - AGENT_SERVER_URL=${AGENT_SERVER_URL:-http://agent-server:8001} - SESSION_API_KEY=${SESSION_API_KEY:-} - OPENCODE_SERVER_URL=${OPENCODE_SERVER_URL:-http://opencode:4096} - OPENCODE_SERVER_PASSWORD=${OPENCODE_SERVER_PASSWORD:-} - OPENCODE_SERVER_USERNAME=${OPENCODE_SERVER_USERNAME:-opencode} - OPENCODE_PROVIDER_ID=${OPENCODE_PROVIDER_ID:-omniroute} - WORKER_OPENCODE_WORKSPACE=${WORKER_OPENCODE_WORKSPACE:-/workspace/repo} - OPENAI_API_KEY=${OPENAI_API_KEY} - OPENAI_BASE_URL=${OPENAI_BASE_URL} - ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY} - OMNIROUTE_BASE_URL=${OMNIROUTE_BASE_URL:-} - OMNIROUTE_API_KEY=${OMNIROUTE_API_KEY:-} - GITHUB_TOKEN=${GITHUB_TOKEN:-} - FORGEJO_TOKEN=${FORGEJO_TOKEN:-} - NODE_ENV=production volumes: - echodo-workspace:/workspace healthcheck: test: - CMD-SHELL - "pgrep -f 'node.*dist/index\\.js' > /dev/null || exit 1" interval: 30s timeout: 5s retries: 3 start_period: 25s networks: - echodo opencode: profiles: ["opencode"] build: context: . dockerfile: docker/opencode/Dockerfile image: submatter-opencode:tooling container_name: submatter-opencode restart: unless-stopped working_dir: /workspace/repo entrypoint: ["/bin/sh", "/config/entrypoint.sh"] command: [ "opencode", "serve", "--hostname", "0.0.0.0", "--port", "4096", ] environment: - HOME=/data - XDG_CONFIG_HOME=/data/.config - XDG_DATA_HOME=/data/.local/share - OPENCODE_SERVER_PASSWORD=${OPENCODE_SERVER_PASSWORD} - OPENCODE_SERVER_USERNAME=${OPENCODE_SERVER_USERNAME:-opencode} - OMNIROUTE_API_KEY=${OMNIROUTE_API_KEY} - FORGEJO_TOKEN=${FORGEJO_TOKEN:-} - GITHUB_TOKEN=${GITHUB_TOKEN:-} - GIT_USER_NAME=${GIT_USER_NAME:-opencode} - GIT_USER_EMAIL=${GIT_USER_EMAIL:-opencode@stillwell.cloud} volumes: - echodo-workspace:/workspace - opencode-data:/data - ./docker/opencode/opencode.json:/config/opencode.json:ro - ./docker/opencode/entrypoint.sh:/config/entrypoint.sh:ro ports: - "4096:4096" healthcheck: # BusyBox wget has no --user/--password; use Authorization header. # $$ escapes so Compose does not interpolate AUTH at parse time. test: - CMD-SHELL - 'AUTH=$$(printf "%s" "$${OPENCODE_SERVER_USERNAME:-opencode}:$${OPENCODE_SERVER_PASSWORD}" | base64 | tr -d "\n"); wget -qO- --timeout=3 --header="Authorization: Basic $$AUTH" http://127.0.0.1:4096/global/health >/dev/null || exit 1' interval: 30s timeout: 10s retries: 3 start_period: 45s deploy: resources: limits: # Next.js builds need headroom beyond the bare serve binary. memory: 3G cpus: "2" networks: - echodo agent-server: profiles: ["agent-server"] image: ghcr.io/open-hands/agent-server:0.19.0 restart: unless-stopped environment: - AGENT_SERVER_HOST=0.0.0.0 - AGENT_SERVER_PORT=8001 - LOCAL_BACKEND_API_KEY=${LOCAL_BACKEND_API_KEY} - LLM_MODEL=${OMNIROUTE_MODEL:-auto/coding} - LLM_API_KEY=${OMNIROUTE_API_KEY} - LLM_BASE_URL=${OMNIROUTE_BASE_URL} - SANDBOX_TYPE=docker - SANDBOX_DOCKER_IMAGE=ghcr.io/open-hands/sandbox:latest - SANDBOX_TIMEOUT=300 - WORKSPACE_BASE=/workspace volumes: - echodo-workspace:/workspace - /var/run/docker.sock:/var/run/docker.sock expose: - "8001" healthcheck: test: - CMD-SHELL - "wget -qO- --timeout=3 http://127.0.0.1:8001/server_info >/dev/null || exit 1" interval: 30s timeout: 10s retries: 3 start_period: 15s deploy: resources: limits: memory: 2G cpus: "2" networks: - echodo volumes: echodo-workspace: opencode-data: networks: echodo: driver: bridge