Files
eagle0/docker-compose.prod.yml
T
45c6b22ef8 Add client update notification system (server-side) (#5820)
Notify connected clients when a new version is available after CI deploy.

Flow:
1. CI deploys new build and waits 60s for CDN cache
2. CI calls admin server HTTP endpoint with shared secret
3. Admin server calls Eagle via gRPC
4. Eagle broadcasts to all connected lobby users

Components:
- Proto: ClientUpdateAvailable message, NotifyClientUpdate RPC
- Eagle: notifyClientUpdate() broadcasts to lobby users
- Admin server: /notify-update HTTP endpoint with secret auth
- CI: Notify steps in mac_build.yml and unity_build.yml
- Docker: NOTIFY_SECRET env var passed to admin container

Client-side handling will be added in a follow-up PR.

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-03 20:57:37 -08:00

300 lines
11 KiB
YAML

# 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:-}"
# 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:-}"
# 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
- "--http-port"
- "8080"
environment:
# Secret for CI to authenticate client update notifications
NOTIFY_SECRET: "${NOTIFY_SECRET:-}"
# 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