Files
galaxy-game/tools/dev-deploy
Ilia Denisov 814eae0802
Tests · Go / test (pull_request) Successful in 1m56s
Tests · Integration / integration (pull_request) Successful in 1m41s
Tests · UI / test (pull_request) Successful in 3m23s
docs: observability stack + the single /_gm gate for Grafana/Mailpit
- ARCHITECTURE §17: the dev (production-mirror) collection stack
  (Prometheus / Loki / Tempo / promtail / node-exporter / cAdvisor) and
  the single /_gm Basic Auth gate fronting Grafana and the Mailpit UI.
- tools/dev-deploy/monitoring/README.md (new): services, what is
  collected, Grafana-behind-the-gate access, config delivery, tuning.
- tools/dev-deploy/README.md: an Observability section; the Mailpit UI
  under /_gm/mailpit/; Networking diagram and Files list updated.
- FUNCTIONAL §10.2.1 (+ ru mirror): the operator console nav links to
  Grafana and Mailpit under the same /_gm gate, one sign-in for all.
2026-06-01 06:37:24 +02:00
..

tools/dev-deploy/ — long-lived Galaxy dev environment

A docker-compose stack that runs the Galaxy backend, gateway, supporting services, and a small Caddy in front of them, reachable through the host Caddy at a single origin (https://galaxy.lan in dev). The stack is single-origin and path-based: the project site, the game UI, and both gateway surfaces live behind one host with no host name baked into the artifacts. Used by the dev-deploy.yaml Gitea Actions workflow as the canonical dev target on every merge into the development branch, and runnable by hand through this Makefile for local debugging of the deploy plumbing itself.

The application Caddy (Caddyfile.dev) is the authoritative routing source; its header comment documents the exact topology:

/                -> project site   (galaxy-dev-site-dist -> /srv/galaxy-site)
/game/*          -> game UI        (galaxy-dev-ui-dist   -> /srv/galaxy-ui)
/api/*, /healthz -> gateway public REST       (galaxy-api:8080)
/rpc/*           -> gateway Connect/gRPC-web  (galaxy-api:9090)

The /rpc prefix is stripped before the gateway, and the game UI bundle is built with base path /game.

This stack is not the developer's primary playground for UI work — that role still belongs to tools/local-dev/, which is faster (Vite HMR, host-side dev server) and isolated to one developer. The two stacks coexist on the same host because every name is distinct:

tools/local-dev/ tools/dev-deploy/
Compose project local-dev galaxy-dev
Container prefix galaxy-local-dev-* galaxy-dev-*
Network galaxy-local-dev-net galaxy-dev-internal, edge
Volumes galaxy-local-dev-* galaxy-dev-*
Host ports 5433/6380/8025/8080/9090 none (only edge network)
Game state /tmp/galaxy-game-state /var/lib/galaxy-dev/game-state
Engine image galaxy-engine:local-dev galaxy-engine:dev

Prerequisites

The host must already provide:

  • Docker daemon reachable as the user running make (member of the docker group, no sudo).

  • An external bridge network named edge (or whatever GALAXY_EDGE_NETWORK overrides to):

    docker network create edge
    
  • A host Caddy listening on :80/:443, attached to the edge network, and proxying the single dev host galaxy.lan to galaxy-caddy:80. The host Caddy only needs that one host; Caddyfile.dev does the path-based fan-out behind it. Example fragment for the host Caddyfile:

    galaxy.lan {
        tls internal
        reverse_proxy galaxy-caddy:80
    }
    
  • Game-state directory writable by the user running make. Default is ${HOME}/.galaxy-dev/game-state; make up creates it on demand. Override by exporting GALAXY_DEV_GAME_STATE_DIR (e.g. to /var/lib/galaxy-dev/game-state once the host is provisioned for it).

Bring it up

make -C tools/dev-deploy up

up (re)builds the local-dev backend and gateway images, makes sure the engine image galaxy-engine:dev exists, and waits for healthchecks. It does not seed the UI or site volumes — that is normally done by CI. The first time you run by hand:

make -C tools/dev-deploy seed-site
make -C tools/dev-deploy seed-ui
make -C tools/dev-deploy up
make -C tools/dev-deploy health

seed-ui runs pnpm build in ui/frontend/ (base path /game), then copies the resulting build/ tree into the galaxy-dev-ui-dist volume. seed-site builds the VitePress project site in site/ and copies its .vitepress/dist/ output into the galaxy-dev-site-dist volume. Subsequent CI deploys overwrite both volumes automatically.

Daily flow

make -C tools/dev-deploy rebuild   # rebuild backend/gateway images + up
make -C tools/dev-deploy logs      # tail compose logs
make -C tools/dev-deploy health    # probe https://galaxy.lan/ , /game/ , /healthz
make -C tools/dev-deploy down      # stop, keep state

State persists in named volumes between up/down cycles. The development branch keeps the dev environment continuously usable — games created last week survive into this week unless somebody calls make clean-data.

Logging in

The same dev-mode email-code override as tools/local-dev/ applies, and the dev-deploy compose ships with it enabled by default:

  1. Enter your email address in the login form.
  2. Submit 123456 as the code — the docker-compose default for BACKEND_AUTH_DEV_FIXED_CODE is 123456, so the bcrypt-hashed email code stays a fallback. To force the real email code (which Mailpit then relays to your Gmail — see Mail below), set BACKEND_AUTH_DEV_FIXED_CODE= (empty) and redeploy.

The fixed-code override is rejected by production env loaders, so it cannot leak into the prod environment.

Mail

The backend always submits mail to Mailpit (galaxy-mailpit:1025), exactly as it would to a production SMTP server. Mailpit captures every message in its UI (internal :8025) and, when configured, relays the ones whose recipient matches GALAXY_DEV_MAIL_RELAY_MATCH up to a real Gmail account — so an OTP addressed to you lands in your real inbox while everything else stays captured-only.

Configure the relay through Gitea Actions secrets/vars (never committed); the dev-deploy.yaml workflow renders Mailpit's relay.conf (from tools/dev-deploy/mailpit/relay.conf.tmpl) and seeds it into the galaxy-dev-mailpit-config volume:

Name Kind Purpose
GALAXY_DEV_MAIL_RELAY_USERNAME secret Gmail address used as the relay login + From.
GALAXY_DEV_MAIL_RELAY_PASSWORD secret Gmail App Password (requires 2FA; not the account password).
GALAXY_DEV_MAIL_RELAY_MATCH var Recipient regex to auto-relay (e.g. your Gmail address). Unset → capture-only.

With none set the stack only captures mail (the compose relay-match defaults to a non-routable address), so it can never email third parties.

The capture UI is exposed through the operator console's /_gm gate at /_gm/mailpit/ — one Basic Auth for the console, Grafana and Mailpit (see Observability). It shows every message the backend sent, relayed or not, so you can read any account's OTP regardless of the relay-match. For multi-account testing: register several you+tag@gmail.com aliases and widen the match to a regex such as ^you(\+[^@]+)?@gmail\.com$ (Gmail folds every +tag into one inbox), or just read the codes in the Mailpit UI, or skip mail entirely with the 123456 dev-code.

Observability

A full metrics + logs + traces stack runs alongside the app on the internal network (no host ports), as a production mirror. Grafana and the Mailpit UI are reached only through the operator console's single /_gm Basic Auth gate — one password (the admin-console account) unlocks the console, /_gm/grafana/ and /_gm/mailpit/, with links in the console nav. Grafana runs anonymous-Admin behind the gate (no own login); Prometheus, Loki and Tempo stay internal-only.

  • Metrics — Prometheus scrapes backend, gateway, node-exporter and cAdvisor.
  • Logs — promtail → Loki (Docker SD on the galaxy.stack=dev-deploy label).
  • Traces — backend + gateway → Tempo over OTLP.

Grafana's admin user is seeded from GALAXY_DEV_GRAFANA_ADMIN_PASSWORD (for provisioning/API; the UI needs no Grafana login). See monitoring/README.md for services, configs and tuning knobs.

Networking

Browser
   │  https://galaxy.lan/   (one origin, path-based)
   ▼
host-Caddy (:80, :443, TLS, attached to `edge` network)
   │  reverse_proxy galaxy.lan → galaxy-caddy:80
   ▼
galaxy-caddy  (networks: edge + galaxy-dev-internal)
   │  /                -> file_server /srv/galaxy-site (volume galaxy-dev-site-dist)
   │  /game/*          -> file_server /srv/galaxy-ui   (volume galaxy-dev-ui-dist)
   │  /api/*, /healthz -> reverse_proxy galaxy-api:8080
   │  /rpc/*           -> reverse_proxy galaxy-api:9090 (strips /rpc)
   │  /_gm, /_gm/*     -> reverse_proxy galaxy-api:8080 (Basic Auth gate;
   │                      /_gm/grafana/ -> grafana, /_gm/mailpit/ -> mailpit)
   ▼
galaxy-dev-internal
   ├─ galaxy-api      (gateway:   :8080 REST, :9090 gRPC)
   ├─ galaxy-backend  (backend:   :8080 HTTP, :8081 gRPC push)
   ├─ galaxy-postgres (postgres:  :5432)
   ├─ galaxy-redis    (redis:     :6379)
   ├─ galaxy-mailpit  (mailpit:   :8025 UI, :1025 SMTP)
   ├─ engine containers (spawned by backend on demand)
   └─ observability   (prometheus, grafana, loki, promtail, tempo,
                       node-exporter, cadvisor)

The compose project deliberately exposes no host ports. Diagnostics that used to go through localhost:8025 etc. now go through the container network: docker compose -f tools/dev-deploy/docker-compose.yml exec galaxy-mailpit wget -qO- localhost:8025/messages and similar.

Persistent state and schema changes

The dev Postgres volume galaxy-dev-postgres-data survives redeploys. Schema deltas land as additive, sequence-numbered migration files (backend/internal/postgres/migrations/0000N_*.sql) and pressly/goose applies them on backend startup without operator action.

Use make -C tools/dev-deploy clean-data only when you deliberately want a fresh database (debugging schema drift, exercising the bootstrap path from scratch, etc.):

make -C tools/dev-deploy clean-data
make -C tools/dev-deploy up

The same volume-persistence model applies to tools/local-dev/.

Make targets

make up             Build images, ensure engine image, seed geoip, bring stack up
make rebuild        Rebuild backend / gateway images (ignores cache), then up
make seed-ui        pnpm build (base /game) + load build/ into galaxy-dev-ui-dist volume
make seed-site      vitepress build + load site dist into galaxy-dev-site-dist volume
make seed-geoip     Copy pkg/geoip fixture into galaxy-dev-geoip-data volume
make build-engine   Build galaxy-engine:dev (no-op if image already present)
make down           Stop containers, keep named volumes
make logs           Tail compose logs
make status         docker compose ps
make health         curl https://galaxy.lan/ , /game/ , and /healthz
make psql           psql as galaxy@galaxy_backend
make clean-data     Stop everything and wipe volumes + game-state dir

Files

  • docker-compose.yml — the application services (postgres, redis, mailpit, galaxy-backend, galaxy-api, galaxy-caddy) plus the observability stack (prometheus, grafana, loki, promtail, tempo, node-exporter, cadvisor). galaxy-caddy mounts both the galaxy-dev-site-dist (/srv/galaxy-site) and galaxy-dev-ui-dist (/srv/galaxy-ui) volumes and reverse-proxies both gateway tiers (REST/health on :8080, Connect/gRPC-web on :9090). Reuses the alpine-runtime Dockerfiles from ../local-dev/ so the backend healthcheck can run wget. Reuses the dev keypair from ../local-dev/keys/.
  • Caddyfile.dev — the application-routing Caddy config and the authoritative single-origin path topology, mounted into galaxy-caddy at /etc/caddy/Caddyfile.
  • Caddyfile.prod — placeholder for a future prod deployment; not used by this compose.
  • monitoring/ — Prometheus / Loki / promtail / Tempo / Grafana configuration, provisioned as code; see monitoring/README.md.
  • Makefile — wrapper over docker compose with helpers for engine, site/UI seeding, health probes, and full wipe.
  • .env.example — non-secret defaults for the compose ${VAR:-} expansions. Copy to .env if you want host-local overrides.

Known issues

See KNOWN-ISSUES.md for symptoms that surface in the long-lived dev environment but are not yet fixed.

Deployment cadence

This environment is single-tenant: one live deployment, redeployed by the dev-deploy.yaml workflow on every merge into development. PR branches do not auto-deploy here — pushes to feature/* only run the test workflows (go-unit, ui-test, integration).

To put a feature branch on the shared dev environment before its PR merges (e.g. to validate a UI flow against the real Caddy edge), run the workflow manually:

  1. Push the branch (git push gitea HEAD).
  2. Gitea UI → Actions → Deploy · Dev → Run workflow, pick the feature ref.

The deploy is idempotent — when the PR later merges into development, the regular push trigger fires the same packaging and healthcheck steps, overwriting whatever the manual dispatch left behind. There is no separate state to clean up between the two paths.

Engine image drift recycle

backend spawns one engine container per running game and the reconciler reattaches to whatever it finds with the galaxy.stack=dev-deploy label. That reattach does not check the running container's image SHA against the freshly-built galaxy-engine:dev tag, so an unchanged container would otherwise keep serving the previous engine code after a redeploy.

The dev-deploy.yaml workflow handles this in the Recycle engine containers on image drift step. When docker build produces a new galaxy-engine:dev SHA, the step compares it against every running galaxy-game-* container and, for each drifted one, stops the backend, removes the container, wipes its bind-mounted state directory (Engine.Init() writes turn-0 over any pre-existing turn-N files), and cascade-deletes the lobby games row.

When the engine sources are unchanged, the BuildKit cache hits and the SHA stays the same — the recycle step is a no-op and the running games keep their state across the deploy.

Relationship to other infrastructure

  • tools/local-dev/ — single-developer playground, host-port mapped, Vite dev server on the side. Recommended for active UI work.
  • .gitea/workflows/dev-deploy.yaml — the CI side of this stack: builds images, seeds the site and UI volumes, runs docker compose up -d on every merge into development. The Makefile in this directory is what that workflow ultimately calls into.