Files
2026FIFAWorldCup/docker-compose.prod.yml
QuantBot 076b5c1bc3
Some checks failed
2026 World Cup Quant Platform - Production Deployment / Code Quality, Security Gate & Testing (push) Failing after 2m24s
2026 World Cup Quant Platform - Production Deployment / Deploy to Production VM via Gitea CD (push) Has been skipped
security: gate fifa2026 production deploys
2026-06-18 11:26:20 +08:00

311 lines
11 KiB
YAML

version: '3.8'
# ── Security anchor template ────────────────────────────────────────────────────
# Applied to every service via <<: *security-defaults
x-security-defaults: &security-defaults
security_opt:
- no-new-privileges:true # Prevent privilege escalation
cap_drop:
- ALL # Drop all Linux capabilities by default
read_only: false # Override per-service if needed
tmpfs:
- /tmp:size=64m,noexec,nosuid,nodev
x-backend-security: &backend-security
security_opt:
- no-new-privileges:true
cap_drop:
- ALL
services:
fifa2026-postgres:
image: timescale/timescaledb:latest-pg16
restart: always
<<: *backend-security
cap_add:
- CHOWN
- SETUID
- SETGID
- DAC_OVERRIDE # postgres needs these minimal caps
environment:
POSTGRES_USER: fifa_user
POSTGRES_PASSWORD: ${DB_PASSWORD:?DB_PASSWORD is required}
POSTGRES_DB: fifa2026
volumes:
- pg-data:/var/lib/postgresql/data
- ./platform/backend/db_init_timescaledb.sql:/docker-entrypoint-initdb.d/init.sql:ro
networks:
- wc2026-net
healthcheck:
test: ["CMD-SHELL", "pg_isready -U fifa_user -d fifa2026"]
interval: 10s
timeout: 5s
retries: 5
fifa2026-redis:
image: redis:7-alpine
restart: always
<<: *backend-security
cap_add:
- SETUID
- SETGID
- CHOWN
- DAC_OVERRIDE
command: >
redis-server
--appendonly yes
--protected-mode yes
--bind 0.0.0.0
--requirepass ${REDIS_PASSWORD:?REDIS_PASSWORD is required}
volumes:
- redis-data:/data
networks:
- wc2026-net
healthcheck:
test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD:?REDIS_PASSWORD is required}", "ping"]
interval: 10s
timeout: 5s
retries: 5
fifa2026-backend:
build:
context: ./platform/backend
restart: always
<<: *security-defaults
ports:
- "127.0.0.1:8000:8000"
extra_hosts:
- "host.docker.internal:host-gateway"
environment:
- DATABASE_URL=postgresql+asyncpg://fifa_user:${DB_PASSWORD:?DB_PASSWORD is required}@fifa2026-postgres:5432/fifa2026
- REDIS_URL=redis://:${REDIS_PASSWORD:?REDIS_PASSWORD is required}@fifa2026-redis:6379/0
- THE_ODDS_API_KEY=${THE_ODDS_API_KEY:-}
- THE_ODDS_BASE=${THE_ODDS_BASE:-https://api.the-odds-api.com}
- THE_ODDS_SPORT_KEY=${THE_ODDS_SPORT_KEY:-soccer_fifa_world_cup}
- THE_ODDS_REGIONS=${THE_ODDS_REGIONS:-eu}
- THE_ODDS_MARKETS=${THE_ODDS_MARKETS:-h2h,spreads,totals,btts,draw_no_bet,h2h_3_way,alternate_spreads,alternate_totals,team_totals,alternate_team_totals}
- THE_ODDS_ADDITIONAL_EVENTS_LIMIT=${THE_ODDS_ADDITIONAL_EVENTS_LIMIT:-24}
- GEMINI_API_KEY=${GEMINI_API_KEY:-}
- GEMINI_MODEL=${GEMINI_MODEL:-gemini-3-flash-preview}
- GEMINI_REVIEW_MODEL=${GEMINI_REVIEW_MODEL:-gemini-2.5-flash}
- GEMINI_COST_CAP_USD=${GEMINI_COST_CAP_USD:-5}
- GEMINI_INPUT_PRICE_PER_1M_USD=${GEMINI_INPUT_PRICE_PER_1M_USD:-0.50}
- GEMINI_OUTPUT_PRICE_PER_1M_USD=${GEMINI_OUTPUT_PRICE_PER_1M_USD:-3.00}
- GEMINI_GROUNDING_PRICE_PER_1K_USD=${GEMINI_GROUNDING_PRICE_PER_1K_USD:-14.00}
- GEMINI_FALLBACK_REQUEST_COST_USD=${GEMINI_FALLBACK_REQUEST_COST_USD:-0.01}
- NEMOTRON_API_BASE=${NEMOTRON_API_BASE:-}
- NEMOTRON_MODEL=${NEMOTRON_MODEL:-nvidia/nemotron}
- OLLAMA_BASE_URL=${OLLAMA_BASE_URL:-}
- OLLAMA_NEMOTRON_MODEL=${OLLAMA_NEMOTRON_MODEL:-nemotron}
- APP_TIME_ZONE=Asia/Taipei
- TZ=Asia/Taipei
depends_on:
fifa2026-seed:
condition: service_completed_successfully
fifa2026-postgres:
condition: service_healthy
fifa2026-redis:
condition: service_healthy
networks:
- wc2026-net
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
interval: 30s
timeout: 10s
retries: 3
fifa2026-seed:
build:
context: ./platform/backend
restart: "no"
<<: *backend-security
command: ["python", "-m", "app.analytics.worldcup_seed"]
environment:
- DATABASE_URL=postgresql+asyncpg://fifa_user:${DB_PASSWORD:?DB_PASSWORD is required}@fifa2026-postgres:5432/fifa2026
- TZ=Asia/Taipei
depends_on:
fifa2026-postgres:
condition: service_healthy
networks:
- wc2026-net
fifa2026-odds-worker:
build:
context: ./platform/backend
restart: always
<<: *security-defaults
command: ["python", "-m", "app.analytics.crawler"]
environment:
- DATABASE_URL=postgresql+asyncpg://fifa_user:${DB_PASSWORD:?DB_PASSWORD is required}@fifa2026-postgres:5432/fifa2026
- REDIS_URL=redis://:${REDIS_PASSWORD:?REDIS_PASSWORD is required}@fifa2026-redis:6379/0
- THE_ODDS_API_KEY=${THE_ODDS_API_KEY:-}
- THE_ODDS_BASE=${THE_ODDS_BASE:-https://api.the-odds-api.com}
- THE_ODDS_SPORT_KEY=${THE_ODDS_SPORT_KEY:-soccer_fifa_world_cup}
- THE_ODDS_REGIONS=${THE_ODDS_REGIONS:-eu}
- THE_ODDS_MARKETS=${THE_ODDS_MARKETS:-h2h,spreads,totals,btts,draw_no_bet,h2h_3_way,alternate_spreads,alternate_totals,team_totals,alternate_team_totals}
- THE_ODDS_ADDITIONAL_EVENTS_LIMIT=${THE_ODDS_ADDITIONAL_EVENTS_LIMIT:-24}
- ODDS_POLL_INTERVAL_SECONDS=${ODDS_POLL_INTERVAL_SECONDS:-300}
- ESPN_SCOREBOARD_LOOKBACK_DAYS=${ESPN_SCOREBOARD_LOOKBACK_DAYS:-5}
- ESPN_SCOREBOARD_LOOKAHEAD_DAYS=${ESPN_SCOREBOARD_LOOKAHEAD_DAYS:-2}
- APP_TIME_ZONE=Asia/Taipei
- TZ=Asia/Taipei
depends_on:
fifa2026-seed:
condition: service_completed_successfully
fifa2026-postgres:
condition: service_healthy
fifa2026-redis:
condition: service_healthy
networks:
- wc2026-net
fifa2026-news-worker:
build:
context: ./platform/backend
restart: always
<<: *security-defaults
command: ["python", "-m", "app.analytics.news_worker"]
environment:
- REDIS_URL=redis://:${REDIS_PASSWORD:?REDIS_PASSWORD is required}@fifa2026-redis:6379/0
- NEWS_POLL_INTERVAL_SECONDS=${NEWS_POLL_INTERVAL_SECONDS:-900}
- NEWS_QUERY=${NEWS_QUERY:-2026 FIFA World Cup OR 2026 世界盃 OR 世界盃 2026}
- GEMINI_API_KEY=${GEMINI_API_KEY:-}
- GEMINI_MODEL=${GEMINI_MODEL:-gemini-3-flash-preview}
- GEMINI_COST_CAP_USD=${GEMINI_COST_CAP_USD:-5}
- TZ=Asia/Taipei
depends_on:
fifa2026-redis:
condition: service_healthy
networks:
- wc2026-net
fifa2026-agent-review-worker:
build:
context: ./platform/backend
restart: always
<<: *security-defaults
command: ["python", "-m", "app.analytics.agent_review_worker"]
extra_hosts:
- "host.docker.internal:host-gateway"
environment:
- DATABASE_URL=postgresql+asyncpg://fifa_user:${DB_PASSWORD:?DB_PASSWORD is required}@fifa2026-postgres:5432/fifa2026
- REDIS_URL=redis://:${REDIS_PASSWORD:?REDIS_PASSWORD is required}@fifa2026-redis:6379/0
- GEMINI_API_KEY=${GEMINI_API_KEY:-}
- GEMINI_MODEL=${GEMINI_MODEL:-gemini-3-flash-preview}
- GEMINI_REVIEW_MODEL=${GEMINI_REVIEW_MODEL:-gemini-2.5-flash}
- GEMINI_COST_CAP_USD=${GEMINI_COST_CAP_USD:-5}
- GEMINI_INPUT_PRICE_PER_1M_USD=${GEMINI_INPUT_PRICE_PER_1M_USD:-0.50}
- GEMINI_OUTPUT_PRICE_PER_1M_USD=${GEMINI_OUTPUT_PRICE_PER_1M_USD:-3.00}
- GEMINI_GROUNDING_PRICE_PER_1K_USD=${GEMINI_GROUNDING_PRICE_PER_1K_USD:-14.00}
- GEMINI_REVIEW_TIMEOUT_SECONDS=${GEMINI_REVIEW_TIMEOUT_SECONDS:-18}
- NEMOTRON_API_BASE=${NEMOTRON_API_BASE:-}
- NEMOTRON_MODEL=${NEMOTRON_MODEL:-nvidia/nemotron}
- OLLAMA_BASE_URL=${OLLAMA_BASE_URL:-}
- OLLAMA_NEMOTRON_MODEL=${OLLAMA_NEMOTRON_MODEL:-nemotron}
- NEMOTRON_REVIEW_TIMEOUT_SECONDS=${AGENT_REVIEW_MODEL_TIMEOUT_SECONDS:-45}
- NEMOTRON_REVIEW_NUM_PREDICT=${AGENT_REVIEW_NUM_PREDICT:-120}
- AGENT_REVIEW_POLL_INTERVAL_SECONDS=${AGENT_REVIEW_POLL_INTERVAL_SECONDS:-1800}
- AGENT_REVIEW_LOOKAHEAD_DAYS=${AGENT_REVIEW_LOOKAHEAD_DAYS:-2}
- AGENT_REVIEW_CACHE_TTL_SECONDS=${AGENT_REVIEW_CACHE_TTL_SECONDS:-259200}
- APP_TIME_ZONE=Asia/Taipei
- TZ=Asia/Taipei
depends_on:
fifa2026-seed:
condition: service_completed_successfully
fifa2026-postgres:
condition: service_healthy
fifa2026-redis:
condition: service_healthy
networks:
- wc2026-net
fifa2026-calendar-cache-worker:
build:
context: ./platform/backend
restart: always
<<: *security-defaults
command: ["python", "-m", "app.analytics.daily_card_calendar_worker"]
environment:
- DATABASE_URL=postgresql+asyncpg://fifa_user:${DB_PASSWORD:?DB_PASSWORD is required}@fifa2026-postgres:5432/fifa2026
- REDIS_URL=redis://:${REDIS_PASSWORD:?REDIS_PASSWORD is required}@fifa2026-redis:6379/0
- DAILY_CARD_CALENDAR_POLL_INTERVAL_SECONDS=${DAILY_CARD_CALENDAR_POLL_INTERVAL_SECONDS:-300}
- DAILY_CARD_CALENDAR_CACHE_TTL_SECONDS=${DAILY_CARD_CALENDAR_CACHE_TTL_SECONDS:-420}
- DAILY_CARD_CALENDAR_START_DATE=${DAILY_CARD_CALENDAR_START_DATE:-2026-06-11}
- APP_TIME_ZONE=Asia/Taipei
- TZ=Asia/Taipei
depends_on:
fifa2026-seed:
condition: service_completed_successfully
fifa2026-postgres:
condition: service_healthy
fifa2026-redis:
condition: service_healthy
networks:
- wc2026-net
fifa2026-fixtures-worker:
build:
context: ./platform/backend
restart: always
<<: *security-defaults
command: ["python", "-m", "app.analytics.fixtures_worker"]
environment:
- DATABASE_URL=postgresql+asyncpg://fifa_user:${DB_PASSWORD:?DB_PASSWORD is required}@fifa2026-postgres:5432/fifa2026
- REDIS_URL=redis://:${REDIS_PASSWORD:?REDIS_PASSWORD is required}@fifa2026-redis:6379/0
- FIXTURES_JSON_URL=${FIXTURES_JSON_URL:-https://www.thestatsapi.com/world-cup/data/fixtures.json}
- FIXTURES_POLL_INTERVAL_SECONDS=${FIXTURES_POLL_INTERVAL_SECONDS:-21600}
- TZ=Asia/Taipei
depends_on:
fifa2026-seed:
condition: service_completed_successfully
fifa2026-postgres:
condition: service_healthy
fifa2026-redis:
condition: service_healthy
networks:
- wc2026-net
fifa2026-web:
build:
context: ./platform/web
restart: always
<<: *security-defaults
ports:
- "127.0.0.1:3108:3000"
environment:
- NEXT_PUBLIC_API_URL=https://2026fifa.wooo.work/api
- NEXT_PUBLIC_WS_URL=wss://2026fifa.wooo.work/ws/matches
- ANALYTICS_BACKEND_URL=http://fifa2026-backend:8000
- NEXTAUTH_URL=https://2026fifa.wooo.work
- NEXTAUTH_SECRET=${NEXTAUTH_SECRET:?NEXTAUTH_SECRET is required}
- DATABASE_URL=postgresql://fifa_user:${DB_PASSWORD:?DB_PASSWORD is required}@fifa2026-postgres:5432/fifa2026
- TZ=Asia/Taipei
depends_on:
fifa2026-backend:
condition: service_healthy
networks:
- wc2026-net
fifa2026-alerts:
build:
context: ./platform/alerts
restart: always
<<: *security-defaults
environment:
- REDIS_URL=redis://:${REDIS_PASSWORD:?REDIS_PASSWORD is required}@fifa2026-redis:6379/0
- TELEGRAM_BOT_TOKEN=${TELEGRAM_BOT_TOKEN:-}
- TELEGRAM_CHAT_ID=${TELEGRAM_CHAT_ID:-}
depends_on:
fifa2026-redis:
condition: service_healthy
networks:
- wc2026-net
networks:
wc2026-net:
driver: bridge
volumes:
pg-data:
redis-data: