# Self-host Sotto from pre-built public images (no source, no build). # The curl|bash installer (scripts/install.sh) writes .env and runs this. # # docker compose -f docker-compose.selfhost.yml up -d # # Images are published by .github/workflows/oss-image.yml. Postgres and Redis # are pulled from public registries. name: sotto services: postgres: image: pgvector/pgvector:pg17 environment: POSTGRES_USER: sotto POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?Set POSTGRES_PASSWORD in .env} POSTGRES_DB: sotto volumes: - pgdata:/var/lib/postgresql/data healthcheck: test: ['CMD-SHELL', 'pg_isready -h 127.0.0.1 -U sotto -d sotto'] interval: 5s timeout: 5s retries: 12 networks: [sotto-network] restart: unless-stopped redis: image: redis:7-alpine command: ['redis-server', '--appendonly', 'yes'] volumes: - redisdata:/data healthcheck: test: ['CMD', 'redis-cli', 'ping'] interval: 5s timeout: 5s retries: 12 networks: [sotto-network] restart: unless-stopped web: image: ghcr.io/affromero/sotto-web:${SOTTO_IMAGE_TAG:-latest} env_file: [.env] environment: NODE_ENV: production HOSTNAME: '0.0.0.0' BYOK_ENCRYPTION_KEY: ${BYOK_ENCRYPTION_KEY:?Set BYOK_ENCRYPTION_KEY in .env} SOTTO_CREDENTIAL_SYNC_DIR: /run/cli-credentials CLAUDE_HOME: /home/sotto/.claude CODEX_HOME: /home/sotto/.codex SIDEDOOR_EXECUTION_DIR: /app/apps/web/.sotto/runtime/executions ports: - '127.0.0.1:${WEB_PORT:-3000}:3000' volumes: - storage:/app/apps/web/.sotto/storage - executions:/app/apps/web/.sotto/runtime - cli-credentials:/run/cli-credentials - agent-home:/home/sotto/.claude - agent-codex-home:/home/sotto/.codex depends_on: postgres: { condition: service_healthy } redis: { condition: service_healthy } credential-sync: { condition: service_healthy } healthcheck: test: [ 'CMD', 'wget', '--no-verbose', '--tries=1', '--spider', 'http://127.0.0.1:3000/api/v1/health', ] interval: 30s timeout: 10s retries: 5 start_period: 40s networks: [sotto-network] restart: unless-stopped workers: image: ghcr.io/affromero/sotto-workers:${SOTTO_IMAGE_TAG:-latest} env_file: [.env] environment: NODE_ENV: production SOTTO_CREDENTIAL_SYNC_DIR: /run/cli-credentials CLAUDE_HOME: /home/sotto/.claude CODEX_HOME: /home/sotto/.codex SIDEDOOR_EXECUTION_DIR: /app/apps/web/.sotto/runtime/executions volumes: - storage:/app/apps/web/.sotto/storage - executions:/app/apps/web/.sotto/runtime - cli-credentials:/run/cli-credentials - agent-home:/home/sotto/.claude - agent-codex-home:/home/sotto/.codex depends_on: postgres: { condition: service_healthy } redis: { condition: service_healthy } credential-sync: { condition: service_healthy } networks: [sotto-network] restart: unless-stopped credential-sync: image: alpine:3.22 network_mode: none read_only: true tmpfs: [/tmp] volumes: - cli-credentials:/credential-sync - ${HOME}/.claude:/host-claude:ro - ${HOME}/.codex:/host-codex:ro - ./sync-cli-credentials.sh:/usr/local/bin/sync-cli-credentials:ro command: ['/bin/sh', '/usr/local/bin/sync-cli-credentials'] healthcheck: test: ['CMD', 'test', '-f', '/credential-sync/ready'] interval: 2s timeout: 1s retries: 15 restart: unless-stopped volumes: # The agent CLI rotates its OAuth refresh token in place and retires the old # one, so its credentials file has to outlive the container. agent-home: agent-codex-home: pgdata: redisdata: storage: executions: cli-credentials: networks: sotto-network: