# Echodo production stack — shared CT 102 Postgres/Redis, multi-service build. # # Phase A (default): web, collab, mcp, orchestrator, worker # Phase B (optional): OpenHands agent-server — enable with profile `agent-server` # # 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) # # 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: # docker compose -f docker/docker-compose.prod.yml --profile agent-server up -d # 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:-} - 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 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: networks: echodo: driver: bridge