x-logging: &default-logging driver: json-file options: max-size: "50m" max-file: "5" x-app-environment: &app-environment # Application database. Temporal owns separate schemas and never uses this URL. DATABASE_URL: "${COMPOSE_DATABASE_URL:?Set COMPOSE_DATABASE_URL in .env; use the mysql service hostname}" REDIS_HOST: redis REDIS_PORT: "6379" REDIS_USERNAME: "" REDIS_PASSWORD: "${REDIS_PASSWORD:?Set REDIS_PASSWORD in .env}" REDIS_TLS: "" # Edition overlays own the concrete storage endpoint. Empty values here keep # this shared base composable; runtime storage initialization still fails # closed when the selected edition does not provide a complete contract. S3_ENDPOINT: "${S3_ENDPOINT:-}" S3_UPLOAD_ENDPOINT: "${S3_UPLOAD_ENDPOINT:-}" S3_REGION: "${S3_REGION:-us-east-1}" S3_BUCKET: "${S3_BUCKET:-}" S3_ACCESS_KEY_ID: "${S3_ACCESS_KEY_ID:-}" S3_SECRET_ACCESS_KEY: "${S3_SECRET_ACCESS_KEY:-}" S3_SESSION_TOKEN: "${S3_SESSION_TOKEN:-}" S3_FORCE_PATH_STYLE: "${S3_FORCE_PATH_STYLE:-false}" NEXTAUTH_URL: "${NEXTAUTH_URL:-http://localhost:13000}" INTERNAL_APP_URL: "http://app:3000" NEXTAUTH_SECRET: "${NEXTAUTH_SECRET:?Set NEXTAUTH_SECRET in .env}" CRON_SECRET: "${CRON_SECRET:?Set CRON_SECRET in .env}" API_ENCRYPTION_KEY: "${API_ENCRYPTION_KEY:?Set API_ENCRYPTION_KEY in .env}" TRUSTED_PROXY_HOPS: "${TRUSTED_PROXY_HOPS:-0}" LOG_UNIFIED_ENABLED: "true" LOG_LEVEL: "${LOG_LEVEL:-INFO}" LOG_DEBUG_ENABLED: "${LOG_DEBUG_ENABLED:-false}" LOG_AUDIT_ENABLED: "${LOG_AUDIT_ENABLED:-true}" LOG_SERVICE: "waoowaoo" DEPLOYMENT_EDITION: "self-hosted" PROVIDER_CREDENTIAL_MODE: "user-key" BILLING_MODE: "OFF" LLM_STREAM_EPHEMERAL_ENABLED: "${LLM_STREAM_EPHEMERAL_ENABLED:-true}" TEMPORAL_ADDRESS: "temporal:7233" TEMPORAL_NAMESPACE: "${TEMPORAL_NAMESPACE:-waoowaoo}" TEMPORAL_TASK_QUEUE: "${TEMPORAL_TASK_QUEUE:-waoowaoo-runtime}" TEMPORAL_API_KEY: "" TEMPORAL_TLS_ENABLED: "false" x-app-service: &app-service image: "${APP_IMAGE:-ghcr.io/waooai/waoowaoo@sha256:0000000000000000000000000000000000000000000000000000000000000000}" restart: unless-stopped logging: *default-logging environment: *app-environment volumes: - ./data:/app/data - ./docker-logs:/app/logs depends_on: mysql: condition: service_healthy app-schema: condition: service_completed_successfully redis: condition: service_healthy temporal-create-namespace: condition: service_completed_successfully x-temporal-worker-service: &temporal-worker-service restart: unless-stopped logging: *default-logging volumes: - ./data:/app/data - ./docker-logs:/app/logs depends_on: mysql: condition: service_healthy app-schema: condition: service_completed_successfully redis: condition: service_healthy temporal-create-namespace: condition: service_completed_successfully command: - /bin/sh - -ec - | case "$$TEMPORAL_WORKER_IMAGE" in *@sha256:*) ;; *) echo "TEMPORAL_WORKER_IMAGE must be an immutable repository@sha256 digest" >&2 exit 1 ;; esac worker_digest=$${TEMPORAL_WORKER_IMAGE#*@sha256:} if [ "$${#worker_digest}" -ne 64 ]; then echo "TEMPORAL_WORKER_IMAGE sha256 digest must contain exactly 64 hexadecimal characters" >&2 exit 1 fi if [ "$$worker_digest" = "0000000000000000000000000000000000000000000000000000000000000000" ]; then echo "TEMPORAL_WORKER_IMAGE still contains the local-development placeholder digest" >&2 exit 1 fi case "$$worker_digest" in *[!0-9a-fA-F]*) echo "TEMPORAL_WORKER_IMAGE sha256 digest contains non-hexadecimal characters" >&2 exit 1 ;; esac npm run storage:init exec npm run start:temporal-worker services: mysql: image: mysql:8.0 restart: unless-stopped logging: *default-logging environment: MYSQL_ROOT_PASSWORD: "${MYSQL_ROOT_PASSWORD:?Set MYSQL_ROOT_PASSWORD in .env}" MYSQL_DATABASE: "${MYSQL_DATABASE:-waoowaoo}" MYSQL_USER: "${MYSQL_USER:-waoowaoo}" MYSQL_PASSWORD: "${MYSQL_PASSWORD:?Set MYSQL_PASSWORD in .env}" ports: - "${INFRA_BIND_ADDRESS:-127.0.0.1}:${MYSQL_HOST_PORT:-13306}:3306" volumes: - mysql_data:/var/lib/mysql command: - "--default-authentication-plugin=mysql_native_password" - "--sql_mode=STRICT_TRANS_TABLES,NO_ENGINE_SUBSTITUTION" healthcheck: test: ["CMD-SHELL", "mysqladmin ping -h 127.0.0.1 -uroot -p\"$$MYSQL_ROOT_PASSWORD\""] interval: 5s timeout: 5s retries: 30 start_period: 15s redis: image: redis:7-alpine restart: unless-stopped logging: *default-logging environment: REDIS_PASSWORD: "${REDIS_PASSWORD:?Set REDIS_PASSWORD in .env}" ports: - "${INFRA_BIND_ADDRESS:-127.0.0.1}:${REDIS_HOST_PORT:-16379}:6379" volumes: - redis_data:/data command: ["sh", "-c", "exec redis-server --appendonly yes --requirepass \"$$REDIS_PASSWORD\""] healthcheck: test: ["CMD-SHELL", "redis-cli -a \"$$REDIS_PASSWORD\" ping"] interval: 5s timeout: 5s retries: 30 start_period: 5s # One explicit application-schema writer prevents Web and Worker races. # This is retained for the current self-hosted release contract; production # deployments should run the reviewed migration before replacing the image. app-schema: image: "${APP_IMAGE:-ghcr.io/waooai/waoowaoo@sha256:0000000000000000000000000000000000000000000000000000000000000000}" restart: "no" logging: *default-logging environment: *app-environment depends_on: mysql: condition: service_healthy command: ["npm", "run", "db:push"] temporal-db-bootstrap: image: mysql:8.0 restart: "no" logging: *default-logging network_mode: "service:mysql" depends_on: mysql: condition: service_healthy environment: MYSQL_HOST: 127.0.0.1 MYSQL_PORT: "3306" MYSQL_ROOT_PASSWORD: "${MYSQL_ROOT_PASSWORD:?Set MYSQL_ROOT_PASSWORD in .env}" TEMPORAL_MYSQL_USER: "${TEMPORAL_MYSQL_USER:-temporal}" TEMPORAL_MYSQL_PASSWORD: "${TEMPORAL_MYSQL_PASSWORD:?Set TEMPORAL_MYSQL_PASSWORD in .env}" entrypoint: ["/bin/sh", "-ec"] command: - | case "$$TEMPORAL_MYSQL_USER" in ''|*[!A-Za-z0-9_]*) echo "TEMPORAL_MYSQL_USER must contain only letters, digits, and underscores" >&2 exit 1 ;; esac if [ "$${#TEMPORAL_MYSQL_USER}" -gt 32 ]; then echo "TEMPORAL_MYSQL_USER must not exceed 32 characters" >&2 exit 1 fi temporal_password_base64=$$( printf '%s' "$$TEMPORAL_MYSQL_PASSWORD" | base64 | tr -d '\n' ) MYSQL_PWD="$$MYSQL_ROOT_PASSWORD" mysql \ --protocol=TCP \ --host="$$MYSQL_HOST" \ --port="$$MYSQL_PORT" \ --user=root < /config/docker.yaml test -s /config/docker.yaml temporal: image: temporalio/server:1.31.2 restart: unless-stopped logging: *default-logging depends_on: temporal-schema: condition: service_completed_successfully temporal-config: condition: service_completed_successfully environment: DB: mysql8 DB_PORT: "3306" MYSQL_SEEDS: mysql MYSQL_USER: "${TEMPORAL_MYSQL_USER:-temporal}" MYSQL_PWD: "${TEMPORAL_MYSQL_PASSWORD:?Set TEMPORAL_MYSQL_PASSWORD in .env}" BIND_ON_IP: 0.0.0.0 ports: - "${INFRA_BIND_ADDRESS:-127.0.0.1}:${TEMPORAL_HOST_PORT:-17233}:7233" volumes: - temporal_config:/etc/temporal/config/dynamicconfig:ro healthcheck: test: ["CMD", "nc", "-z", "127.0.0.1", "7233"] interval: 5s timeout: 3s retries: 60 start_period: 30s temporal-create-namespace: image: temporalio/admin-tools:1.31.2 restart: "no" logging: *default-logging depends_on: temporal: condition: service_healthy environment: TEMPORAL_ADDRESS: temporal:7233 TEMPORAL_NAMESPACE: "${TEMPORAL_NAMESPACE:-waoowaoo}" TEMPORAL_NAMESPACE_RETENTION: "${TEMPORAL_NAMESPACE_RETENTION:-30d}" entrypoint: ["/bin/sh", "-ec"] command: - | max_attempts=$${TEMPORAL_HEALTH_CHECK_MAX_ATTEMPTS:-30} sleep_seconds=$${TEMPORAL_HEALTH_CHECK_SLEEP_SECONDS:-5} case "$$max_attempts" in ''|*[!0-9]*|0) echo "TEMPORAL_HEALTH_CHECK_MAX_ATTEMPTS must be a positive integer" >&2; exit 1 ;; esac case "$$sleep_seconds" in ''|*[!0-9]*) echo "TEMPORAL_HEALTH_CHECK_SLEEP_SECONDS must be a non-negative integer" >&2; exit 1 ;; esac attempt=1 until temporal operator cluster health --address "$$TEMPORAL_ADDRESS" >/dev/null 2>&1; do if [ "$$attempt" -ge "$$max_attempts" ]; then echo "Temporal server did not become healthy" >&2 exit 1 fi attempt=$$((attempt + 1)) sleep "$$sleep_seconds" done if temporal operator namespace describe \ --namespace "$$TEMPORAL_NAMESPACE" \ --address "$$TEMPORAL_ADDRESS" >/dev/null 2>&1; then echo "Temporal namespace '$$TEMPORAL_NAMESPACE' already exists." exit 0 fi temporal operator namespace create \ --namespace "$$TEMPORAL_NAMESPACE" \ --retention "$$TEMPORAL_NAMESPACE_RETENTION" \ --address "$$TEMPORAL_ADDRESS" temporal-ui: image: temporalio/ui:2.49.1 profiles: ["temporal-ui"] restart: unless-stopped logging: *default-logging depends_on: temporal-create-namespace: condition: service_completed_successfully environment: TEMPORAL_ADDRESS: temporal:7233 TEMPORAL_CORS_ORIGINS: "${NEXTAUTH_URL:-http://localhost:13000}" ports: - "127.0.0.1:${TEMPORAL_UI_HOST_PORT:-18080}:8080" # App startup verifies that an explicit Worker rollout already established a # Current Version. This service never changes routing; bootstrap and later # promotions are owned only by scripts/temporal/worker-rollout.sh. temporal-verify-worker-version: image: temporalio/admin-tools:1.31.2 restart: "no" logging: *default-logging depends_on: temporal-create-namespace: condition: service_completed_successfully environment: TEMPORAL_ADDRESS: temporal:7233 TEMPORAL_NAMESPACE: "${TEMPORAL_NAMESPACE:-waoowaoo}" TEMPORAL_WORKER_DEPLOYMENT_NAME: "${TEMPORAL_WORKER_DEPLOYMENT_NAME:-waoowaoo}" entrypoint: ["/bin/sh", "-ec"] command: - | attempt=1 while [ "$$attempt" -le 60 ]; do deployment_json=$$( temporal worker deployment describe \ --name "$$TEMPORAL_WORKER_DEPLOYMENT_NAME" \ --namespace "$$TEMPORAL_NAMESPACE" \ --address "$$TEMPORAL_ADDRESS" \ --output json 2>/dev/null || true ) if printf '%s\n' "$$deployment_json" \ | grep -Eq '"currentVersionBuildID"[[:space:]]*:[[:space:]]*"[^"]+"'; then echo "Temporal Worker Deployment has an explicit Current Version." exit 0 fi attempt=$$((attempt + 1)) sleep 2 done echo "Temporal Worker Deployment has no Current Version; run temporal:worker:rollout bootstrap first" >&2 exit 1 temporal-admin: image: temporalio/admin-tools:1.31.2 profiles: ["temporal-ops"] network_mode: "service:temporal" entrypoint: ["temporal"] app: <<: *app-service environment: <<: *app-environment CODEX_RUNTIME_DRIVER: docker CODEX_RUNTIME_HOST_ROOT: "${CODEX_RUNTIME_HOST_ROOT:-/var/lib/wao/codex-runtime}" CODEX_RUNTIME_IDLE_TIMEOUT_MS: "${CODEX_RUNTIME_IDLE_TIMEOUT_MS:-900000}" CODEX_RUNTIME_IMAGE: "${CODEX_RUNTIME_IMAGE:-ghcr.io/waooai/waoowaoo-codex-runtime@sha256:0000000000000000000000000000000000000000000000000000000000000000}" CODEX_RUNTIME_NETWORK: "${COMPOSE_PROJECT_NAME:-waoowaoo}_codex-runtime" CODEX_RUNTIME_CPU_LIMIT: "${CODEX_RUNTIME_CPU_LIMIT:-2}" CODEX_RUNTIME_MEMORY_BYTES: "${CODEX_RUNTIME_MEMORY_BYTES:-2147483648}" CODEX_RUNTIME_PIDS_LIMIT: "${CODEX_RUNTIME_PIDS_LIMIT:-256}" CODEX_RUNTIME_WAO_BASE_URL: "http://app:3000" ports: - "${APP_BIND_ADDRESS:-127.0.0.1}:${APP_HOST_PORT:-13000}:3000" volumes: - ./data:/app/data - ./docker-logs:/app/logs - "${DOCKER_SOCKET_PATH:-/var/run/docker.sock}:/var/run/docker.sock" # Mount this host path from durable storage in production. The daemon and # Web container must resolve the exact same path because runtime bind # mounts are created by the daemon and Codex owns Thread persistence here. - type: bind source: "${CODEX_RUNTIME_HOST_ROOT:-/var/lib/wao/codex-runtime}" target: "${CODEX_RUNTIME_HOST_ROOT:-/var/lib/wao/codex-runtime}" networks: - default - codex-runtime depends_on: mysql: condition: service_healthy app-schema: condition: service_completed_successfully redis: condition: service_healthy temporal-verify-worker-version: condition: service_completed_successfully command: ["sh", "-c", "npm run storage:init && exec npm run start:next"] temporal-worker-blue: <<: *temporal-worker-service profiles: ["temporal-worker-rollout"] image: "${TEMPORAL_WORKER_BLUE_IMAGE:-ghcr.io/waooai/waoowaoo@sha256:0000000000000000000000000000000000000000000000000000000000000000}" environment: <<: *app-environment TEMPORAL_WORKER_DEPLOYMENT_NAME: "${TEMPORAL_WORKER_DEPLOYMENT_NAME:-waoowaoo}" TEMPORAL_WORKER_BUILD_ID: "${TEMPORAL_WORKER_BLUE_BUILD_ID:-}" TEMPORAL_WORKER_VERSIONING_ENABLED: "true" TEMPORAL_WORKER_SLOT: blue TEMPORAL_WORKER_IMAGE: "${TEMPORAL_WORKER_BLUE_IMAGE:-ghcr.io/waooai/waoowaoo@sha256:0000000000000000000000000000000000000000000000000000000000000000}" deploy: replicas: ${TEMPORAL_WORKER_BLUE_REPLICAS:-1} temporal-worker-green: <<: *temporal-worker-service profiles: ["temporal-worker-rollout"] image: "${TEMPORAL_WORKER_GREEN_IMAGE:-ghcr.io/waooai/waoowaoo@sha256:0000000000000000000000000000000000000000000000000000000000000000}" environment: <<: *app-environment TEMPORAL_WORKER_DEPLOYMENT_NAME: "${TEMPORAL_WORKER_DEPLOYMENT_NAME:-waoowaoo}" TEMPORAL_WORKER_BUILD_ID: "${TEMPORAL_WORKER_GREEN_BUILD_ID:-}" TEMPORAL_WORKER_VERSIONING_ENABLED: "true" TEMPORAL_WORKER_SLOT: green TEMPORAL_WORKER_IMAGE: "${TEMPORAL_WORKER_GREEN_IMAGE:-ghcr.io/waooai/waoowaoo@sha256:0000000000000000000000000000000000000000000000000000000000000000}" deploy: replicas: ${TEMPORAL_WORKER_GREEN_REPLICAS:-0} volumes: mysql_data: redis_data: temporal_config: networks: codex-runtime: name: "${COMPOSE_PROJECT_NAME:-waoowaoo}_codex-runtime" internal: true