# App services — deployed by blue-green swap. # Production deploy.sh requires an immutable image built on a separate machine. # The build definition remains available for local development and self-hosting. # # Usage: # WEB_PORT=3000 docker compose -f docker-compose.app.yml -p sotto-blue up -d # WEB_PORT=3010 docker compose -f docker-compose.app.yml -p sotto-green up -d services: web: image: ${SOTTO_WEB_IMAGE:-sotto-web}:${SOTTO_IMAGE_TAG:-local} build: context: . dockerfile: apps/web/Dockerfile args: COMMIT_SHA: ${COMMIT_SHA:-dev} RUNTIME_UID: '1000' NEXT_PUBLIC_APP_URL: ${NEXT_PUBLIC_APP_URL:-} NEXT_PUBLIC_VAPID_PUBLIC_KEY: ${NEXT_PUBLIC_VAPID_PUBLIC_KEY:-} restart: unless-stopped deploy: resources: limits: memory: 1g ports: - '127.0.0.1:${WEB_PORT:?Set WEB_PORT}:3000' env_file: - .env environment: NODE_ENV: production HOSTNAME: '0.0.0.0' DATABASE_URL: ${DATABASE_URL:?Set DATABASE_URL} REDIS_URL: ${REDIS_URL:?Set REDIS_URL} BYOK_ENCRYPTION_KEY: ${BYOK_ENCRYPTION_KEY:?Set BYOK_ENCRYPTION_KEY} SOTTO_CREDENTIAL_SYNC_DIR: /run/cli-credentials LOCAL_STORAGE_DIR: ${LOCAL_STORAGE_DIR:-/app/apps/web/.sotto/storage} SIDEDOOR_EXECUTION_DIR: /app/apps/web/.sotto/runtime/executions CLAUDE_HOME: /home/sotto/.claude CODEX_HOME: /home/sotto/.codex # Shared CLI homes have one UID 1000 owner across web and worker images. volumes: # Shared with the workers that write it; see docker-compose.workers.yml. - 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 healthcheck: test: [ 'CMD', 'wget', '--no-verbose', '--tries=1', '--spider', 'http://127.0.0.1:3000/api/v1/health', ] interval: 30s timeout: 10s retries: 3 start_period: 40s networks: - sotto-network logging: driver: json-file options: max-size: '50m' max-file: '5' networks: sotto-network: external: true name: sotto-network volumes: # Host-wide, deliberately NOT stack-scoped: the CLIs rotate their refresh token # in place and the previous one is retired server-side, so two stacks holding # separate copies of one login would kill each other's token. One file, one # lineage, shared by every stack on this host. agent-home: external: true name: agent-claude-home agent-codex-home: external: true name: agent-codex-home cli-credentials: external: true name: ${SOTTO_STACK:?Set SOTTO_STACK}-cli-credentials # Shared by both blue and green and by the worker stack, so it is named # rather than project-scoped. Not external: whichever stack starts first # creates it. storage: name: ${SOTTO_STACK:?Set SOTTO_STACK}-storage executions: name: ${SOTTO_STACK:?Set SOTTO_STACK}-executions