Files
galaxy-game/tools/local-dev/docker-compose.yml
T
Ilia Denisov a9087691a3
Tests · Go / test (push) Successful in 2m6s
Tests · Go / test (pull_request) Successful in 3m1s
Tests · Integration / integration (pull_request) Successful in 1m42s
chore(ci): tidy CI/dev infra — drop local-ci, lift migration rule, scope by galaxy.stack label
Five connected cleanups across the dev/CI infrastructure:

1. Drop tools/local-ci/. The standalone Gitea + act_runner stack was
   the legacy "offline workflow validator"; the per-stage CI gate now
   runs on gitea.lan and the directory was only retained as a
   fallback. Removing it leaves no operational dependency: backend,
   gateway, and game code have no references; documentation that
   pointed at it (CLAUDE.md, docs/ARCHITECTURE.md, ui/docs/testing.md,
   tools/dev-deploy/README.md, tools/local-dev/README.md) is updated
   in this same change. Historical "Verified on local-ci run N"
   markers in ui/PLAN.md are preserved unchanged.

2. Lift the pre-production single-migration rule. The rule forced
   every schema delta into 00001_init.sql and required a manual
   make clean-data wipe on every backward-incompatible change in
   tools/dev-deploy/. Future schema deltas now land as additive
   sequence-numbered files (00002_*.sql, …) that goose applies
   automatically on backend startup; 00001_init.sql becomes an
   immutable baseline. Authoring conventions live in
   backend/internal/postgres/migrations/README.md. The chain may be
   squashed back into a fresh 00001 as a deliberate one-time
   operation before the first production deployment.

3. Document the deployment cadence. The dev environment is
   single-tenant: pushes to feature/* run the test workflows
   (go-unit, ui-test, integration) only; dev-deploy.yaml fires on
   push to development. A workflow_dispatch override on
   dev-deploy.yaml lets a developer preview a feature branch on the
   shared dev environment before merge; the next merge into
   development overwrites the manual deploy idempotently.

4. Scope compose-managed resources by an explicit
   galaxy.stack=<local-dev|dev-deploy> label. Both compose files
   stamp the label on every service, network, and named volume.
   Makefiles in tools/local-dev/ and tools/dev-deploy/ filter their
   engine-cleanup operations by (stack-label AND engine OCI title)
   so they never touch unrelated workloads on the same daemon.
   dev-deploy.yaml gains a pre-`compose up` step that reaps stale
   exited/dead containers under the dev-deploy stack label.

5. Backend now stamps the same galaxy.stack=<value> label on every
   engine container it spawns, sourced from a new BACKEND_STACK_LABEL
   env var (empty → label not applied; legacy-safe). Both compose
   files set it to their stack name (local-dev / dev-deploy). The
   contract is recorded in docs/ARCHITECTURE.md under
   "Container labels". A package-level test in
   backend/internal/runtime exercises both the label-present and
   label-absent paths.

No tests intentionally regressed: go test ./backend/internal/{config,
runtime,dockerclient} is green, both compose files validate cleanly,
and the backend, gateway, and game modules all build.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 23:32:42 +02:00

229 lines
8.3 KiB
YAML

# Local development stack for the Galaxy UI.
#
# Brings up postgres + redis + mailpit + backend + gateway so the UI
# Vite dev server (run on the host with `pnpm -C ui/frontend dev`) can
# talk to a real authenticated stack without any cloud dependency.
#
# Browser: http://localhost:8080 (gateway public REST + Connect-Web)
# Mailpit UI: http://localhost:8025
# Postgres: localhost:5433 (host-mapped)
# Redis: localhost:6380 (host-mapped)
#
# Bring up: make -C tools/local-dev up
# Tear down: make -C tools/local-dev down
# Wipe state: make -C tools/local-dev clean
#
# The backend reads `BACKEND_AUTH_DEV_FIXED_CODE=123456` from the
# `.env` file alongside this compose; ConfirmEmailCode accepts that
# literal in addition to the real bcrypt-verified code, so a developer
# can log in without touching Mailpit. Real codes still arrive in
# Mailpit; both paths coexist.
name: galaxy-local-dev
services:
postgres:
image: postgres:16-alpine
container_name: galaxy-local-dev-postgres
restart: unless-stopped
labels:
galaxy.stack: local-dev
environment:
POSTGRES_USER: galaxy
POSTGRES_PASSWORD: galaxy
POSTGRES_DB: galaxy_backend
ports:
- "${LOCAL_DEV_POSTGRES_PORT:-5433}:5432"
volumes:
- postgres-data:/var/lib/postgresql/data
networks:
- galaxy-net
healthcheck:
test: ["CMD-SHELL", "pg_isready -U galaxy -d galaxy_backend"]
interval: 3s
timeout: 3s
retries: 30
start_period: 5s
redis:
image: redis:7-alpine
container_name: galaxy-local-dev-redis
restart: unless-stopped
labels:
galaxy.stack: local-dev
command:
- redis-server
- --requirepass
- galaxy-dev
- --appendonly
- "no"
- --save
- ""
ports:
- "${LOCAL_DEV_REDIS_PORT:-6380}:6379"
networks:
- galaxy-net
healthcheck:
test: ["CMD", "redis-cli", "-a", "galaxy-dev", "PING"]
interval: 3s
timeout: 3s
retries: 30
start_period: 3s
mailpit:
image: axllent/mailpit:v1.21
container_name: galaxy-local-dev-mailpit
restart: unless-stopped
labels:
galaxy.stack: local-dev
ports:
- "${LOCAL_DEV_MAILPIT_PORT:-8025}:8025"
networks:
- galaxy-net
healthcheck:
test: ["CMD", "wget", "-q", "-O-", "http://localhost:8025/livez"]
interval: 3s
timeout: 3s
retries: 30
start_period: 3s
backend:
build:
context: ../..
dockerfile: tools/local-dev/backend.Dockerfile
image: galaxy/backend:local-dev
container_name: galaxy-local-dev-backend
restart: unless-stopped
labels:
galaxy.stack: local-dev
user: "0:0"
depends_on:
postgres:
condition: service_healthy
mailpit:
condition: service_healthy
environment:
BACKEND_LOGGING_LEVEL: debug
BACKEND_HTTP_LISTEN_ADDR: ":8080"
BACKEND_GRPC_PUSH_LISTEN_ADDR: ":8081"
BACKEND_POSTGRES_DSN: "postgres://galaxy:galaxy@postgres:5432/galaxy_backend?search_path=backend&sslmode=disable"
BACKEND_SMTP_HOST: mailpit
BACKEND_SMTP_PORT: "1025"
BACKEND_SMTP_FROM: "galaxy-backend@galaxy.local"
BACKEND_SMTP_TLS_MODE: none
BACKEND_DOCKER_NETWORK: galaxy-local-dev-net
BACKEND_STACK_LABEL: local-dev
BACKEND_GAME_STATE_ROOT: /tmp/galaxy-game-state
BACKEND_GEOIP_DB_PATH: /var/lib/galaxy/geoip.mmdb
BACKEND_NOTIFICATION_ADMIN_EMAIL: admin@galaxy.local
BACKEND_AUTH_CHALLENGE_THROTTLE_MAX: "100"
BACKEND_MAIL_WORKER_INTERVAL: 500ms
BACKEND_NOTIFICATION_WORKER_INTERVAL: 500ms
BACKEND_OTEL_TRACES_EXPORTER: none
BACKEND_OTEL_METRICS_EXPORTER: none
BACKEND_AUTH_DEV_FIXED_CODE: ${BACKEND_AUTH_DEV_FIXED_CODE:-}
BACKEND_DEV_SANDBOX_EMAIL: ${BACKEND_DEV_SANDBOX_EMAIL:-}
BACKEND_DEV_SANDBOX_ENGINE_IMAGE: ${BACKEND_DEV_SANDBOX_ENGINE_IMAGE:-}
BACKEND_DEV_SANDBOX_ENGINE_VERSION: ${BACKEND_DEV_SANDBOX_ENGINE_VERSION:-}
BACKEND_DEV_SANDBOX_PLAYER_COUNT: ${BACKEND_DEV_SANDBOX_PLAYER_COUNT:-}
volumes:
- /var/run/docker.sock:/var/run/docker.sock
# Per-game state directories live under the same absolute path
# both inside the backend container and on the Docker daemon
# host (colima VM), so the bind-mount source the backend hands
# to the daemon resolves correctly when spawning engine
# containers. See backend/internal/runtime/service.go:454.
- type: bind
source: /tmp/galaxy-game-state
target: /tmp/galaxy-game-state
bind:
create_host_path: true
- ../../pkg/geoip/test-data/test-data/GeoIP2-Country-Test.mmdb:/var/lib/galaxy/geoip.mmdb:ro
networks:
- galaxy-net
healthcheck:
test: ["CMD", "wget", "-q", "-O-", "http://localhost:8080/healthz"]
interval: 3s
timeout: 3s
retries: 60
start_period: 10s
gateway:
build:
context: ../..
dockerfile: tools/local-dev/gateway.Dockerfile
image: galaxy/gateway:local-dev
container_name: galaxy-local-dev-gateway
restart: unless-stopped
labels:
galaxy.stack: local-dev
depends_on:
backend:
condition: service_healthy
redis:
condition: service_healthy
environment:
GATEWAY_LOG_LEVEL: debug
GATEWAY_PUBLIC_HTTP_ADDR: ":8080"
GATEWAY_AUTHENTICATED_GRPC_ADDR: ":9090"
GATEWAY_BACKEND_HTTP_URL: "http://backend:8080"
GATEWAY_BACKEND_GRPC_PUSH_URL: "backend:8081"
GATEWAY_BACKEND_GATEWAY_CLIENT_ID: local-dev-gateway-1
GATEWAY_RESPONSE_SIGNER_PRIVATE_KEY_PEM_PATH: /run/secrets/gateway-response.pem
GATEWAY_REDIS_MASTER_ADDR: "redis:6379"
GATEWAY_REDIS_PASSWORD: galaxy-dev
# Loosen anti-abuse so a developer hammering the form does not
# rate-limit themselves between cycles.
GATEWAY_PUBLIC_HTTP_ANTI_ABUSE_PUBLIC_AUTH_RATE_LIMIT_REQUESTS: "10000"
GATEWAY_PUBLIC_HTTP_ANTI_ABUSE_PUBLIC_AUTH_RATE_LIMIT_BURST: "1000"
GATEWAY_PUBLIC_HTTP_ANTI_ABUSE_SEND_EMAIL_CODE_IDENTITY_RATE_LIMIT_REQUESTS: "10000"
GATEWAY_PUBLIC_HTTP_ANTI_ABUSE_SEND_EMAIL_CODE_IDENTITY_RATE_LIMIT_BURST: "1000"
GATEWAY_PUBLIC_HTTP_ANTI_ABUSE_CONFIRM_EMAIL_CODE_IDENTITY_RATE_LIMIT_REQUESTS: "10000"
GATEWAY_PUBLIC_HTTP_ANTI_ABUSE_CONFIRM_EMAIL_CODE_IDENTITY_RATE_LIMIT_BURST: "1000"
# public_misc class wraps the authenticated EdgeGateway gRPC
# endpoints (ExecuteCommand, SubscribeEvents). The gateway's
# default for this class is 0 bytes, which rejects every
# non-empty body with HTTP 413; override with a generous limit
# so browser-side commands carrying signed envelopes go through.
GATEWAY_PUBLIC_HTTP_ANTI_ABUSE_PUBLIC_MISC_MAX_BODY_BYTES: "131072"
GATEWAY_PUBLIC_HTTP_ANTI_ABUSE_PUBLIC_MISC_RATE_LIMIT_REQUESTS: "10000"
GATEWAY_PUBLIC_HTTP_ANTI_ABUSE_PUBLIC_MISC_RATE_LIMIT_BURST: "1000"
GATEWAY_PUBLIC_HTTP_ANTI_ABUSE_BROWSER_BOOTSTRAP_MAX_BODY_BYTES: "65536"
GATEWAY_PUBLIC_HTTP_ANTI_ABUSE_BROWSER_ASSET_MAX_BODY_BYTES: "65536"
GATEWAY_AUTHENTICATED_GRPC_ANTI_ABUSE_IP_RATE_LIMIT_REQUESTS: "10000"
GATEWAY_AUTHENTICATED_GRPC_ANTI_ABUSE_IP_RATE_LIMIT_BURST: "1000"
GATEWAY_AUTHENTICATED_GRPC_ANTI_ABUSE_SESSION_RATE_LIMIT_REQUESTS: "10000"
GATEWAY_AUTHENTICATED_GRPC_ANTI_ABUSE_SESSION_RATE_LIMIT_BURST: "1000"
GATEWAY_AUTHENTICATED_GRPC_ANTI_ABUSE_USER_RATE_LIMIT_REQUESTS: "10000"
GATEWAY_AUTHENTICATED_GRPC_ANTI_ABUSE_USER_RATE_LIMIT_BURST: "1000"
GATEWAY_AUTHENTICATED_GRPC_ANTI_ABUSE_MESSAGE_CLASS_RATE_LIMIT_REQUESTS: "10000"
GATEWAY_AUTHENTICATED_GRPC_ANTI_ABUSE_MESSAGE_CLASS_RATE_LIMIT_BURST: "1000"
ports:
- "${LOCAL_DEV_GATEWAY_REST_PORT:-8080}:8080"
# Authenticated EdgeGateway connect-web/gRPC listener. The
# browser reaches it via the Vite dev proxy in
# ui/frontend/vite.config.ts.
- "${LOCAL_DEV_GATEWAY_GRPC_PORT:-9090}:9090"
volumes:
- ./keys/gateway-response.pem:/run/secrets/gateway-response.pem:ro
networks:
- galaxy-net
healthcheck:
test: ["CMD", "wget", "-q", "-O-", "http://localhost:8080/healthz"]
interval: 3s
timeout: 3s
retries: 30
start_period: 5s
networks:
galaxy-net:
name: galaxy-local-dev-net
labels:
galaxy.stack: local-dev
volumes:
postgres-data:
name: galaxy-local-dev-postgres-data
labels:
galaxy.stack: local-dev