Compose was interpolating $AUTH at parse time, breaking the BusyBox probe. Co-authored-by: Cursor <cursoragent@cursor.com>
305 lines
9.9 KiB
YAML
305 lines
9.9 KiB
YAML
# 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"]
|
|
image: ghcr.io/anomalyco/opencode:latest
|
|
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}
|
|
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:
|
|
memory: 2G
|
|
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
|