diff --git a/.gitea/workflows/ci.yaml b/.gitea/workflows/ci.yaml index 1299e62..fe5a438 100644 --- a/.gitea/workflows/ci.yaml +++ b/.gitea/workflows/ci.yaml @@ -301,8 +301,11 @@ jobs: # App version for the About screen: the git tag if present, else the short SHA # (the test checkout is shallow/untagged, so this is the SHA here — fine). export APP_VERSION="$(git -C "$GITHUB_WORKSPACE" describe --tags --always 2>/dev/null || echo dev)" - docker compose --ansi never build --progress plain - docker compose --ansi never up -d --remove-orphans + # The telegram-local profile brings the bot + its VPN sidecar; prod runs the + # bot on its own host instead (deploy/docker-compose.bot.yml), and the prod + # main host omits both. Without the profile they would not start here. + docker compose --ansi never --profile telegram-local build --progress plain + docker compose --ansi never --profile telegram-local up -d --remove-orphans # The config-only services bind-mount the reseeded config dir. A plain `up -d` # leaves them on the previous bind mount (the dir was rm'd + recreated), so a # changed Caddyfile or Grafana dashboard is ignored — force-recreate them to diff --git a/deploy/docker-compose.bot.yml b/deploy/docker-compose.bot.yml new file mode 100644 index 0000000..b4c0975 --- /dev/null +++ b/deploy/docker-compose.bot.yml @@ -0,0 +1,53 @@ +# Production Telegram bot host descriptor (standalone — NOT an overlay). Run only on +# the bot host: +# docker compose -f docker-compose.bot.yml up -d +# +# The bot egresses to the Bot API directly (no VPN sidecar) and dials the main host's +# published bot-link :9443 over mTLS. It exports no telemetry — otelcol lives on the +# main host and is unreachable from here — so observe it via `docker logs` on this host. +# Values come from the prod-deploy workflow (PROD_ secrets/variables); BOT_IMAGE is the +# pushed registry tag and BOTLINK_GATEWAY_ADDR is the main host's :9443. +name: scrabble-bot + +services: + bot: + container_name: scrabble-telegram-bot + image: ${BOT_IMAGE:?set BOT_IMAGE to the registry tag} + restart: unless-stopped + logging: + driver: json-file + options: + max-size: "10m" + max-file: "3" + environment: + TELEGRAM_BOT_TOKEN: ${TELEGRAM_BOT_TOKEN:?set TELEGRAM_BOT_TOKEN} + TELEGRAM_GAME_CHANNEL_ID: ${TELEGRAM_GAME_CHANNEL_ID:-} + TELEGRAM_CHAT_ID: ${TELEGRAM_CHAT_ID:-} + TELEGRAM_PROMO_BOT_TOKEN: ${TELEGRAM_PROMO_BOT_TOKEN:-} + TELEGRAM_BOT_USERNAME: ${TELEGRAM_BOT_USERNAME:-} + TELEGRAM_BOT_LINK: ${TELEGRAM_BOT_LINK:-} + TELEGRAM_MINIAPP_URL: ${TELEGRAM_MINIAPP_URL:?set TELEGRAM_MINIAPP_URL} + # Real Bot API in prod (the test contour pins TELEGRAM_TEST_ENV=true instead). + TELEGRAM_TEST_ENV: "false" + TELEGRAM_API_BASE_URL: ${TELEGRAM_API_BASE_URL:-} + TELEGRAM_OWNS_UPDATES: "true" + # Dials the main host's published bot-link. ServerName stays `gateway` (the cert + # SAN), so TLS validation is independent of the dial address. + TELEGRAM_GATEWAY_ADDR: ${BOTLINK_GATEWAY_ADDR:?set BOTLINK_GATEWAY_ADDR (main:9443)} + TELEGRAM_BOTLINK_SERVER_NAME: gateway + TELEGRAM_BOTLINK_TLS_CERT: /certs/bot.crt + TELEGRAM_BOTLINK_TLS_KEY: /certs/bot.key + TELEGRAM_BOTLINK_TLS_CA: /certs/ca.crt + TELEGRAM_LOG_LEVEL: ${LOG_LEVEL:-info} + TELEGRAM_SERVICE_NAME: scrabble-telegram-bot + # No telemetry export: otelcol is on the main host, unreachable from here. + TELEGRAM_OTEL_TRACES_EXPORTER: none + TELEGRAM_OTEL_METRICS_EXPORTER: none + GOMAXPROCS: "1" + volumes: + - ${SCRABBLE_CONFIG_DIR:-.}/certs:/certs:ro + deploy: + resources: + limits: + cpus: "1.0" + memory: 256M diff --git a/deploy/docker-compose.prod.yml b/deploy/docker-compose.prod.yml new file mode 100644 index 0000000..6b45826 --- /dev/null +++ b/deploy/docker-compose.prod.yml @@ -0,0 +1,92 @@ +# Production main-host overlay, applied on top of docker-compose.yml on the main host: +# docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d +# +# It (1) publishes caddy 80/443 — there is no host caddy in prod, so the contour caddy +# owns the edge and does its own ACME on CADDY_SITE_ADDRESS — and the gateway bot-link +# :9443 the remote bot dials in over mTLS; and (2) retunes the R7 limits down for the +# 2 vCPU / 1.9 GiB host (GOMAXPROCS=2, smaller memory caps, shorter Prometheus +# retention). The contour launches deliberately undersized at zero players; the added +# node_exporter + Grafana watch host memory so it can be resized at Selectel when +# traffic arrives. +# +# The bot + its VPN sidecar are absent here (the telegram-local profile is not +# activated); the prod bot runs on its own host from docker-compose.bot.yml. + +services: + caddy: + ports: + - "80:80" + - "443:443" + deploy: + resources: + limits: + memory: 96M + + gateway: + ports: + - "9443:9443" + environment: + # 2 vCPU host: align the Go scheduler with the cgroup quota (R7's 3 needs 3 cores). + GOMAXPROCS: "2" + deploy: + resources: + limits: + cpus: "2.0" + memory: 384M + + backend: + deploy: + resources: + limits: + memory: 384M + + postgres: + deploy: + resources: + limits: + memory: 384M + + validator: + deploy: + resources: + limits: + memory: 96M + + landing: + deploy: + resources: + limits: + memory: 64M + + otelcol: + deploy: + resources: + limits: + memory: 256M + + prometheus: + command: + - --config.file=/etc/prometheus/prometheus.yml + - --storage.tsdb.retention.time=7d + deploy: + resources: + limits: + memory: 256M + + tempo: + deploy: + resources: + limits: + memory: 384M + + grafana: + deploy: + resources: + limits: + memory: 256M + + postgres_exporter: + deploy: + resources: + limits: + memory: 64M diff --git a/deploy/docker-compose.yml b/deploy/docker-compose.yml index cd17de5..fce7650 100644 --- a/deploy/docker-compose.yml +++ b/deploy/docker-compose.yml @@ -240,14 +240,22 @@ services: networks: [internal] # --- Telegram bot (egress via the VPN sidecar in test; dials the gateway) --- + # vpn + bot are gated to the `telegram-local` profile: the test contour runs them + # locally (CI passes --profile telegram-local), the prod main host omits them, and + # the prod bot runs on its own host from deploy/docker-compose.bot.yml. vpn: container_name: scrabble-telegram-vpn image: docker.iliadenisov.ru/developer/amneziawg-sidecar:latest + profiles: ["telegram-local"] restart: unless-stopped logging: *default-logging privileged: true environment: - AWG_CONF: ${AWG_CONF:?set AWG_CONF} + # Required by the vpn sidecar, which is gated to the telegram-local profile. + # Compose can't scope a `:?` guard to a profile (interpolation runs for + # profiled-out services too) and the prod main host has no VPN, so this is a soft + # default; the test contour always supplies TEST_AWG_CONF and the sidecar validates it. + AWG_CONF: ${AWG_CONF:-} networks: internal: aliases: [telegram] @@ -255,6 +263,7 @@ services: bot: container_name: scrabble-telegram-bot image: scrabble-telegram-bot:latest + profiles: ["telegram-local"] build: context: .. dockerfile: platform/telegram/Dockerfile @@ -444,6 +453,26 @@ services: memory: 128M networks: [internal] + # node_exporter exports host CPU/memory/disk metrics. The prod main host runs a tight + # 1.9 GiB budget, so host memory pressure — not just per-container docker_stats — is + # what warns before an OOM. Prometheus scrapes it at :9100 (see prometheus.yml). + node_exporter: + container_name: scrabble-node-exporter + image: quay.io/prometheus/node-exporter:v1.8.2 + restart: unless-stopped + logging: *default-logging + command: + - --path.rootfs=/host + - --collector.filesystem.mount-points-exclude=^/(sys|proc|dev|host)($|/) + pid: host + volumes: + - /:/host:ro,rslave + deploy: + resources: + limits: + memory: 64M + networks: [internal] + networks: internal: name: scrabble-internal diff --git a/deploy/prometheus/prometheus.yml b/deploy/prometheus/prometheus.yml index 71d6cd8..6dc3d43 100644 --- a/deploy/prometheus/prometheus.yml +++ b/deploy/prometheus/prometheus.yml @@ -18,3 +18,8 @@ scrape_configs: - job_name: postgres_exporter static_configs: - targets: ["postgres_exporter:9187"] + # Host-level metrics (memory/CPU/disk). Matters most on the prod main host's tight + # 1.9 GiB budget, where total host memory is the OOM-proximity signal. + - job_name: node + static_configs: + - targets: ["node_exporter:9100"]