From ee8d4fd85eb7bb3102f60e18956c888a99a02596 Mon Sep 17 00:00:00 2001 From: Ilia Denisov Date: Fri, 5 Jun 2026 12:01:31 +0200 Subject: [PATCH] =?UTF-8?q?Stage=2016:=20deploy/README.md=20=E2=80=94=20fu?= =?UTF-8?q?ll=20environment-variable=20reference?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - deploy/README.md documents the services, how to run it locally and in CI, and every variable: required (the four :? ones + ≥1 bot token) and optional with defaults, marked secret-vs-variable and with the TEST_/PROD_ Gitea mapping; plus the fixed internal wiring and the host-side setup. - ci.yaml maps the remaining POSTGRES_DB/USER, DICT_VERSION and LOG_LEVEL (unset renders empty -> the compose ":-" defaults apply), so every documented var is per-contour overridable. - .env.example points at the README for the full reference. --- .gitea/workflows/ci.yaml | 5 ++ deploy/.env.example | 2 + deploy/README.md | 102 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 109 insertions(+) create mode 100644 deploy/README.md diff --git a/.gitea/workflows/ci.yaml b/.gitea/workflows/ci.yaml index b606d0c..10207a2 100644 --- a/.gitea/workflows/ci.yaml +++ b/.gitea/workflows/ci.yaml @@ -180,6 +180,11 @@ jobs: VITE_TELEGRAM_LINK: ${{ vars.TEST_VITE_TELEGRAM_LINK }} VITE_GATEWAY_URL: ${{ vars.TEST_VITE_GATEWAY_URL }} GATEWAY_DEFAULT_SUPPORTED_LANGUAGES: ${{ vars.TEST_GATEWAY_DEFAULT_SUPPORTED_LANGUAGES }} + # Unset vars render empty -> the compose ":-" defaults apply. + POSTGRES_DB: ${{ vars.TEST_POSTGRES_DB }} + POSTGRES_USER: ${{ vars.TEST_POSTGRES_USER }} + DICT_VERSION: ${{ vars.TEST_DICT_VERSION }} + LOG_LEVEL: ${{ vars.TEST_LOG_LEVEL }} run: | docker compose --ansi never build --progress plain docker compose --ansi never up -d --remove-orphans diff --git a/deploy/.env.example b/deploy/.env.example index 76c9766..55bbd18 100644 --- a/deploy/.env.example +++ b/deploy/.env.example @@ -1,6 +1,8 @@ # Environment for deploy/docker-compose.yml. The CI deploy job (ci.yaml) maps the # Gitea TEST_-prefixed secrets/variables onto these unprefixed names; Stage 17 # maps the PROD_-prefixed set the same way. Copy to deploy/.env for a local run. +# +# Full reference (required vs optional, defaults, secret-vs-variable): deploy/README.md. # --- Postgres --------------------------------------------------------------- POSTGRES_DB=scrabble diff --git a/deploy/README.md b/deploy/README.md new file mode 100644 index 0000000..3df4c9c --- /dev/null +++ b/deploy/README.md @@ -0,0 +1,102 @@ +# deploy + +The full Scrabble contour: `backend` + `gateway` + Postgres + the Telegram +connector (with a VPN sidecar) + the observability stack (OTel Collector → +Prometheus + Tempo → Grafana), fronted by a **caddy** that owns a single `/_gm` +Basic-Auth (the admin console + Grafana). Topology and the decision record are in +[`../docs/ARCHITECTURE.md`](../docs/ARCHITECTURE.md) §13; this file is the +operational reference for **every environment variable**. + +## Services + +| Service | Image | Role | +| --- | --- | --- | +| `caddy` | `caddy:2-alpine` | Edge proxy (alias `scrabble` on `edge`): single `/_gm` Basic-Auth → admin console + Grafana; everything else → gateway. TLS per `CADDY_SITE_ADDRESS`. | +| `gateway` | built (`gateway/Dockerfile`) | Public edge; serves the embedded SPA at `/` and `/telegram/`; Connect-RPC edge. | +| `backend` | built (`backend/Dockerfile`) | Domain service; bakes in the DAWG dictionaries; runs migrations at boot. | +| `postgres` | `postgres:17-alpine` | Database (named volume, `pg_isready` healthcheck). | +| `vpn` + `telegram` | sidecar + built (`platform/telegram/Dockerfile`) | Telegram connector; egresses through the AmneziaWG sidecar; internal gRPC at `telegram:9091`. | +| `otelcol` | `otel/opentelemetry-collector-contrib` | OTLP/gRPC `:4317` → Prometheus scrape (`:9464`) + Tempo. | +| `prometheus` | `prom/prometheus` | Metrics, 15d retention. | +| `tempo` | `grafana/tempo` | Traces, 72h retention. | +| `grafana` | `grafana/grafana` | Dashboards (provisioned), anonymous-admin behind caddy's `/_gm/grafana`. | + +Networking: inter-service traffic is on the private `internal` network +(project-scoped DNS); only `caddy` joins the shared external `edge` network so the +host caddy can reach it at `scrabble:80`. `edge` must already exist on the host +(`docker network create edge`). + +## Run it + +**Locally** — copy the template, fill the required values, bring it up: + +```sh +cp deploy/.env.example deploy/.env # then edit deploy/.env +docker network create edge # once, if it does not exist +cd deploy && docker compose up -d --build +``` + +**In CI** (the test contour) — `.gitea/workflows/ci.yaml`'s `deploy` job maps the +Gitea **`TEST_`-prefixed** secrets/variables onto the unprefixed names below and +runs `docker compose up -d --build` on the runner host. Stage 17 (prod) maps the +**`PROD_`** set the same way. So a Gitea secret named `TEST_POSTGRES_PASSWORD` +feeds the compose's `POSTGRES_PASSWORD`, etc. + +## Required variables + +`docker compose` aborts immediately if any of these is unset (they use `:?`): + +| Variable | Gitea kind | Purpose | +| --- | --- | --- | +| `POSTGRES_PASSWORD` | secret | Postgres password (also embedded in `BACKEND_POSTGRES_DSN`). | +| `AWG_CONF` | secret | AmneziaWG config for the VPN sidecar (the connector's only egress). | +| `GM_BASICAUTH_HASH` | secret | bcrypt hash gating `/_gm` (admin console + Grafana). Generate with `docker run --rm caddy:2-alpine caddy hash-password --plaintext ''`. | +| `TELEGRAM_MINIAPP_URL` | variable | The Mini App URL the connector hands out in deep links / buttons. | + +**Plus at least one bot token** — `TELEGRAM_BOT_TOKEN_EN` or `TELEGRAM_BOT_TOKEN_RU` +(secrets). Compose cannot express "one of", so they default to empty, but the +connector **fails at boot** if both are empty. + +## Optional variables (with defaults) + +| Variable | Gitea kind | Default | Purpose | +| --- | --- | --- | --- | +| `POSTGRES_DB` | variable | `scrabble` | Database name. | +| `POSTGRES_USER` | variable | `scrabble` | Database user. | +| `DICT_VERSION` | variable | `v1.0.0` | `scrabble-dictionary` release tag baked into the backend image (build-arg). | +| `LOG_LEVEL` | variable | `info` | Shared log level for backend / gateway / connector (`debug\|info\|warn\|error`). | +| `CADDY_SITE_ADDRESS` | variable | `:80` | Caddy site address. Test: `:80` (host caddy terminates TLS). Prod: a domain, so caddy does its own ACME. | +| `GM_BASICAUTH_USER` | variable | `gm` | Username for the `/_gm` Basic-Auth. | +| `GRAFANA_ROOT_URL` | variable | `/_gm/grafana/` | Grafana root URL (sub-path serving). Set the full `https:///_gm/grafana/` behind a real domain. | +| `GRAFANA_ADMIN_PASSWORD` | secret | `admin` | Grafana admin password. Low impact (the login form is disabled, access is anonymous-admin behind caddy) but set it anyway. | +| `TELEGRAM_GAME_CHANNEL_ID_EN` | variable | _(empty)_ | English game-channel id; empty/`0` disables channel posts. | +| `TELEGRAM_GAME_CHANNEL_ID_RU` | variable | _(empty)_ | Russian game-channel id; empty/`0` disables channel posts. | +| `TELEGRAM_TEST_ENV` | variable | `false` | `true` routes the bot through Telegram's test environment. | +| `TELEGRAM_API_BASE_URL` | variable | _(empty)_ | Override the Bot API host (a mock/self-hosted server); empty = `https://api.telegram.org`. | +| `GATEWAY_DEFAULT_SUPPORTED_LANGUAGES` | variable | `en,ru` | Variant-gating set for non-Telegram logins (web/email/guest). | +| `VITE_TELEGRAM_BOT_ID` | variable | _(empty)_ | UI build-arg: numeric bot id for the web Login Widget. | +| `VITE_TELEGRAM_LINK` | variable | _(empty)_ | UI build-arg: deep-link base for share-to-Telegram (e.g. `https://t.me//`). | +| `VITE_GATEWAY_URL` | variable | _(empty)_ | UI build-arg: gateway origin; empty = same-origin (the usual single-origin deploy). | + +The three `VITE_*` are **build-args** baked into the gateway image at build time, so +changing them requires a rebuild (`--build`), not just a restart. + +## Fixed internal wiring (not operator-set) + +These are hard-wired in `docker-compose.yml` (no `${...}`), pointing the services +at each other on the `internal` network — listed here so they are not mistaken for +missing config: `BACKEND_POSTGRES_DSN` (→ `postgres`, `search_path=backend`), +`GATEWAY_BACKEND_HTTP_URL`/`_GRPC_ADDR` (→ `backend`), +`GATEWAY_CONNECTOR_ADDR`/`BACKEND_CONNECTOR_ADDR` (→ `telegram:9091`), the three +services' `*_OTEL_*_EXPORTER=otlp` + `OTEL_EXPORTER_OTLP_ENDPOINT=http://otelcol:4317` +(`_INSECURE=true`). `GATEWAY_ADMIN_*` is intentionally **unset** — caddy owns `/_gm` +in the contour. + +## Host-side setup (outside this repo) + +- **`edge` network** must exist on the host (`docker network create edge`). +- **Host caddy** route ` → scrabble:80` (the in-compose caddy serves HTTP + in the test contour; the host caddy terminates TLS). Not needed on prod, where the + contour caddy owns TLS (set `CADDY_SITE_ADDRESS` to the domain). +- **Branch protection** required-status-check names are `CI / unit`, + `CI / integration`, `CI / ui` (see [`../CLAUDE.md`](../CLAUDE.md) "Branching & CI").