Files
rf4-spotter/compose.production.yaml
T
ik e2bed0db89 R13: Fix X-Forwarded-For trust boundary — only trust from known proxies
- Add _is_trusted_proxy() to check client IP against trusted CIDRs
- Only use X-Forwarded-For if connection came from trusted proxy
- Add TRUSTED_PROXY_CIDRS config (default: 127.0.0.1/32, ::1/128)
- Add parse_comma_separated_lists for env var parsing
- Add 3 unit tests: trusted CIDR check, untrusted ignores forwarded, trusted uses forwarded
2026-09-10 05:49:23 +07:00

226 lines
7.8 KiB
YAML

name: rf4-spotter
x-logging: &default-logging
driver: json-file
options:
max-size: "10m"
max-file: "5"
services:
proxy:
image: caddy:2.10.2-alpine
restart: unless-stopped
environment:
SITE_DOMAIN: ${SITE_DOMAIN:?Set SITE_DOMAIN}
FILES_DOMAIN: ${FILES_DOMAIN:?Set FILES_DOMAIN}
ACME_EMAIL: ${ACME_EMAIL:?Set ACME_EMAIL}
ADMIN_BASIC_USER: ${ADMIN_BASIC_USER:?Set ADMIN_BASIC_USER}
ADMIN_BASIC_PASSWORD_HASH: ${ADMIN_BASIC_PASSWORD_HASH:?Set ADMIN_BASIC_PASSWORD_HASH}
ports:
- "80:80"
- "443:443"
- "443:443/udp"
volumes:
- ./deploy/Caddyfile:/etc/caddy/Caddyfile:ro
- caddy_data:/data
- caddy_config:/config
depends_on:
web:
condition: service_healthy
networks: [edge, backend]
security_opt: [no-new-privileges:true]
logging: *default-logging
deploy:
resources:
limits: {cpus: "0.50", memory: 256M}
db:
image: postgres:17-alpine
restart: unless-stopped
environment:
POSTGRES_DB: ${POSTGRES_DB:-rf4_spotter}
POSTGRES_USER: ${POSTGRES_USER:-rf4}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?Set POSTGRES_PASSWORD}
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-rf4} -d ${POSTGRES_DB:-rf4_spotter}"]
interval: 10s
timeout: 5s
retries: 10
networks: [backend]
security_opt: [no-new-privileges:true]
logging: *default-logging
deploy:
resources:
limits: {cpus: "1.00", memory: 1G}
minio:
image: minio/minio:RELEASE.2025-07-23T15-54-02Z
command: server /data
restart: unless-stopped
environment:
MINIO_ROOT_USER: ${MINIO_ROOT_USER:?Set MINIO_ROOT_USER}
MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD:?Set MINIO_ROOT_PASSWORD}
volumes:
- minio_data:/data
healthcheck:
test: ["CMD", "curl", "-fsS", "http://localhost:9000/minio/health/live"]
interval: 10s
timeout: 5s
retries: 10
networks: [backend]
security_opt: [no-new-privileges:true]
logging: *default-logging
deploy:
resources:
limits: {cpus: "0.75", memory: 1G}
minio-init:
image: minio/mc:RELEASE.2025-07-21T05-28-08Z
restart: "no"
entrypoint: ["/bin/sh", "-c"]
command:
- >-
test "$$MINIO_ROOT_USER" != "$$S3_ACCESS_KEY" &&
test "$$MINIO_ROOT_PASSWORD" != "$$S3_SECRET_KEY" &&
mc alias set local http://minio:9000 "$$MINIO_ROOT_USER" "$$MINIO_ROOT_PASSWORD" >/dev/null &&
(mc admin user info local "$$S3_ACCESS_KEY" >/dev/null 2>&1 || mc admin user add local "$$S3_ACCESS_KEY" "$$S3_SECRET_KEY") &&
mc admin policy attach local readwrite --user "$$S3_ACCESS_KEY" >/dev/null
environment:
MINIO_ROOT_USER: ${MINIO_ROOT_USER:?Set MINIO_ROOT_USER}
MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD:?Set MINIO_ROOT_PASSWORD}
S3_ACCESS_KEY: ${S3_ACCESS_KEY:?Set S3_ACCESS_KEY}
S3_SECRET_KEY: ${S3_SECRET_KEY:?Set S3_SECRET_KEY}
depends_on:
minio: {condition: service_healthy}
networks: [backend]
security_opt: [no-new-privileges:true]
logging: *default-logging
api:
build:
context: .
dockerfile: apps/api/Dockerfile
restart: unless-stopped
environment:
DEPLOYMENT_ENVIRONMENT: production
APP_VERSION: ${APP_VERSION:-0.1.0}
APP_REVISION: ${APP_REVISION:-unknown}
DATABASE_URL: ${DATABASE_URL:?Set DATABASE_URL}
ADMIN_TOKEN: ${ADMIN_TOKEN:?Set ADMIN_TOKEN}
CORS_ORIGINS: '["https://${SITE_DOMAIN:?Set SITE_DOMAIN}"]'
S3_ENDPOINT_URL: http://minio:9000
S3_PUBLIC_ENDPOINT_URL: https://${FILES_DOMAIN:?Set FILES_DOMAIN}
S3_ACCESS_KEY: ${S3_ACCESS_KEY:?Set S3_ACCESS_KEY}
S3_SECRET_KEY: ${S3_SECRET_KEY:?Set S3_SECRET_KEY}
S3_BUCKET: ${S3_BUCKET:-catch-screenshots}
OFFICIAL_RECORDS_URL: ${OFFICIAL_RECORDS_URL:-https://rf4game.de/records/region/RU/}
OFFICIAL_RECORDS_REGION: ${OFFICIAL_RECORDS_REGION:-RU}
OFFICIAL_RECORDS_CATEGORY: ${OFFICIAL_RECORDS_CATEGORY:-records}
OFFICIAL_IMPORT_REQUIRED: ${OFFICIAL_IMPORT_REQUIRED:-false}
SEED_DEMO_DATA: "false"
IMPORT_INTERVAL_SECONDS: ${IMPORT_INTERVAL_SECONDS:-3600}
PUBLIC_CACHE_SECONDS: ${PUBLIC_CACHE_SECONDS:-20}
RATE_LIMIT_SECRET: ${RATE_LIMIT_SECRET:?Set RATE_LIMIT_SECRET}
TRUSTED_PROXY_CIDRS: '${TRUSTED_PROXY_CIDRS:-"127.0.0.1/32,::1/128"}'
RETENTION_SUBMISSION_DAYS: ${RETENTION_SUBMISSION_DAYS:-1}
RETENTION_UNREVIEWED_DAYS: ${RETENTION_UNREVIEWED_DAYS:-30}
RETENTION_APPROVED_PERSONAL_DAYS: ${RETENTION_APPROVED_PERSONAL_DAYS:-180}
RETENTION_STAGING_DAYS: ${RETENTION_STAGING_DAYS:-90}
RETENTION_AUDIT_DAYS: ${RETENTION_AUDIT_DAYS:-365}
RETENTION_PUBLISHED_PAYLOAD_DAYS: ${RETENTION_PUBLISHED_PAYLOAD_DAYS:-365}
LOG_LEVEL: ${LOG_LEVEL:-INFO}
depends_on:
db: {condition: service_healthy}
minio: {condition: service_healthy}
minio-init: {condition: service_completed_successfully}
healthcheck:
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/ready')"]
interval: 10s
timeout: 5s
retries: 12
networks: [backend, edge]
security_opt: [no-new-privileges:true]
logging: *default-logging
deploy:
resources:
limits: {cpus: "1.00", memory: 1G}
web:
build:
context: ./apps/web
args:
PUBLIC_API_URL: https://${SITE_DOMAIN:?Set SITE_DOMAIN}
restart: unless-stopped
environment:
PUBLIC_API_URL: https://${SITE_DOMAIN:?Set SITE_DOMAIN}
API_INTERNAL_URL: http://api:8000
depends_on:
api: {condition: service_healthy}
healthcheck:
test: ["CMD", "node", "-e", "fetch('http://localhost:4321').then(r=>{if(!r.ok)process.exit(1)}).catch(()=>process.exit(1))"]
interval: 10s
timeout: 5s
retries: 10
networks: [backend]
security_opt: [no-new-privileges:true]
logging: *default-logging
deploy:
resources:
limits: {cpus: "0.75", memory: 512M}
community-scheduler:
build:
context: .
dockerfile: apps/api/Dockerfile
restart: unless-stopped
environment:
DEPLOYMENT_ENVIRONMENT: production
DATABASE_URL: ${DATABASE_URL:?Set DATABASE_URL}
ADMIN_TOKEN: ${ADMIN_TOKEN:?Set ADMIN_TOKEN}
CORS_ORIGINS: '["https://${SITE_DOMAIN:?Set SITE_DOMAIN}"]'
S3_PUBLIC_ENDPOINT_URL: https://${FILES_DOMAIN:?Set FILES_DOMAIN}
S3_ACCESS_KEY: ${S3_ACCESS_KEY:?Set S3_ACCESS_KEY}
S3_SECRET_KEY: ${S3_SECRET_KEY:?Set S3_SECRET_KEY}
SEED_DEMO_DATA: "false"
RATE_LIMIT_SECRET: ${RATE_LIMIT_SECRET:?Set RATE_LIMIT_SECRET}
TRUSTED_PROXY_CIDRS: '${TRUSTED_PROXY_CIDRS:-"127.0.0.1/32,::1/128"}'
COMMUNITY_IMPORT_INTERVAL_SECONDS: ${COMMUNITY_IMPORT_INTERVAL_SECONDS:-1800}
RF4MAP_POINT_URL: ${RF4MAP_POINT_URL:-https://rf4map.ru/points/275}
RF4POSTS_SPOT_URL: ${RF4POSTS_SPOT_URL:-https://rf4-posts.com/ru/spots/d0c6d9c6-4ebf-49a7-98a8-9a562553a8ee}
LOG_LEVEL: ${LOG_LEVEL:-INFO}
depends_on:
api: {condition: service_healthy}
command: ["python", "-m", "app.community_scheduler"]
networks: [backend, edge]
security_opt: [no-new-privileges:true]
logging: *default-logging
deploy:
resources:
limits: {cpus: "0.50", memory: 384M}
storage-tool:
image: minio/mc:RELEASE.2025-07-21T05-28-08Z
profiles: [tools]
environment:
S3_ACCESS_KEY: ${S3_ACCESS_KEY:?Set S3_ACCESS_KEY}
S3_SECRET_KEY: ${S3_SECRET_KEY:?Set S3_SECRET_KEY}
S3_BUCKET: ${S3_BUCKET:-catch-screenshots}
volumes:
- ${BACKUP_DIRECTORY:-./backups}:/backup
networks: [backend]
security_opt: [no-new-privileges:true]
logging: *default-logging
networks:
edge:
backend:
internal: true
volumes:
postgres_data:
minio_data:
caddy_data:
caddy_config: