Submatter/docker/docker-compose.prod.yml
Randall Stillwell d90ff11f5b
Some checks are pending
CI / Lint, type-check, test (push) Waiting to run
Deploy homelab (CT 116) / Deploy to CT 116 (push) Waiting to run
feat(opencode): ship tooling image with git, node, and build deps
Extend the Alpine OpenCode base so agent sessions can clone, npm/pnpm
install, and compile native modules; seed Forgejo HTTPS creds at start.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-14 12:16:26 -05:00

313 lines
10 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"]
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