# Produzione — Match Live TV # Uso: docker compose -f docker-compose.prod.yml --env-file .env up -d --build services: postgres: image: postgres:16-alpine restart: unless-stopped environment: POSTGRES_DB: matchlivetv POSTGRES_USER: postgres POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} volumes: - postgres_data:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U postgres"] interval: 5s timeout: 5s retries: 5 redis: image: redis:7-alpine restart: unless-stopped volumes: - redis_data:/data healthcheck: test: ["CMD", "redis-cli", "ping"] interval: 5s timeout: 3s retries: 5 mediamtx: image: bluenviron/mediamtx:latest restart: unless-stopped ports: - "1935:1935" # RTMP ingest (telefoni) — esporre su router - "8888:8888" # HLS playback (opzionale) volumes: - ./mediamtx.yml:/mediamtx.yml:ro - ./slates:/slates:ro - ${MATCHLIVETV_VIDEOS_ROOT:-/media/videos/matchlivetv}/recordings:/recordings command: /mediamtx.yml # Immagine distroless: niente shell/wget/curl; API :9997 non esposta sull'host healthcheck: test: ["CMD", "/mediamtx", "--help"] interval: 15s timeout: 5s retries: 3 start_period: 10s rails: build: context: ../backend dockerfile: Dockerfile restart: unless-stopped command: bundle exec rails server -b 0.0.0.0 -p 3000 -e production environment: RAILS_ENV: production RAILS_MAX_THREADS: ${RAILS_MAX_THREADS:-5} DATABASE_URL: postgresql://postgres:${POSTGRES_PASSWORD}@postgres:5432/matchlivetv REDIS_URL: redis://redis:6379/0 MEDIAMTX_API_URL: http://mediamtx:9997 MEDIAMTX_RTMP_URL: ${MEDIAMTX_RTMP_URL:-rtmp://mediamtx:1935} MEDIAMTX_WEBHOOK_SECRET: ${MEDIAMTX_WEBHOOK_SECRET} RAILS_WEBHOOK_URL: http://rails:3000 SECRET_KEY_BASE: ${SECRET_KEY_BASE} JWT_SECRET: ${JWT_SECRET} CORS_ORIGINS: ${CORS_ORIGINS:-*} ALLOWED_HOSTS: ${ALLOWED_HOSTS:-*} YOUTUBE_CLIENT_ID: ${YOUTUBE_CLIENT_ID:-} YOUTUBE_CLIENT_SECRET: ${YOUTUBE_CLIENT_SECRET:-} YOUTUBE_REDIRECT_URI: ${YOUTUBE_REDIRECT_URI:-} YOUTUBE_PLATFORM_REFRESH_TOKEN: ${YOUTUBE_PLATFORM_REFRESH_TOKEN:-} HLS_PUBLIC_URL: ${HLS_PUBLIC_URL:-http://localhost:8888} APP_PUBLIC_URL: ${APP_PUBLIC_URL:-http://localhost:3000} PRIVACY_CONTACT_EMAIL: ${PRIVACY_CONTACT_EMAIL:-privacy@matchlive.it} PRIVACY_CONTROLLER_NAME: ${PRIVACY_CONTROLLER_NAME:-Emiliano Frascaro} PRIVACY_CONTROLLER_ADDRESS: ${PRIVACY_CONTROLLER_ADDRESS:-Via Guido De Ruggiero, 89 - 20142 - Milano (MI)} PRIVACY_CONTROLLER_VAT: ${PRIVACY_CONTROLLER_VAT:-} MAILER_FROM: ${MAILER_FROM:-Match Live TV } SMTP_ADDRESS: ${SMTP_ADDRESS:-} SMTP_PORT: ${SMTP_PORT:-465} SMTP_USERNAME: ${SMTP_USERNAME:-} SMTP_PASSWORD: ${SMTP_PASSWORD:-} SMTP_AUTH: ${SMTP_AUTH:-plain} SMTP_SSL: ${SMTP_SSL:-true} SMTP_STARTTLS: ${SMTP_STARTTLS:-false} RAILS_LOG_TO_STDOUT: "true" RAILS_LOG_LEVEL: ${RAILS_LOG_LEVEL:-info} STRIPE_SECRET_KEY: ${STRIPE_SECRET_KEY:-} STRIPE_WEBHOOK_SECRET: ${STRIPE_WEBHOOK_SECRET:-} STRIPE_PREMIUM_LIGHT_MONTHLY_PRICE_ID: ${STRIPE_PREMIUM_LIGHT_MONTHLY_PRICE_ID:-} STRIPE_PREMIUM_LIGHT_YEARLY_PRICE_ID: ${STRIPE_PREMIUM_LIGHT_YEARLY_PRICE_ID:-${STRIPE_PREMIUM_LIGHT_PRICE_ID:-}} STRIPE_PREMIUM_FULL_MONTHLY_PRICE_ID: ${STRIPE_PREMIUM_FULL_MONTHLY_PRICE_ID:-} STRIPE_PREMIUM_FULL_YEARLY_PRICE_ID: ${STRIPE_PREMIUM_FULL_YEARLY_PRICE_ID:-${STRIPE_PREMIUM_FULL_PRICE_ID:-}} STRIPE_PREMIUM_LIGHT_PRICE_ID: ${STRIPE_PREMIUM_LIGHT_PRICE_ID:-} STRIPE_PREMIUM_FULL_PRICE_ID: ${STRIPE_PREMIUM_FULL_PRICE_ID:-} RECORDINGS_PATH: /recordings REPLAY_STORAGE_ENDPOINT: ${REPLAY_STORAGE_ENDPOINT:-} REPLAY_STORAGE_BUCKET: ${REPLAY_STORAGE_BUCKET:-matchlivetv-replays} REPLAY_STORAGE_REGION: ${REPLAY_STORAGE_REGION:-garage} REPLAY_STORAGE_ACCESS_KEY_ID: ${REPLAY_STORAGE_ACCESS_KEY_ID:-} REPLAY_STORAGE_SECRET_ACCESS_KEY: ${REPLAY_STORAGE_SECRET_ACCESS_KEY:-} REPLAY_STORAGE_FORCE_PATH_STYLE: ${REPLAY_STORAGE_FORCE_PATH_STYLE:-true} GOOGLE_ANALYTICS_MEASUREMENT_ID: ${GOOGLE_ANALYTICS_MEASUREMENT_ID:-} OPS_NTFY_URL: ${OPS_NTFY_URL:-} OPS_NTFY_TOKEN: ${OPS_NTFY_TOKEN:-} OPS_ALERT_EMAIL: ${OPS_ALERT_EMAIL:-} OPS_NOTIFY_SEVERITIES: ${OPS_NOTIFY_SEVERITIES:-critical,warning} OPS_NOTIFY_COOLDOWN_MINUTES: ${OPS_NOTIFY_COOLDOWN_MINUTES:-30} OPS_HEALTH_INTERVAL_SECS: ${OPS_HEALTH_INTERVAL_SECS:-180} OPS_HEALTH_TOKEN: ${OPS_HEALTH_TOKEN:-} OPS_DISK_WARN_PERCENT: ${OPS_DISK_WARN_PERCENT:-80} OPS_DISK_CRIT_PERCENT: ${OPS_DISK_CRIT_PERCENT:-90} OPS_RECORDINGS_WARN_GB: ${OPS_RECORDINGS_WARN_GB:-20} OPS_RECORDINGS_CRIT_GB: ${OPS_RECORDINGS_CRIT_GB:-50} OPS_SIDEKIQ_STALE_SECS: ${OPS_SIDEKIQ_STALE_SECS:-300} OPS_LOG_SUBSCRIBER: ${OPS_LOG_SUBSCRIBER:-false} SENTRY_DSN: ${SENTRY_DSN:-} depends_on: postgres: condition: service_healthy redis: condition: service_healthy mediamtx: condition: service_started garage: condition: service_started volumes: - ${MATCHLIVETV_VIDEOS_ROOT:-/media/videos/matchlivetv}/recordings:/recordings - ${MATCHLIVETV_VIDEOS_ROOT:-/media/videos/matchlivetv}/active_storage:/app/storage - ${MATCHLIVETV_VIDEOS_ROOT:-/media/videos/matchlivetv}/log:/app/log healthcheck: test: ["CMD", "curl", "-f", "http://127.0.0.1:3000/up"] interval: 15s timeout: 5s retries: 10 start_period: 90s edge: image: nginx:alpine restart: unless-stopped ports: - "3000:80" # Solo LAN — NPM proxy HTTPS (pagina cortesia se Rails è in restart) volumes: - ./nginx-edge/nginx.conf:/etc/nginx/nginx.conf:ro - ./nginx-edge/maintenance.html:/usr/share/nginx/maintenance/index.html:ro healthcheck: test: ["CMD", "wget", "-q", "--spider", "http://127.0.0.1:80/edge-health"] interval: 15s timeout: 5s retries: 3 sidekiq: build: context: ../backend dockerfile: Dockerfile restart: unless-stopped command: bundle exec sidekiq -C config/sidekiq.yml -e production environment: RAILS_ENV: production RAILS_MAX_THREADS: ${RAILS_MAX_THREADS:-5} DATABASE_URL: postgresql://postgres:${POSTGRES_PASSWORD}@postgres:5432/matchlivetv REDIS_URL: redis://redis:6379/0 MEDIAMTX_API_URL: http://mediamtx:9997 MEDIAMTX_WEBHOOK_SECRET: ${MEDIAMTX_WEBHOOK_SECRET} RAILS_WEBHOOK_URL: http://rails:3000 SECRET_KEY_BASE: ${SECRET_KEY_BASE} JWT_SECRET: ${JWT_SECRET} YOUTUBE_CLIENT_ID: ${YOUTUBE_CLIENT_ID:-} YOUTUBE_CLIENT_SECRET: ${YOUTUBE_CLIENT_SECRET:-} YOUTUBE_REDIRECT_URI: ${YOUTUBE_REDIRECT_URI:-} YOUTUBE_PLATFORM_REFRESH_TOKEN: ${YOUTUBE_PLATFORM_REFRESH_TOKEN:-} YOUTUBE_RELAY_WORKER: "1" RAILS_INTERNAL_URL: http://rails:3000 MEDIAMTX_HLS_URL: http://mediamtx:8888 MEDIAMTX_INTERNAL_RTMP_URL: rtmp://mediamtx:1935 HLS_PUBLIC_URL: ${HLS_PUBLIC_URL:-http://localhost:8888} APP_PUBLIC_URL: ${APP_PUBLIC_URL:-http://localhost:3000} MAILER_FROM: ${MAILER_FROM:-Match Live TV } SMTP_ADDRESS: ${SMTP_ADDRESS:-} SMTP_PORT: ${SMTP_PORT:-465} SMTP_USERNAME: ${SMTP_USERNAME:-} SMTP_PASSWORD: ${SMTP_PASSWORD:-} SMTP_AUTH: ${SMTP_AUTH:-plain} SMTP_SSL: ${SMTP_SSL:-true} SMTP_STARTTLS: ${SMTP_STARTTLS:-false} RAILS_LOG_LEVEL: ${RAILS_LOG_LEVEL:-info} RECORDINGS_PATH: /recordings REPLAY_STORAGE_ENDPOINT: ${REPLAY_STORAGE_ENDPOINT:-} REPLAY_STORAGE_BUCKET: ${REPLAY_STORAGE_BUCKET:-matchlivetv-replays} REPLAY_STORAGE_REGION: ${REPLAY_STORAGE_REGION:-garage} REPLAY_STORAGE_ACCESS_KEY_ID: ${REPLAY_STORAGE_ACCESS_KEY_ID:-} REPLAY_STORAGE_SECRET_ACCESS_KEY: ${REPLAY_STORAGE_SECRET_ACCESS_KEY:-} REPLAY_STORAGE_FORCE_PATH_STYLE: ${REPLAY_STORAGE_FORCE_PATH_STYLE:-true} OPS_NTFY_URL: ${OPS_NTFY_URL:-} OPS_NTFY_TOKEN: ${OPS_NTFY_TOKEN:-} OPS_ALERT_EMAIL: ${OPS_ALERT_EMAIL:-} OPS_NOTIFY_SEVERITIES: ${OPS_NOTIFY_SEVERITIES:-critical,warning} OPS_NOTIFY_COOLDOWN_MINUTES: ${OPS_NOTIFY_COOLDOWN_MINUTES:-30} OPS_HEALTH_INTERVAL_SECS: ${OPS_HEALTH_INTERVAL_SECS:-180} OPS_HEALTH_TOKEN: ${OPS_HEALTH_TOKEN:-} OPS_DISK_WARN_PERCENT: ${OPS_DISK_WARN_PERCENT:-80} OPS_DISK_CRIT_PERCENT: ${OPS_DISK_CRIT_PERCENT:-90} OPS_RECORDINGS_WARN_GB: ${OPS_RECORDINGS_WARN_GB:-20} OPS_RECORDINGS_CRIT_GB: ${OPS_RECORDINGS_CRIT_GB:-50} OPS_SIDEKIQ_STALE_SECS: ${OPS_SIDEKIQ_STALE_SECS:-300} OPS_LOG_SUBSCRIBER: ${OPS_LOG_SUBSCRIBER:-false} SENTRY_DSN: ${SENTRY_DSN:-} depends_on: rails: condition: service_healthy garage: condition: service_started ntfy: condition: service_started volumes: - ${MATCHLIVETV_VIDEOS_ROOT:-/media/videos/matchlivetv}/recordings:/recordings - ${MATCHLIVETV_VIDEOS_ROOT:-/media/videos/matchlivetv}/active_storage:/app/storage - ${MATCHLIVETV_VIDEOS_ROOT:-/media/videos/matchlivetv}/log:/app/log garage: image: dxflrs/garage:v1.0.1 restart: unless-stopped # API S3 solo rete Docker (rails/sidekiq → http://garage:3900). Non pubblicare 3900 su WAN. volumes: - ${MATCHLIVETV_VIDEOS_ROOT:-/media/videos/matchlivetv}/garage/meta:/var/lib/garage/meta - ${MATCHLIVETV_VIDEOS_ROOT:-/media/videos/matchlivetv}/garage/data:/var/lib/garage/data - ./garage/garage.prod.toml:/etc/garage.toml:ro command: ["/garage", "-c", "/etc/garage.toml", "server"] ntfy: image: binwiederhier/ntfy:latest restart: unless-stopped # Solo LAN :8090 — esporre su HTTPS via NPM (es. ntfy.matchlivetv.it). Non aprire 8090 su WAN. command: serve environment: NTFY_BASE_URL: ${NTFY_PUBLIC_URL:-http://127.0.0.1:8090} volumes: - ntfy_data:/var/lib/ntfy - ./ntfy/server.yml:/etc/ntfy/server.yml:ro ports: - "8090:80" healthcheck: test: ["CMD", "wget", "-q", "--spider", "http://127.0.0.1:80/v1/health"] interval: 30s timeout: 5s retries: 3 volumes: postgres_data: redis_data: ntfy_data: