2026-09-08 23:33:59 -04:00
# Echodo production stack — shared CT 102 Postgres/Redis, multi-service build.
#
# Phase A (default): web, collab, mcp, orchestrator, worker
2026-09-12 13:47:56 -04:00
# Phase B (optional coding harness):
# Preferred: OpenCode — COMPOSE_PROFILES=opencode, WORKER_SESSION_TYPE=opencode
# Legacy: OpenHands — COMPOSE_PROFILES=agent-server, WORKER_SESSION_TYPE=openhands
2026-09-08 23:33:59 -04:00
#
# 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)
2026-10-29 15:30:20 -04:00
# chat → chat.stillwell.cloud (collab service, CHAT_PORT default 1235)
2026-09-12 13:47:56 -04:00
# opencode (optional) → opencode.stillwell.cloud (port 4096)
2026-09-08 23:33:59 -04:00
#
# 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
#
2026-09-12 13:47:56 -04:00
# 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
2026-09-08 23:33:59 -04:00
# 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}
2026-10-29 15:30:20 -04:00
NEXT_PUBLIC_CHAT_SERVER_URL : ${NEXT_PUBLIC_CHAT_SERVER_URL:-ws://localhost:1235}
2026-09-08 23:33:59 -04:00
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}
2026-10-29 15:30:20 -04:00
- CHAT_TICKET_SECRET=${CHAT_TICKET_SECRET:?Set a shared Chat signing secret of at least 32 characters}
2026-09-08 23:33:59 -04:00
- 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
2026-10-29 15:30:20 -04:00
- CHAT_PORT=${CHAT_PORT:-1235}
- CHAT_TICKET_SECRET=${CHAT_TICKET_SECRET:?Set a shared Chat signing secret of at least 32 characters}
2026-09-08 23:33:59 -04:00
- NODE_ENV=production
expose :
- "1234"
2026-10-29 15:30:20 -04:00
- "${CHAT_PORT:-1235}"
2026-09-08 23:33:59 -04:00
healthcheck :
test :
- CMD-SHELL
2026-10-29 15:30:20 -04:00
- 'node -e "let remaining=2;for(const p of [+(process.env.PORT||1234),+(process.env.CHAT_PORT||1235)]){const s=require(' 'net' ').createConnection(p,' '127.0.0.1' ');s.setTimeout(3000,()=>process.exit(1));s.on(' 'connect' ',()=>{s.destroy();if(!--remaining)process.exit(0)}).on(' 'error' ',()=>process.exit(1))}"'
2026-09-08 23:33:59 -04:00
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:-}
2026-09-12 13:47:56 -04:00
- 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}
2026-09-08 23:33:59 -04:00
- 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
2026-09-12 13:47:56 -04:00
opencode :
profiles : [ "opencode" ]
2026-09-14 13:16:26 -04:00
build :
context : .
dockerfile : docker/opencode/Dockerfile
image : submatter-opencode:tooling
2026-09-12 13:47:56 -04:00
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}
2026-09-14 13:16:26 -04:00
- FORGEJO_TOKEN=${FORGEJO_TOKEN:-}
- GITHUB_TOKEN=${GITHUB_TOKEN:-}
- GIT_USER_NAME=${GIT_USER_NAME:-opencode}
- GIT_USER_EMAIL=${GIT_USER_EMAIL:-opencode@stillwell.cloud}
2026-09-12 13:47:56 -04:00
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 :
2026-09-12 17:31:45 -04:00
# BusyBox wget has no --user/--password; use Authorization header.
2026-09-12 17:32:15 -04:00
# $$ escapes so Compose does not interpolate AUTH at parse time.
2026-09-12 13:47:56 -04:00
test :
- CMD-SHELL
2026-09-12 17:32:15 -04:00
- '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'
2026-09-12 13:47:56 -04:00
interval : 30s
timeout : 10s
retries : 3
start_period : 45s
deploy :
resources :
limits :
2026-09-14 13:16:26 -04:00
# Next.js builds need headroom beyond the bare serve binary.
memory : 3G
2026-09-12 13:47:56 -04:00
cpus : "2"
networks :
- echodo
2026-09-08 23:33:59 -04:00
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 :
2026-09-14 14:51:13 -04:00
# Backed by ZFS bind mounts on CT 116 (apps/submatter/* → /mnt/*).
# Repos, worktrees, and builds grow here — not on the 32G local-lvm root.
2026-09-08 23:33:59 -04:00
echodo-workspace :
2026-09-14 14:51:13 -04:00
driver : local
driver_opts :
type : none
o : bind
device : /mnt/workspace
2026-09-12 13:47:56 -04:00
opencode-data :
2026-09-14 14:51:13 -04:00
driver : local
driver_opts :
type : none
o : bind
device : /mnt/opencode-data
2026-09-08 23:33:59 -04:00
networks :
echodo :
driver : bridge