docker-compose.yml
Rama main · réplica de lectura del repositorio original.
# Todd — self-hosted multi-agent operator.
# docker compose up -d --build → dashboard at http://localhost:3000
# docker compose --profile local up → also runs Ollama for fully local models
# Requires Docker Compose v2.24+.
#
# Isolation:
# core postgres, api, web (sandbox and browser can't reach the DB or the dashboard)
# sandbox_net api ↔ sandbox (agent-run code can reach the API port, but every API call needs a
# browser_net api ↔ browser token that only api and web have: see the api-token volume)
# signedin_net api ↔ signedin commands that use your sign-ins (git push, gh, deploy CLIs, EAS, CLI
# sign-ins) run here, in their own container with the same workspace: nothing running in the sandbox
# (a dev server, an npm install script) can read their tokens
# media_net api ↔ media embeddings, slides and MP4s for the video toolset: the shared workspace,
# a random token only the API has (media-token volume), no vault, fetches only MEDIA_FETCH_HOSTS
# previews localhost:3000 (and other dev ports) in the browser → api:17000+ → the same port in the sandbox, so
# agents can look at what they're building without deploying it (PREVIEW_PORTS / PREVIEW_RELAY_BASE)
# Published ports bind to 127.0.0.1 only. The dashboard answers only to localhost and only to its own pages (see
# services/web/lib/guard.ts; TODD_ALLOWED_HOSTS adds names), and the live view has a password (random unless
# VNC_PASSWORD is set). Todd can spend money: don't expose it publicly without adding real sign-in.
name: todd
services:
postgres:
image: postgres:16-alpine
restart: unless-stopped
environment:
POSTGRES_USER: todd
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-todd}
POSTGRES_DB: todd
volumes:
- pgdata:/var/lib/postgresql/data
networks: [core]
healthcheck:
test: ["CMD-SHELL", "pg_isready -U todd -d todd"]
interval: 5s
timeout: 3s
retries: 20
api:
build: ./services/api
restart: unless-stopped
env_file:
- path: .env
required: false
environment:
DATABASE_URL: postgresql+psycopg://todd:${POSTGRES_PASSWORD:-todd}@postgres:5432/todd
TODD_DATA_DIR: /data
TODD_API_TOKEN_FILE: /shared/api_token
SANDBOX_URL: http://sandbox:7000
SANDBOX_TOKEN: ${SANDBOX_TOKEN:-change-me-sandbox-token}
SIGNEDIN_URL: http://signedin:7000
SIGNEDIN_TOKEN_FILE: /signedin/token
MEDIA_URL: http://media:7000
MEDIA_TOKEN_FILE: /media/token
BROWSER_CDP_HOST: browser
BROWSER_CDP_PORT: "9223"
BROWSER_LIVE_URL: ${BROWSER_LIVE_URL:-http://localhost:6080/vnc_lite.html?scale=true}
VNC_PASSWORD: ${VNC_PASSWORD:-}
VNC_PASSWORD_FILE: /live/vnc_password
CORS_ORIGINS: http://localhost:3000
volumes:
- apidata:/data
- api-token:/shared
- signedin-token:/signedin
- media-token:/media
- live-password:/live:ro
- ./plugins:/app/custom/plugins
- ./prompts:/app/custom/prompts
networks: [core, sandbox_net, browser_net, signedin_net, media_net]
depends_on:
postgres:
condition: service_healthy
sandbox:
condition: service_started
signedin:
condition: service_started
media:
condition: service_started
browser:
condition: service_started
sandbox:
build: ./services/sandbox
restart: unless-stopped
init: true # reaps the background processes agents and CLI sign-ins leave behind
environment:
SANDBOX_TOKEN: ${SANDBOX_TOKEN:-change-me-sandbox-token}
volumes:
- workspace:/workspace
networks: [sandbox_net]
security_opt: ["no-new-privileges:true"]
# Same image, same workspace; runs only the commands that carry your sign-ins. Only the API reaches it, with a
# random token the API makes (signedin-token volume).
signedin:
build: ./services/sandbox
restart: unless-stopped
init: true
environment:
SANDBOX_TOKEN_FILE: /secret/token
SANDBOX_PREVIEWS: "0"
volumes:
- workspace:/workspace
- signedin-token:/secret:ro
networks: [signedin_net]
security_opt: ["no-new-privileges:true"]
# Image embeddings (CLIP), captioned slides and MP4s for the video toolset. Only the API reaches it, with a random
# token the API makes (media-token volume); it mounts the workspace and never sees the vault.
media:
build: ./services/media
restart: unless-stopped
init: true
environment:
MEDIA_TOKEN_FILE: /secret/token
WORKSPACE_ROOT: /workspace
MEDIA_FETCH_HOSTS: ${MEDIA_FETCH_HOSTS:-images.pexels.com,videos.pexels.com,pixabay.com,cdn.pixabay.com}
volumes:
- workspace:/workspace
- media-token:/secret:ro
networks: [media_net]
security_opt: ["no-new-privileges:true"]
browser:
build: ./services/browser
restart: unless-stopped
shm_size: 2gb
environment:
VNC_PASSWORD: ${VNC_PASSWORD:-}
SCREEN_WIDTH: "1440"
SCREEN_HEIGHT: "900"
volumes:
- browser-profile:/data
- live-password:/secret
networks: [browser_net]
ports:
- "127.0.0.1:6080:6080" # live view (noVNC)
web:
build: ./services/web
restart: unless-stopped
environment:
API_URL: http://api:8000
TODD_API_TOKEN_FILE: /shared/api_token
TODD_ALLOWED_HOSTS: ${TODD_ALLOWED_HOSTS:-} # names besides localhost the dashboard answers to (comma-separated)
volumes:
- api-token:/shared:ro
networks: [core]
ports:
- "127.0.0.1:3000:3000"
depends_on:
api:
condition: service_healthy
ollama:
image: ollama/ollama:latest
profiles: ["local"]
restart: unless-stopped
volumes:
- ollama:/root/.ollama
networks: [core]
networks:
core:
sandbox_net:
browser_net:
signedin_net:
media_net:
volumes:
pgdata:
apidata:
api-token:
signedin-token:
media-token:
live-password:
workspace:
browser-profile:
ollama: