# `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: ```text / -> 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/`](../local-dev/README.md), 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): ```sh 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: ```caddy 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 ```sh 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: ```sh 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 ```sh 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. ## 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) ▼ 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) ``` 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.): ```sh 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 ```text 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` — six services: postgres, redis, mailpit, galaxy-backend, galaxy-api, galaxy-caddy. `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. - `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`](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.