MosswartOverlord/docker-compose.yml
Erik a28b61511c security: enforce real plugin secret, fix proxy auth bypass, loopback DB ports, nightly backups
- SHARED_SECRET now read from env and fail-closed: unset/placeholder refuses
  ALL plugin connections (constant-time compare). The old hardcoded
  'your_shared_secret' in this public repo was no auth at all. Dockerfile
  default removed; generate_data.py reads the env var.
- SECRET_KEY fails closed at startup (main.py and agent/auth.py) instead of
  falling back to a publicly-known signing key; agent systemd unit now
  requires /etc/overlord/agent.env (no '-' prefix).
- AuthMiddleware + /ws/live: replace the 172.x source-IP trust (which every
  nginx-proxied internet request satisfied via docker-proxy — full session
  bypass and unauthenticated in-game command injection) with
  private-source AND no X-Forwarded-For, i.e. only genuinely internal
  callers (overlord-agent on the host, compose-network services). Invariant
  documented in nginx/overlord.conf: every tracker-bound location must set
  X-Forwarded-For.
- /character-stats/test endpoints gated behind admin (they upsert real rows).
- docker-compose: bind 5432/5433 to 127.0.0.1 (both DBs were internet-
  reachable; active brute-force observed in dereth-db logs).
- discord-rare-monitor: drop dead SHARED_SECRET constant.
- scripts/backup-databases.sh + docs/backups.md: nightly pg_dump of both DBs
  (telemetry/spawn hypertable data excluded), 10MB canary, umask 077,
  TimescaleDB restore procedure.
- Remove stray mangled-path css file from repo root.

Adversarially reviewed pre-deploy (3-lens workflow): ship verdict; deploy-
sequencing blockers addressed (secret staged before enforcement, exec bit
set, cron uses bash).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 17:02:47 +02:00

182 lines
No EOL
5.9 KiB
YAML

## Docker Compose configuration for Dereth Tracker microservices
version: "3.8"
services:
# Application service: Dereth Tracker API and WebSockets server
dereth-tracker:
build: .
ports:
- "127.0.0.1:8765:8765"
depends_on:
- db
volumes:
- "./main.py:/app/main.py"
- "./db_async.py:/app/db_async.py"
- "./static:/app/static"
- "./icons:/app/icons"
- "./alembic:/app/alembic"
- "./alembic.ini:/app/alembic.ini"
environment:
# Database connection URL for TimescaleDB (built from POSTGRES_PASSWORD)
DATABASE_URL: "postgresql://postgres:${POSTGRES_PASSWORD}@db:5432/dereth"
# Override application settings as needed
DB_MAX_SIZE_MB: "${DB_MAX_SIZE_MB}"
DB_RETENTION_DAYS: "${DB_RETENTION_DAYS}"
DB_MAX_SQL_LENGTH: "${DB_MAX_SQL_LENGTH}"
DB_MAX_SQL_VARIABLES: "${DB_MAX_SQL_VARIABLES}"
DB_WAL_AUTOCHECKPOINT_PAGES: "${DB_WAL_AUTOCHECKPOINT_PAGES}"
SHARED_SECRET: "${SHARED_SECRET}"
SECRET_KEY: "${SECRET_KEY}"
INVENTORY_SERVICE_URL: "http://inventory-service:8000"
DISCORD_ACLOG_WEBHOOK: "${DISCORD_ACLOG_WEBHOOK:-}"
LOG_LEVEL: "INFO"
restart: unless-stopped
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
# TimescaleDB service for telemetry data storage
db:
image: timescale/timescaledb:2.19.3-pg14
container_name: dereth-db
# Override PostgreSQL memory settings. The default timescaledb-tune values
# targeted a much larger machine — shared_buffers was set to 96GB on a
# 32GB host, causing the kernel to swap-thrash and leaving <100MB free.
# These values follow the standard recommendation: shared_buffers ~25% RAM,
# effective_cache_size ~50% RAM, work_mem modest to avoid multiplication
# blow-up across the ~20-connection pool.
command: >
postgres
-c shared_buffers=8GB
-c effective_cache_size=16GB
-c work_mem=16MB
-c maintenance_work_mem=1GB
-c max_wal_size=4GB
environment:
POSTGRES_DB: dereth
POSTGRES_USER: postgres
POSTGRES_PASSWORD: "${POSTGRES_PASSWORD}"
DB_RETENTION_DAYS: 30
volumes:
- timescale-data:/var/lib/postgresql/data
ports:
# Loopback only — Docker-published ports bypass ufw, and this host is
# internet-facing (active brute-force on the open port observed June
# 2026). In-stack consumers use the compose network; host-side tools
# (psql, overlord-agent) use 127.0.0.1.
- "127.0.0.1:5432:5432"
restart: unless-stopped
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 30s
timeout: 5s
retries: 5
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
# Inventory Service: Separate microservice for item data processing
inventory-service:
build: ./inventory-service
ports:
- "127.0.0.1:8766:8000"
depends_on:
- inventory-db
volumes:
- "./inventory-service:/app"
environment:
DATABASE_URL: "postgresql://inventory_user:${INVENTORY_DB_PASSWORD}@inventory-db:5432/inventory_db"
LOG_LEVEL: "INFO"
restart: unless-stopped
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
# Separate database for inventory service
inventory-db:
image: postgres:14
container_name: inventory-db
environment:
POSTGRES_DB: inventory_db
POSTGRES_USER: inventory_user
POSTGRES_PASSWORD: "${INVENTORY_DB_PASSWORD}"
volumes:
- inventory-data:/var/lib/postgresql/data
ports:
# Loopback only — see db service note.
- "127.0.0.1:5433:5432"
restart: unless-stopped
healthcheck:
test: ["CMD-SHELL", "pg_isready -U inventory_user"]
interval: 30s
timeout: 5s
retries: 5
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
# Discord Rare Monitor Bot: Monitors rare discoveries and posts to Discord
discord-rare-monitor:
build: ./discord-rare-monitor
depends_on:
- dereth-tracker
volumes:
- "./discord-rare-monitor:/app"
environment:
DISCORD_RARE_BOT_TOKEN: "${DISCORD_RARE_BOT_TOKEN}"
DERETH_TRACKER_WS_URL: "ws://dereth-tracker:8765/ws/live"
COMMON_RARE_CHANNEL_ID: "${COMMON_RARE_CHANNEL_ID:-1355328792184226014}"
GREAT_RARE_CHANNEL_ID: "${GREAT_RARE_CHANNEL_ID:-1353676584334131211}"
ACLOG_CHANNEL_ID: "${ACLOG_CHANNEL_ID:-1349649482786275328}"
MONITOR_CHARACTER: "${MONITOR_CHARACTER:-Dunking Rares}"
LOG_LEVEL: "INFO"
restart: unless-stopped
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
# Grafana service for visualization and dashboards
grafana:
image: grafana/grafana:latest
container_name: dereth-grafana
ports:
- "127.0.0.1:3000:3000"
depends_on:
- db
environment:
# Grafana admin settings
GF_SECURITY_ADMIN_PASSWORD: "${GF_SECURITY_ADMIN_PASSWORD}"
# Allow embedding Grafana dashboards in iframes
GF_SECURITY_ALLOW_EMBEDDING: "true"
# Enable anonymous access so embedded panels work without login
GF_AUTH_ANONYMOUS_ENABLED: "true"
GF_AUTH_ANONYMOUS_ORG_ROLE: "Viewer"
GF_USERS_ALLOW_SIGN_UP: "false"
# Serve Grafana under /grafana path
GF_SERVER_ROOT_URL: "https://overlord.snakedesert.se/grafana"
GF_SERVER_SERVE_FROM_SUB_PATH: "true"
# Postgres password for provisioning datasource
POSTGRES_PASSWORD: "${POSTGRES_PASSWORD}"
volumes:
# Provisioning directories for automated data source and dashboards
- ./grafana/provisioning:/etc/grafana/provisioning
- ./grafana/dashboards:/var/lib/grafana/dashboards
restart: unless-stopped
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
volumes:
timescale-data:
inventory-data: