Stage 1 of the dev-as-prod-mirror rework. The auto-provisioned "Dev Sandbox" game and dummy users are removed so the dev contour starts empty like prod; the separate legacy-report loader stays as the test-data path. - delete backend/internal/devsandbox (package + tests) - drop the bootstrap call + DevSandboxConfig (struct, Config field, BACKEND_DEV_SANDBOX_* env, defaults, loader, validation) - strip BACKEND_DEV_SANDBOX_* from dev-deploy + local-dev compose and .env.example; the generic engine-recycle / prune-broken-engines logic stays (it serves real games) - update tooling docs (dev-deploy README + KNOWN-ISSUES, local-dev README + Makefile) and stale comments; DeleteGame and InsertMembershipDirect remain (exercised by lobby integration tests) No app behaviour change beyond not auto-creating the sandbox game.
13 KiB
tools/local-dev/ — Galaxy local development stack
A docker-compose stack that brings up postgres + redis + mailpit + backend + gateway so the UI Vite dev server (run on the host) can talk to a real authenticated stack without any cloud dependency.
The stack is the recommended baseline for UI work that goes beyond the mocked Playwright fixtures: every payload exercises the real FlatBuffers wire, every authenticated call verifies the response signature against the dev keypair, and every email passes through Mailpit's web UI for inspection.
This stack is not a CI gate (the per-stage CI gate now lives on
gitea.lan; see project-level CLAUDE.md). It is also distinct from
the long-lived dev environment at
tools/dev-deploy/, which is redeployed on
every merge into development and is reachable at the single origin
https://galaxy.lan (site at /, game UI at /game/). The two stacks
(tools/local-dev/ and tools/dev-deploy/) coexist on the same host
because every name — compose project, container, network, volume — is
distinct.
Bring it up
make -C tools/local-dev up
up builds the local-dev backend and gateway images on first run
(pulls postgres, redis, mailpit), waits for every service to report
healthy, and returns. Subsequent invocations reuse the built images.
After the stack is healthy:
pnpm -C ui/frontend dev
Open http://localhost:5173 for the UI and http://localhost:8025 for Mailpit.
The first make up builds the engine image (galaxy-engine:local-dev)
from game/Dockerfile. Subsequent invocations skip the build when the
image already exists; force a rebuild with docker rmi galaxy-engine:local-dev
followed by make build-engine.
Daily flow
make -C tools/local-dev up # bring up (idempotent, fast on warm cache)
pnpm -C ui/frontend dev # in another terminal
# ...edit UI, browse, repeat...
make -C tools/local-dev down # stop containers, keep state
State persists in named Docker volumes between up/down cycles, so
games created on Tuesday survive into Wednesday. Wipe with
make clean when you want a fresh database.
Logging in
Two paths coexist by default:
- Fixed dev code (fast).
tools/local-dev/.envshipsBACKEND_AUTH_DEV_FIXED_CODE=123456. After requesting a code in the UI, type123456—ConfirmEmailCodeaccepts that literal in addition to the real bcrypt-verified code stored on the challenge row. The override emits a loud warning at backend boot and is rejected by the production env loader (BACKEND_ENVguard inbackend/internal/config). - Real Mailpit code. Open http://localhost:8025, find the most recent message, copy the six-digit code, paste it into the UI. This exercises the full mail outbox path, including SMTP handoff and gomail TLS-mode handling.
To force the second path (no fast-bypass), edit
tools/local-dev/.env and clear BACKEND_AUTH_DEV_FIXED_CODE, then
make rebuild (or simply docker compose up -d backend to recreate
the backend with the new env).
No auto-provisioned game
make up brings up the stack with an empty lobby — there is no
auto-provisioned game. Sign in with email-OTP (the fixed dev code
123456 works when BACKEND_AUTH_DEV_FIXED_CODE is set in
tools/local-dev/.env):
make -C tools/local-dev uppnpm -C ui/frontend dev(in another terminal)- Open http://localhost:5173/login, enter your email, then the dev
code
123456.
To exercise the map and report views without running a full game, use
the UI's DEV synthetic report loader: convert a legacy .REP with
tools/local-dev/legacy-report/ and load the resulting JSON through the
loader (see that tool's README). To play a real game, create one in the
lobby and let the engine (galaxy-engine:local-dev, built by
make build-engine) run it.
- A frozen turn schedule (
0 0 1 1 *— once a year) so the visible game state stays at turn 1 until you explicitly progress it.
Network map
host compose network "galaxy-local-dev-net"
┌────────────────────────────────┐ ┌──────────────────────────────┐
│ browser localhost:5173 │── pnpm dev (Vite, host) ──┐ │
│ ↳ /api/* proxied ───┼──────────────────────────▶│ gateway:8080 │
│ ↳ /rpc/* proxied ───┼──────────────────────────▶│ gateway:9090 │
│ browser localhost:8025 │─────────────────────────▶│ mailpit:8025 │
│ psql localhost:5433 │─────────────────────────▶│ postgres:5432 │
│ redis-cli localhost:6380 │─────────────────────────▶│ redis:6379 │
└────────────────────────────────┘ │ ↳ backend:8080 (HTTP) │
│ ↳ backend:8081 (gRPC push) │
│ ↳ mailpit:1025 (SMTP in) │
└────────────────────────────────┘
Vite's dev server proxies /api (to the gateway REST listener) and
/rpc (to the authenticated Connect/gRPC-Web listener, stripping the
/rpc prefix), so every browser request stays same-origin (no CORS
preflight). The gateway is therefore reachable only through Vite at
http://localhost:5173, not at http://localhost:8080 from the
browser tab. Direct curl/wget against http://localhost:8080 still
works for diagnostic probes — only the browser-side requests are
proxied.
Mailpit (8025), postgres (5433), and redis (6380) remain directly
reachable for diagnostics (make psql, redis-cli -h localhost -p 6380 -a galaxy-dev).
To point the proxy at a non-local gateway, run
VITE_DEV_PROXY_TARGET=http://gateway.host:8080 pnpm -C ui/frontend dev
— no compose changes needed.
Refreshing after Go-side changes
make up reuses any pre-built images and, by default, only rebuilds
the engine image (build-engine) when the tag is missing. Touching
backend or gateway code (handlers, routes, transcoders, model
constants) does not trigger a rebuild on its own — the next
docker compose up -d will reattach to the stale image and the
new behaviour silently disappears. After any change under
backend/, gateway/, pkg/, or the FBS schemas, force a
rebuild:
make -C tools/local-dev rebuild
rebuild runs compose build --no-cache backend gateway followed
by up -d --wait, so the next request through the stack hits the
new code. Engine code lives in a separate image — touch the engine
and run make stop-engines plus docker rmi galaxy-engine:local-dev
before make up (or make build-engine) so per-game containers
respawn from the freshly built layers.
Make targets
make up Bring up the stack (build engine + compose images if needed) and wait for health
make rebuild Rebuild the backend / gateway images (ignores cache)
make build-engine Build galaxy-engine:local-dev from game/Dockerfile (no-op if image already present)
make down Stop containers, keep volumes
make clean Stop and wipe volumes (postgres + game-state)
make logs Tail every service's logs
make logs-backend Tail backend only
make logs-gateway Tail gateway only
make logs-mail Tail mailpit only
make psql Open a psql shell as galaxy@galaxy_backend
make status docker compose ps
Files
docker-compose.yml— five services: postgres, redis, mailpit, backend, gateway, plus shared network and volumes.backend.Dockerfile,gateway.Dockerfile— local-dev runtime images built on alpine (sowgetis available for the compose healthchecks). The build stage mirrorsbackend/Dockerfileandgateway/Dockerfileexactly.Makefile— wrapper overdocker composewith thin targets for the most common dev cycles..env— committed defaults for the compose${VAR:-}expansions. Edit per-developer or override via your shell.keys/gateway-response.pem,keys/gateway-response.pub— dev-only Ed25519 keypair used by the gateway for response signing. Pairs with theVITE_GATEWAY_RESPONSE_PUBLIC_KEYvalue inui/frontend/.env.development. Seekeys/README.mdbefore rotating.keys/regenerate.go— one-shot Go helper that regenerates the pair and prints the new base64 public key.
Troubleshooting
-
Lobby shows "no games yet" after
make clean && make up— the browser still holds a keypair + device session bound to the user_id from the previous DB. The new user has the same email (dev@local.test) but a fresh user_id, so the old keypair authenticates against a session row that no longer exists or points at the wrong account. Open the page in an incognito window, or wipe site data forlocalhost:5173(DevTools → Application → Storage → Clear site data) and log in again. -
make downleaves agalaxy-game-…container behind — fixed in this Makefile:make downandmake cleannow stop spawned engine containers via thegalaxy.backend=1label. To stop them by hand without touching the rest of the stack,make stop-engines. -
Engine container exits with
bind source path does not exist: /tmp/galaxy-game-state/<uuid>after a host reboot — macOS clears/private/tmpon reboot, so the per-game state directory the long-lived engine container bind-mounts is gone and Docker refuses to restart it underrestart: unless-stopped.make upauto-heals this in one cycle:prune-broken-engines(runs as part ofup) removes every engine container that is not inrunning/restartingstate, the backend's pre-bootstrap reconciler tick cascades the orphan runtime row toremoved, and the lobby cancels the matching game. To run the cleanup by hand without restarting the rest of the stack,make prune-broken-engines.The cycle relies on the backend image carrying the pre-bootstrap reconciler tick (
backend/cmd/backend/main.go).make upreuses the cached image, so after pulling this commit the first time you mustmake rebuildonce to bake the fix in. Futuremake upcycles will heal in one shot. -
make upreports a build error mentioningpkg/cronutil— upstream module list drifted; copy any newpkg/<name>/line into the local-devbackend.Dockerfile/gateway.Dockerfileto matchbackend/Dockerfile/gateway/Dockerfile. -
Gateway exits at boot with "redis: …" — the redis container is still bootstrapping.
make up --waitwaits for healthchecks; if it times out, increasestart_periodin the gateway service or inspectmake logs-redis. -
Login form rejects every code — confirm
BACKEND_AUTH_DEV_FIXED_CODEis set intools/local-dev/.envand the backend has been recreated since the last edit (docker compose up -d backend). Real Mailpit codes work regardless. -
UI talks to old gateway: Vite caches
import.meta.envat boot. Restartpnpm devafter editingui/frontend/.env.development.local. -
Port 8080 already in use (or any other host-port in the stack — postgres
5433, redis6380, mailpit8025, gateway REST8080, gateway gRPC9090) — each host-port mapping indocker-compose.ymlis parameterised throughLOCAL_DEV_*_PORTwith the listed values as defaults. Set a non-conflicting value either by uncommenting / editing the entry intools/local-dev/.env, by exporting the variable in your shell, or by dropping a local override into atools/local-dev/docker-compose.override.yml(compose auto-merges that file and it stays untracked by git). When moving the gateway REST port off8080, also point the Vite dev server at the new host port viaVITE_DEV_PROXY_TARGET=http://localhost:<port>inui/frontend/.env.development.local(or exported perpnpm devinvocation).
Relationship to other infrastructure
tools/dev-deploy/— long-lived dev environment redeployed on every merge intodevelopment; reachable at the single originhttps://galaxy.lan(site at/, game UI at/game/). Distinct compose project, container names, network and volumes.integration/testenv/— testcontainers harness used bymake -C integration integration. Uses the canonicalbackend/Dockerfile/gateway/Dockerfileat production defaults; do not confuse with this local-dev stack, which carries alpine-runtime images for ergonomics and the dev-mode auth override.