311 lines
11 KiB
YAML
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:-change_me}
|
|
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:-change_me_redis}
|
|
volumes:
|
|
- redis-data:/data
|
|
networks:
|
|
- wc2026-net
|
|
healthcheck:
|
|
test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD:-change_me_redis}", "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:-change_me}@fifa2026-postgres:5432/fifa2026
|
|
- REDIS_URL=redis://:${REDIS_PASSWORD:-change_me_redis}@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:-change_me}@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:-change_me}@fifa2026-postgres:5432/fifa2026
|
|
- REDIS_URL=redis://:${REDIS_PASSWORD:-change_me_redis}@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:-change_me_redis}@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:-change_me}@fifa2026-postgres:5432/fifa2026
|
|
- REDIS_URL=redis://:${REDIS_PASSWORD:-change_me_redis}@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:-change_me}@fifa2026-postgres:5432/fifa2026
|
|
- REDIS_URL=redis://:${REDIS_PASSWORD:-change_me_redis}@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:-change_me}@fifa2026-postgres:5432/fifa2026
|
|
- REDIS_URL=redis://:${REDIS_PASSWORD:-change_me_redis}@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:-replace-me-in-production}
|
|
- DATABASE_URL=postgresql://fifa_user:${DB_PASSWORD:-change_me}@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:-change_me_redis}@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:
|