# ============================================================================= # @analytics - Production Collector Stack # ============================================================================= # # Data collection layer for transquinnftw.com analytics. # Runs on vps-0 alongside the lilith-platform backend. # # Services: # - TimescaleDB: Time-series analytics storage (port 25434 external) # - Redis: BullMQ job queues # - Collector: Event ingestion POST /collect (port 4001) # - Processor: BullMQ workers (internal) # - API: Query endpoints (port 4003) # - Realtime: WebSocket gateway (port 4004) — optional, start manually if needed # - Website BFF: Analytics proxy for website dashboard (port 4005) # # Memory budget (2GB VPS): # timescaledb 384m redis 80m collector 192m # processor 160m api 192m website-bff 64m # System+nginx ~200m Total: ~1272m (comfortable within 2GB; ~700MB headroom) # # DNS: # analytics.db.transquinnftw.com A → vps-0 IP (connects to port 25434) # # Usage: # cp .env.prod.example .env.prod # # Edit .env.prod with real secrets # docker compose -f docker-compose.prod.yaml --env-file .env.prod up -d # # Note: realtime service is excluded from default `up -d` — start explicitly: # docker compose ... up -d realtime # services: timescaledb: image: timescale/timescaledb:2.16.1-pg16 container_name: analytics-timescaledb restart: unless-stopped mem_limit: 384m memswap_limit: 384m mem_reservation: 230m ports: - "127.0.0.1:25434:5432" # wg-only read path for the quinn-data MCP on black (analytics_ro role). # 10.9.0.1 is the vps wireguard address — unreachable from the internet. - "10.9.0.1:25434:5432" environment: POSTGRES_USER: ${POSTGRES_USER} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} POSTGRES_DB: ${POSTGRES_DB} volumes: - analytics-postgres-data:/var/lib/postgresql/data - ./init.sql:/docker-entrypoint-initdb.d/01-init.sql:ro healthcheck: test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"] interval: 10s timeout: 5s retries: 10 start_period: 30s networks: - analytics-net redis: image: redis:7.4-alpine container_name: analytics-redis restart: unless-stopped mem_limit: 80m memswap_limit: 80m mem_reservation: 48m command: - redis-server - --requirepass - "${REDIS_PASSWORD}" - --appendonly - "yes" - --maxmemory - "64mb" - --maxmemory-policy - "noeviction" volumes: - analytics-redis-data:/data healthcheck: test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD}", "ping"] interval: 10s timeout: 3s retries: 5 networks: - analytics-net collector: build: context: ../services/collector dockerfile: Dockerfile container_name: analytics-collector restart: unless-stopped mem_limit: 384m memswap_limit: 384m mem_reservation: 192m ports: - "127.0.0.1:4001:4001" environment: NODE_ENV: production PORT: "4001" REDIS_HOST: redis REDIS_PORT: "6379" REDIS_PASSWORD: ${REDIS_PASSWORD} CORS_ORIGINS: ${CORS_ORIGINS} COLLECTOR_WRITE_KEY: ${COLLECTOR_WRITE_KEY} LOG_LEVEL: info DATABASE_HOST: timescaledb DATABASE_PORT: "5432" DATABASE_USER: ${POSTGRES_USER} DATABASE_PASSWORD: ${POSTGRES_PASSWORD} DATABASE_NAME: ${POSTGRES_DB} DB_SYNCHRONIZE: "false" depends_on: timescaledb: condition: service_healthy redis: condition: service_healthy healthcheck: test: ["CMD-SHELL", "wget -q --spider http://localhost:4001/health || exit 1"] interval: 30s timeout: 10s retries: 3 start_period: 10s networks: - analytics-net processor: build: context: ../services/processor dockerfile: Dockerfile container_name: analytics-processor restart: unless-stopped mem_limit: 160m memswap_limit: 160m mem_reservation: 96m environment: NODE_ENV: production REDIS_HOST: redis REDIS_PORT: "6379" REDIS_PASSWORD: ${REDIS_PASSWORD} DATABASE_HOST: timescaledb DATABASE_PORT: "5432" DATABASE_USER: ${POSTGRES_USER} DATABASE_PASSWORD: ${POSTGRES_PASSWORD} DATABASE_NAME: ${POSTGRES_DB} CONCURRENCY: "5" BATCH_SIZE: "100" depends_on: timescaledb: condition: service_healthy redis: condition: service_healthy networks: - analytics-net api: build: context: ../services/api dockerfile: Dockerfile container_name: analytics-api restart: unless-stopped mem_limit: 192m memswap_limit: 192m mem_reservation: 115m ports: - "127.0.0.1:4003:4003" environment: NODE_ENV: production PORT: "4003" DATABASE_HOST: timescaledb DATABASE_PORT: "5432" DATABASE_USER: ${POSTGRES_USER} DATABASE_PASSWORD: ${POSTGRES_PASSWORD} DATABASE_NAME: ${POSTGRES_DB} REDIS_HOST: redis REDIS_PORT: "6379" REDIS_PASSWORD: ${REDIS_PASSWORD} API_KEYS: ${API_KEYS} DB_SYNCHRONIZE: "false" depends_on: timescaledb: condition: service_healthy redis: condition: service_healthy healthcheck: test: ["CMD-SHELL", "wget -q --spider http://localhost:4003/health || exit 1"] interval: 30s timeout: 10s retries: 3 start_period: 10s networks: - analytics-net # realtime is intentionally excluded from the default profile. # It handles SSE streams for the live dashboard widget only. # Start manually when needed: docker compose ... up -d realtime realtime: build: context: ../services/realtime dockerfile: Dockerfile container_name: analytics-realtime restart: unless-stopped mem_limit: 128m memswap_limit: 128m mem_reservation: 77m profiles: - realtime ports: - "127.0.0.1:4004:4004" environment: NODE_ENV: production PORT: "4004" REDIS_HOST: redis REDIS_PORT: "6379" REDIS_PASSWORD: ${REDIS_PASSWORD} depends_on: redis: condition: service_healthy networks: - analytics-net website-bff: build: context: ../services/website-bff dockerfile: Dockerfile container_name: analytics-website-bff restart: unless-stopped mem_limit: 64m memswap_limit: 64m mem_reservation: 38m ports: - "127.0.0.1:4005:4005" environment: NODE_ENV: production PORT: "4005" COLLECTOR_URL: http://collector:4001 QUERY_API_URL: http://api:4003 ADMIN_URL: ${ADMIN_URL:-http://localhost:3023} depends_on: collector: condition: service_healthy api: condition: service_healthy healthcheck: test: ["CMD-SHELL", "wget -q --spider http://localhost:4005/health || exit 1"] interval: 30s timeout: 10s retries: 3 start_period: 10s networks: - analytics-net volumes: analytics-postgres-data: name: analytics-postgres-data analytics-redis-data: name: analytics-redis-data networks: analytics-net: name: analytics-net driver: bridge