# Docker Compose for production deployment # # Local testing: # Build images: bazel run //ci:eagle_server_load && bazel run //ci:auth_server_load # Run: docker compose -f docker-compose.prod.yml up # # Production deployment: # Run: docker compose -f docker-compose.prod.yml up -d # # Note: Shardok runs on Hetzner ARM64 server, deployed via shardok_arm64_build.yml workflow. services: # Blue-green deployment: eagle-blue is the primary (production) instance # eagle-green is the staging instance for zero-downtime deployments # See scripts/deploy-blue-green.sh for deployment workflow eagle-blue: image: ${EAGLE_IMAGE:-registry.digitalocean.com/eagle0/eagle-server:latest} container_name: eagle-blue command: - "--shardok-interface-remote-address" - "${SHARDOK_ADDRESS}" - "--auth-service-url" - "auth:40033" ports: - "40032:40032" environment: OPENAI_API_KEY: "${OPENAI_API_KEY:-}" ANTHROPIC_API_KEY: "${ANTHROPIC_API_KEY:-}" GEMINI_API_KEY: "${GEMINI_API_KEY:-}" EAGLE_ENABLE_S3: "${EAGLE_ENABLE_S3:-false}" DO_SPACES_ENDPOINT: "${DO_SPACES_ENDPOINT:-https://sfo3.digitaloceanspaces.com}" DO_SPACES_ACCESS_KEY: "${DO_SPACES_ACCESS_KEY:-}" DO_SPACES_SECRET_KEY: "${DO_SPACES_SECRET_KEY:-}" EAGLE_HISTORY_BACKEND: "${EAGLE_HISTORY_BACKEND:-postgres}" EAGLE_POSTGRES_HOST: "${EAGLE_POSTGRES_HOST:-}" EAGLE_POSTGRES_PORT: "${EAGLE_POSTGRES_PORT:-}" EAGLE_POSTGRES_DATABASE: "${EAGLE_POSTGRES_DATABASE:-}" EAGLE_POSTGRES_USER: "${EAGLE_POSTGRES_USER:-}" EAGLE_POSTGRES_PASSWORD: "${EAGLE_POSTGRES_PASSWORD:-}" EAGLE_POSTGRES_SSLMODE: "${EAGLE_POSTGRES_SSLMODE:-require}" EAGLE_POSTGRES_MAX_POOL_SIZE: "${EAGLE_POSTGRES_MAX_POOL_SIZE:-7}" EAGLE_POSTGRES_MIN_IDLE: "${EAGLE_POSTGRES_MIN_IDLE:-0}" EAGLE_POSTGRES_CONNECTION_TIMEOUT_MILLIS: "${EAGLE_POSTGRES_CONNECTION_TIMEOUT_MILLIS:-10000}" EAGLE_POSTGRES_IDLE_TIMEOUT_MILLIS: "${EAGLE_POSTGRES_IDLE_TIMEOUT_MILLIS:-120000}" EAGLE_POSTGRES_MAX_LIFETIME_MILLIS: "${EAGLE_POSTGRES_MAX_LIFETIME_MILLIS:-1500000}" EAGLE_POSTGRES_KEEPALIVE_TIME_MILLIS: "${EAGLE_POSTGRES_KEEPALIVE_TIME_MILLIS:-300000}" EAGLE_POSTGRES_LEAK_DETECTION_THRESHOLD_MILLIS: "${EAGLE_POSTGRES_LEAK_DETECTION_THRESHOLD_MILLIS:-0}" # JWT public key for token validation (auth service handles signing) # Reads from /etc/eagle0/keys/public.pem via shared volume # Auth token for Shardok on Hetzner (required) SHARDOK_AUTH_TOKEN: "${SHARDOK_AUTH_TOKEN:-}" # Use persistent volume for save data (users, games, etc.) EAGLE_SAVE_DIR: "/app/saves" EAGLE_ARCHIVE_DIR: "/app/archived" SENTRY_DSN: "${SENTRY_DSN:-}" SENTRY_ENVIRONMENT: "production" volumes: - ./saves:/app/saves # Game saves and user database - ./archived:/app/archived # Archived completed games - ./jfr:/app/jfr # JFR recordings - dump with: docker exec eagle-blue jcmd 1 JFR.dump filename=/app/jfr/profile.jfr - jvm-tmp:/tmp # Shared with jfr-sidecar for JVM attach socket files - jwt-keys:/etc/eagle0/keys:ro # JWT public key from auth service (read-only) depends_on: - auth restart: unless-stopped logging: driver: "json-file" options: max-size: "100m" max-file: "5" healthcheck: test: ["CMD-SHELL", "nc -z localhost 40032 || exit 1"] interval: 30s timeout: 10s retries: 3 start_period: 30s eagle-green: image: ${EAGLE_IMAGE_NEW:-registry.digitalocean.com/eagle0/eagle-server:latest} container_name: eagle-green profiles: ["blue-green"] # Only started during blue-green deployment command: - "--shardok-interface-remote-address" - "${SHARDOK_ADDRESS}" - "--auth-service-url" - "auth:40033" ports: - "40034:40032" # Different host port for staging environment: OPENAI_API_KEY: "${OPENAI_API_KEY:-}" ANTHROPIC_API_KEY: "${ANTHROPIC_API_KEY:-}" GEMINI_API_KEY: "${GEMINI_API_KEY:-}" EAGLE_ENABLE_S3: "${EAGLE_ENABLE_S3:-false}" DO_SPACES_ENDPOINT: "${DO_SPACES_ENDPOINT:-https://sfo3.digitaloceanspaces.com}" DO_SPACES_ACCESS_KEY: "${DO_SPACES_ACCESS_KEY:-}" DO_SPACES_SECRET_KEY: "${DO_SPACES_SECRET_KEY:-}" EAGLE_HISTORY_BACKEND: "${EAGLE_HISTORY_BACKEND:-postgres}" EAGLE_POSTGRES_HOST: "${EAGLE_POSTGRES_HOST:-}" EAGLE_POSTGRES_PORT: "${EAGLE_POSTGRES_PORT:-}" EAGLE_POSTGRES_DATABASE: "${EAGLE_POSTGRES_DATABASE:-}" EAGLE_POSTGRES_USER: "${EAGLE_POSTGRES_USER:-}" EAGLE_POSTGRES_PASSWORD: "${EAGLE_POSTGRES_PASSWORD:-}" EAGLE_POSTGRES_SSLMODE: "${EAGLE_POSTGRES_SSLMODE:-require}" EAGLE_POSTGRES_MAX_POOL_SIZE: "${EAGLE_POSTGRES_MAX_POOL_SIZE:-7}" EAGLE_POSTGRES_MIN_IDLE: "${EAGLE_POSTGRES_MIN_IDLE:-0}" EAGLE_POSTGRES_CONNECTION_TIMEOUT_MILLIS: "${EAGLE_POSTGRES_CONNECTION_TIMEOUT_MILLIS:-10000}" EAGLE_POSTGRES_IDLE_TIMEOUT_MILLIS: "${EAGLE_POSTGRES_IDLE_TIMEOUT_MILLIS:-120000}" EAGLE_POSTGRES_MAX_LIFETIME_MILLIS: "${EAGLE_POSTGRES_MAX_LIFETIME_MILLIS:-1500000}" EAGLE_POSTGRES_KEEPALIVE_TIME_MILLIS: "${EAGLE_POSTGRES_KEEPALIVE_TIME_MILLIS:-300000}" EAGLE_POSTGRES_LEAK_DETECTION_THRESHOLD_MILLIS: "${EAGLE_POSTGRES_LEAK_DETECTION_THRESHOLD_MILLIS:-0}" # JWT public key for token validation (auth service handles signing) # Reads from /etc/eagle0/keys/public.pem via shared volume SHARDOK_AUTH_TOKEN: "${SHARDOK_AUTH_TOKEN:-}" EAGLE_SAVE_DIR: "/app/saves" EAGLE_ARCHIVE_DIR: "/app/archived" SENTRY_DSN: "${SENTRY_DSN:-}" SENTRY_ENVIRONMENT: "production" volumes: - ./saves:/app/saves # Same save directory as blue - ./archived:/app/archived # Same archive directory as blue - ./jfr:/app/jfr # JFR recordings (same as blue) - jvm-tmp:/tmp # Shared with jfr-sidecar-green for JVM attach socket files - jwt-keys:/etc/eagle0/keys:ro # JWT public key from auth service (read-only) depends_on: - auth restart: "no" # Don't auto-restart during deployment logging: driver: "json-file" options: max-size: "100m" max-file: "5" healthcheck: test: ["CMD-SHELL", "nc -z localhost 40032 || exit 1"] interval: 10s timeout: 5s retries: 6 start_period: 60s # Backward compatibility alias - for scripts that reference 'eagle' service eagle: extends: service: eagle-blue auth: image: ${AUTH_IMAGE:-registry.digitalocean.com/eagle0/auth-server:latest} container_name: auth-server environment: # gRPC port for Auth service AUTH_GRPC_PORT: "40033" # HTTP port for OAuth callbacks AUTH_HTTP_PORT: "8080" # User data persistence directory AUTH_DATA_DIR: "/app/data" # Legacy path for migrating users from Eagle (Phase 1 migration) AUTH_LEGACY_DATA_DIR: "/app/saves/auth" # OAuth provider credentials DISCORD_CLIENT_ID: "${DISCORD_CLIENT_ID:-}" DISCORD_CLIENT_SECRET: "${DISCORD_CLIENT_SECRET:-}" GOOGLE_CLIENT_ID: "${GOOGLE_CLIENT_ID:-}" GOOGLE_CLIENT_SECRET: "${GOOGLE_CLIENT_SECRET:-}" GH_OAUTH_CLIENT_ID: "${GH_OAUTH_CLIENT_ID:-}" GH_OAUTH_CLIENT_SECRET: "${GH_OAUTH_CLIENT_SECRET:-}" # Apple Sign-In credentials APPLE_SIGNIN_CLIENT_ID: "${APPLE_SIGNIN_CLIENT_ID:-}" APPLE_TEAM_ID: "${APPLE_TEAM_ID:-}" APPLE_SIGNIN_KEY_ID: "${APPLE_SIGNIN_KEY_ID:-}" APPLE_SIGNIN_PRIVATE_KEY: "${APPLE_SIGNIN_PRIVATE_KEY:-}" # Twitch OAuth credentials TWITCH_CLIENT_ID: "${TWITCH_CLIENT_ID:-}" TWITCH_CLIENT_SECRET: "${TWITCH_CLIENT_SECRET:-}" # Server base URL for OAuth callbacks SERVER_BASE_URL: "${SERVER_BASE_URL:-https://prod.eagle0.net}" # JWT keys - PEM files in volume, bootstrapped from JWK on first run JWT_KEYS_PATH: "/etc/eagle0/keys" JWT_PRIVATE_KEY: "${JWT_PRIVATE_KEY:-}" # Fastmail JMAP API for sending invitation emails FASTMAIL_API_TOKEN: "${FASTMAIL_API_TOKEN:-}" FASTMAIL_FROM_EMAIL: "${FASTMAIL_FROM_EMAIL:-}" FASTMAIL_FROM_NAME: "${FASTMAIL_FROM_NAME:-}" # Require invitation codes for new user registration REQUIRE_INVITATION_CODE: "true" # Note: port 40033 is exposed via nginx, not directly volumes: - jwt-keys:/etc/eagle0/keys # Shared JWT keys with Eagle - ./auth-data:/app/data # User database persistence - ./saves:/app/saves:ro # Read-only access to Eagle's saves for migration restart: unless-stopped logging: driver: "json-file" options: max-size: "50m" max-file: "3" healthcheck: test: ["CMD-SHELL", "nc -z localhost 40033 || exit 1"] interval: 30s timeout: 10s retries: 3 start_period: 10s # Note: Shardok runs on Hetzner ARM64 server, not in this docker-compose. # Configure SHARDOK_ADDRESS to point to the Hetzner instance. nginx: image: nginx:alpine container_name: nginx ports: - "443:443" - "80:80" - "40033:40033" # Go Auth service gRPC (Phase 2 direct client connections) volumes: - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro - ./certbot/conf:/etc/letsencrypt:ro - ./certbot/www:/var/www/certbot:ro - ./auth:/etc/nginx/auth:ro depends_on: - admin # Note: nginx connects to eagle via EAGLE_ADDR (default: eagle-blue:40032) # For blue-green deployments, update EAGLE_ADDR in .env before switching restart: unless-stopped logging: driver: "json-file" options: max-size: "50m" max-file: "3" admin: image: ${ADMIN_IMAGE:-registry.digitalocean.com/eagle0/admin-server:latest} container_name: admin-server command: - "--eagle-addr" - "${EAGLE_ADDR:-eagle-blue:40032}" # Can be switched for blue-green - "--auth-addr" - "auth:40033" - "--jfr-sidecar-addr" - "${JFR_SIDECAR_ADDR:-jfr-sidecar:8081}" # Can be switched for blue-green - "--shardok-profile-addr" - "${SHARDOK_ADDRESS}" - "--shardok-profile-tls" - "true" - "--http-port" - "8080" - "--maps-dir" - "/app/maps" environment: # Secret for CI to authenticate client update notifications NOTIFY_SECRET: "${NOTIFY_SECRET:-}" # S3/Spaces credentials for What's New storage (uses eagle0-assets bucket) DO_SPACES_ENDPOINT: "${DO_SPACES_ENDPOINT:-https://sfo3.digitaloceanspaces.com}" DO_SPACES_ACCESS_KEY: "${ADMIN_S3_ACCESS_KEY:-}" DO_SPACES_SECRET_KEY: "${ADMIN_S3_SECRET_KEY:-}" # GitHub PAT for map editor PR creation (scoped to contents:write + pull_requests:write) GITHUB_TOKEN: "${GITHUB_TOKEN:-}" # Reuse Eagle's Shardok endpoint and auth token for admin-only CPU profiling. SHARDOK_AUTH_TOKEN: "${SHARDOK_AUTH_TOKEN:-}" volumes: - /var/run/docker.sock:/var/run/docker.sock # No external port - accessed via nginx at admin.eagle0.net depends_on: - auth # Note: admin connects to eagle via EAGLE_ADDR and jfr-sidecar via JFR_SIDECAR_ADDR # For blue-green deployments, set both in .env before switching restart: unless-stopped logging: driver: "json-file" options: max-size: "50m" max-file: "3" healthcheck: test: ["CMD-SHELL", "wget -q --spider http://localhost:8080/health || exit 1"] interval: 30s timeout: 10s retries: 3 start_period: 10s jfr-sidecar: image: ${JFR_SIDECAR_IMAGE:-registry.digitalocean.com/eagle0/jfr-sidecar:latest} container_name: jfr-sidecar # Share PID namespace with Eagle to access its JVM via jcmd # For blue-green: use JFR_SIDECAR_ADDR=jfr-sidecar-green:8081 when green is active pid: "service:eagle-blue" volumes: - jvm-tmp:/tmp # Shared with Eagle for JVM attach socket files depends_on: - eagle-blue restart: unless-stopped logging: driver: "json-file" options: max-size: "10m" max-file: "2" healthcheck: test: ["CMD-SHELL", "wget -q --spider http://localhost:8081/health || exit 1"] interval: 30s timeout: 10s retries: 3 start_period: 10s jfr-sidecar-green: image: ${JFR_SIDECAR_IMAGE:-registry.digitalocean.com/eagle0/jfr-sidecar:latest} container_name: jfr-sidecar-green profiles: ["blue-green"] # Only started during blue-green deployment # Share PID namespace with Eagle green instance pid: "service:eagle-green" volumes: - jvm-tmp:/tmp # Shared with Eagle for JVM attach socket files depends_on: - eagle-green restart: "no" # Don't auto-restart during deployment logging: driver: "json-file" options: max-size: "10m" max-file: "2" healthcheck: test: ["CMD-SHELL", "wget -q --spider http://localhost:8081/health || exit 1"] interval: 30s timeout: 10s retries: 3 start_period: 10s certbot: image: certbot/certbot container_name: certbot volumes: - ./certbot/conf:/etc/letsencrypt - ./certbot/www:/var/www/certbot entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew; sleep 12h & wait $${!}; done;'" volumes: jvm-tmp: # Shared /tmp for JVM attach socket files between Eagle and jfr-sidecar jwt-keys: # Shared JWT RSA keys between Eagle and auth service networks: default: driver: bridge enable_ipv6: true ipam: config: - subnet: 172.28.0.0/16 - subnet: fd00:dead:beef::/48