chore(ci): tidy CI/dev infra — drop local-ci, lift migration rule, scope by galaxy.stack label
Tests · Go / test (push) Successful in 2m6s
Tests · Go / test (pull_request) Successful in 3m1s
Tests · Integration / integration (pull_request) Successful in 1m42s

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>
This commit is contained in:
Ilia Denisov
2026-05-18 23:32:42 +02:00
parent 5eec7013ba
commit a9087691a3
23 changed files with 325 additions and 532 deletions
+6 -3
View File
@@ -4,6 +4,7 @@
REPO_ROOT := $(realpath $(CURDIR)/../..)
ENGINE_IMAGE := galaxy-engine:dev
STACK_LABEL := galaxy.stack=dev-deploy
ENGINE_LABEL := org.opencontainers.image.title=galaxy-game-engine
# Game-state root lives under the invoking user's home by default so
# `make up` works without sudo. Override `GALAXY_DEV_GAME_STATE_DIR`
@@ -93,12 +94,14 @@ psql:
clean-data:
@echo "Stopping containers and engines, then wiping volumes + game-state…"
@ids=$$(docker ps -aq --filter label=$(ENGINE_LABEL)); \
$(COMPOSE) down -v
@ids=$$(docker ps -aq \
--filter "label=$(STACK_LABEL)" \
--filter "label=$(ENGINE_LABEL)"); \
if [ -n "$$ids" ]; then \
echo "stopping engine containers"; \
echo "stopping engine containers for $(STACK_LABEL)"; \
docker rm -f $$ids >/dev/null; \
fi
$(COMPOSE) down -v
@if [ -d "$(GALAXY_DEV_GAME_STATE_DIR)" ]; then \
echo "wiping $(GALAXY_DEV_GAME_STATE_DIR)"; \
docker run --rm -v "$(GALAXY_DEV_GAME_STATE_DIR):/state" alpine sh -c 'rm -rf /state/*' 2>/dev/null \
+28 -8
View File
@@ -135,17 +135,20 @@ 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.
Until the pre-production migration rule is lifted, every
backward-incompatible change to `backend/internal/postgres/migrations/00001_init.sql`
needs a manual wipe before the next deploy succeeds:
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
```
This is the same caveat as `tools/local-dev/`, just with a different
volume name.
The same volume-persistence model applies to `tools/local-dev/`.
## Make targets
@@ -183,13 +186,30 @@ See [`KNOWN-ISSUES.md`](KNOWN-ISSUES.md) for symptoms that surface
in the long-lived dev environment but are not yet fixed (currently:
the sandbox game flipping to `cancelled` after a redispatch).
## 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.
## Relationship to other infrastructure
- `tools/local-dev/` — single-developer playground, host-port mapped,
Vite dev server on the side. Recommended for active UI work.
- `tools/local-ci/` — Gitea + act runner for **fallback** workflow
testing without `gitea.lan`. Optional, not part of the per-stage CI
gate anymore.
- `.gitea/workflows/dev-deploy.yaml` — the CI side of this stack:
builds images, seeds the UI volume, runs `docker compose up -d` on
every merge into `development`. The Makefile in this directory is
+21
View File
@@ -22,6 +22,8 @@ services:
image: postgres:16-alpine
container_name: galaxy-dev-postgres
restart: unless-stopped
labels:
galaxy.stack: dev-deploy
environment:
POSTGRES_USER: galaxy
POSTGRES_PASSWORD: galaxy
@@ -41,6 +43,8 @@ services:
image: redis:7-alpine
container_name: galaxy-dev-redis
restart: unless-stopped
labels:
galaxy.stack: dev-deploy
command:
- redis-server
- --requirepass
@@ -62,6 +66,8 @@ services:
image: axllent/mailpit:v1.21
container_name: galaxy-dev-mailpit
restart: unless-stopped
labels:
galaxy.stack: dev-deploy
networks:
- galaxy-internal
healthcheck:
@@ -78,6 +84,8 @@ services:
image: galaxy/backend:dev
container_name: galaxy-dev-backend
restart: unless-stopped
labels:
galaxy.stack: dev-deploy
user: "0:0"
depends_on:
galaxy-postgres:
@@ -94,6 +102,7 @@ services:
BACKEND_SMTP_FROM: "galaxy-backend@galaxy.lan"
BACKEND_SMTP_TLS_MODE: none
BACKEND_DOCKER_NETWORK: galaxy-dev-internal
BACKEND_STACK_LABEL: dev-deploy
BACKEND_GAME_STATE_ROOT: ${GALAXY_DEV_GAME_STATE_DIR}
BACKEND_GEOIP_DB_PATH: /var/lib/galaxy/geoip.mmdb
BACKEND_NOTIFICATION_ADMIN_EMAIL: admin@galaxy.lan
@@ -152,6 +161,8 @@ services:
image: galaxy/gateway:dev
container_name: galaxy-dev-api
restart: unless-stopped
labels:
galaxy.stack: dev-deploy
depends_on:
galaxy-backend:
condition: service_healthy
@@ -209,6 +220,8 @@ services:
image: caddy:2.11.2-alpine
container_name: galaxy-dev-caddy
restart: unless-stopped
labels:
galaxy.stack: dev-deploy
depends_on:
galaxy-api:
condition: service_healthy
@@ -225,6 +238,8 @@ networks:
name: galaxy-dev-internal
driver: bridge
internal: false
labels:
galaxy.stack: dev-deploy
edge:
name: ${GALAXY_EDGE_NETWORK:-edge}
external: true
@@ -232,7 +247,13 @@ networks:
volumes:
galaxy-dev-postgres-data:
name: galaxy-dev-postgres-data
labels:
galaxy.stack: dev-deploy
galaxy-dev-caddy-data:
name: galaxy-dev-caddy-data
labels:
galaxy.stack: dev-deploy
galaxy-dev-ui-dist:
name: galaxy-dev-ui-dist
labels:
galaxy.stack: dev-deploy
-1
View File
@@ -1 +0,0 @@
.env
-42
View File
@@ -1,42 +0,0 @@
.PHONY: help up down logs status clean push
.DEFAULT_GOAL := help
COMPOSE := docker compose
GITEA_USER := galaxy
GITEA_PASS := galaxy-dev
REPO_NAME := galaxy
REMOTE_NAME := local-gitea
REPO_ROOT := $(realpath $(CURDIR)/../..)
GIT := git -C $(REPO_ROOT)
REMOTE_URL := http://$(GITEA_USER):$(GITEA_PASS)@localhost:3000/$(GITEA_USER)/$(REPO_NAME).git
help:
@echo "Local Gitea CI for galaxy:"
@echo " make up Bring up Gitea + runner (idempotent)"
@echo " make down Stop both containers"
@echo " make logs Tail logs"
@echo " make status Show container status"
@echo " make push Push current branch to local Gitea"
@echo " make clean Stop and wipe all local state"
up:
@./bootstrap.sh
down:
$(COMPOSE) down
logs:
$(COMPOSE) logs -f --tail=50
status:
$(COMPOSE) ps
push:
@$(GIT) remote get-url $(REMOTE_NAME) >/dev/null 2>&1 || \
$(GIT) remote add $(REMOTE_NAME) $(REMOTE_URL)
$(GIT) push $(REMOTE_NAME) HEAD
clean:
$(COMPOSE) down -v
rm -f .env
-106
View File
@@ -1,106 +0,0 @@
# Local Gitea CI (fallback)
> **Status:** fallback / opt-in. The primary CI target is now
> `gitea.lan` with its host-mode `act_runner`. The per-stage CI gate
> closes against `gitea.lan`, not against this stack. Use this
> directory when you want to validate `.gitea/workflows/*` without
> reaching `gitea.lan` — for example, when iterating on a workflow
> file from a flight without LAN access — or when isolating a runner
> issue from production-shaped infrastructure.
Self-contained Gitea + Actions runner for verifying
`.gitea/workflows/*` honestly before pushing to `gitea.lan`. Runs
natively on arm64 (Apple Silicon) — every image below has an arm64
variant, so Docker pulls the right architecture and the runner
executes workflow steps without QEMU emulation.
## Prerequisites
- Docker (Colima or Docker Desktop)
- `python3`, `curl`, `bash` — all built into macOS
## First time
```sh
make -C tools/local-ci up
```
This:
1. brings up the Gitea container;
2. creates an admin user (`galaxy` / `galaxy-dev`);
3. creates the `galaxy/galaxy` repo;
4. fetches a runner registration token from the Gitea API;
5. brings up the runner with that token (the runner persists its
credentials in a Docker volume and ignores the token on subsequent
restarts).
The script is idempotent — re-running it is safe.
## Pushing a branch
```sh
make -C tools/local-ci push
```
This adds a `local-gitea` remote on the first run and then pushes the
current `HEAD`. Equivalent manual flow:
```sh
git remote add local-gitea \
http://galaxy:galaxy-dev@localhost:3000/galaxy/galaxy.git
git push local-gitea HEAD
```
The Tier 1 workflow fires on `push` to any branch and the Tier 2
workflow fires on tags matching `v*`. Watch runs at:
<http://localhost:3000/galaxy/galaxy/actions>
## Operational targets
| Target | What it does |
| ---------------- | -------------------------------------------- |
| `make up` | Bring up Gitea + runner (idempotent) |
| `make down` | Stop both containers (state preserved) |
| `make logs` | Tail logs from both containers |
| `make status` | Show container status |
| `make push` | Push current `HEAD` to local Gitea |
| `make clean` | Stop and wipe all local state (full reset) |
## What's in the box
| Component | Image | Role |
| ---------- | ---------------------------------- | ------------------------------------------- |
| Gitea | `gitea/gitea:1.23` | Server with SQLite backend |
| act_runner | `gitea/act_runner:0.6.1` | Single-capacity runner registered on boot |
| Workflow | `catthehacker/ubuntu:act-latest` | Image spawned per job (multi-arch) |
The runner mounts the host Docker socket and spawns workflow
containers on the same Docker network as Gitea, so
`actions/checkout` reaches the server at `http://gitea:3000` from
inside spawned containers.
## Caveats
- Gitea's `ROOT_URL` is set to `http://gitea:3000/` so spawned
workflow containers reach the server through the compose network.
The web UI works at `http://localhost:3000` via port mapping, but
copy-paste URLs in the UI may show `gitea:3000` instead of
`localhost:3000`. Harmless for local dev; switch the host part by
hand when copying.
- The runner is single-capacity (`runner.capacity: 1` in
`config.yaml`). Concurrent jobs queue. Bump if you need parallel
jobs.
- First push from a fresh checkout uploads the full repo history
(~tens of MB). Subsequent pushes are deltas.
- `actions/upload-artifact@v4` requires Gitea ≥ 1.21 — we pin
`1.23` to stay above the cutoff.
- Workflow steps run as `root` inside the spawned container; this
matches the upstream catthehacker behaviour. Keep that in mind if
you add steps that touch host-mounted directories.
- On Apple Silicon the runner image and its catthehacker child run
natively as arm64. Some pre-built tools that ship in the image are
amd64-only and would fall back to QEMU; `setup-go`, `setup-node`,
and `pnpm/action-setup` all download arm64 binaries themselves, so
the workflow steps we care about stay native.
-86
View File
@@ -1,86 +0,0 @@
#!/usr/bin/env bash
# Bring up Gitea, create the admin user and the galaxy/galaxy repo,
# fetch a runner registration token, bring up the runner.
# Idempotent — re-runnable.
set -euo pipefail
cd "$(dirname "$0")"
GITEA_USER=galaxy
GITEA_PASS=galaxy-dev
GITEA_EMAIL=galaxy@local
REPO_NAME=galaxy
GITEA_URL=http://localhost:3000
echo ">>> Bringing up Gitea..."
docker compose up -d gitea
echo ">>> Waiting for Gitea API..."
for _ in $(seq 1 120); do
if curl -fsS "${GITEA_URL}/api/v1/version" >/dev/null 2>&1; then
echo "Gitea is up."
break
fi
sleep 1
done
if ! curl -fsS "${GITEA_URL}/api/v1/version" >/dev/null 2>&1; then
echo "Gitea did not come up within 120 seconds." >&2
docker compose logs gitea | tail -30 >&2
exit 1
fi
echo ">>> Creating admin user (idempotent)..."
docker compose exec -T gitea su git -c "
gitea admin user create \
--username ${GITEA_USER} \
--password ${GITEA_PASS} \
--email ${GITEA_EMAIL} \
--admin \
--must-change-password=false 2>&1 || true
"
echo ">>> Creating repo (idempotent)..."
HTTP_CODE=$(curl -s -o /dev/null -w '%{http_code}' \
-u "${GITEA_USER}:${GITEA_PASS}" \
-H "Content-Type: application/json" \
-d "{\"name\":\"${REPO_NAME}\",\"private\":true,\"auto_init\":false}" \
"${GITEA_URL}/api/v1/user/repos")
case "${HTTP_CODE}" in
201) echo "Repo created." ;;
409) echo "Repo already exists." ;;
*)
echo "Unexpected response (${HTTP_CODE}) creating repo." >&2
exit 1
;;
esac
echo ">>> Fetching runner registration token..."
RUNNER_TOKEN=$(curl -fsS \
-u "${GITEA_USER}:${GITEA_PASS}" \
"${GITEA_URL}/api/v1/admin/runners/registration-token" \
| python3 -c "import json, sys; print(json.load(sys.stdin)['token'])")
# act_runner uses RUNNER_TOKEN only on the first boot. After registration
# it persists credentials in the named runner-data volume (/data/.runner)
# and ignores the env token on subsequent restarts. Writing a fresh token
# every time is harmless.
echo "RUNNER_TOKEN=${RUNNER_TOKEN}" > .env
echo ">>> Bringing up runner..."
docker compose up -d runner
cat <<EOF
Done.
Push the current branch and watch a run:
cd $(cd ../.. && pwd)
git remote add local-gitea http://${GITEA_USER}:${GITEA_PASS}@localhost:3000/${GITEA_USER}/${REPO_NAME}.git 2>/dev/null || true
git push local-gitea HEAD
open http://localhost:3000/${GITEA_USER}/${REPO_NAME}/actions
Or use \`make push\` from this directory.
EOF
-35
View File
@@ -1,35 +0,0 @@
# act_runner configuration.
#
# The `ubuntu-latest` label is mapped to catthehacker/ubuntu:act-latest,
# which is multi-arch — Docker on Apple Silicon pulls the arm64 variant
# and runs it natively (no QEMU). The same image is what `act` uses
# locally, so workflows behave the same.
log:
level: info
runner:
file: /data/.runner
capacity: 1
fetch_timeout: 5s
fetch_interval: 2s
labels:
- "ubuntu-latest:docker://catthehacker/ubuntu:act-latest"
cache:
enabled: true
dir: /data/cache
container:
# Spawned workflow containers join the same network as Gitea so
# actions/checkout and other steps can reach the server at
# http://gitea:3000.
network: galaxy-local-gitea-net
privileged: false
options: ""
workdir_parent: ""
valid_volumes: []
force_pull: false
host:
workdir_parent: ""
@@ -1,16 +0,0 @@
# Local-only override: this developer's host already runs another
# Gitea instance bound to 0.0.0.0:3000 and 0.0.0.0:2222, so the
# default port mappings in docker-compose.yml conflict. Remap the
# local-ci Gitea to 13000 (HTTP) and 12222 (SSH) on the host. The
# in-network ports stay 3000 / 22 — runners and workflow containers
# keep reaching Gitea by hostname through the compose network.
#
# This file is intentionally NOT committed to the repo; it captures
# per-host port allocation. Use `make -C tools/local-ci push` only
# after pointing the `local-gitea` git remote at the override port.
services:
gitea:
ports: !override
- "13000:3000"
- "12222:22"
-78
View File
@@ -1,78 +0,0 @@
# Local Gitea + Actions runner for verifying .gitea/workflows/*.
# Runs natively on arm64 (Apple Silicon) — every image below is multi-arch.
#
# Browser: http://localhost:3000
# API: http://localhost:3000/api/v1
# Push URL: http://galaxy:galaxy-dev@localhost:3000/galaxy/galaxy.git
# Actions: http://localhost:3000/galaxy/galaxy/actions
#
# `bootstrap.sh` (or `make up`) brings everything up and registers the
# runner. State persists in named Docker volumes; `make clean` wipes them.
services:
gitea:
image: gitea/gitea:1.23
container_name: galaxy-local-gitea
restart: unless-stopped
environment:
USER_UID: "1000"
USER_GID: "1000"
GITEA__database__DB_TYPE: sqlite3
GITEA__database__PATH: /data/gitea/gitea.db
# ROOT_URL uses the in-network hostname so the runner and spawned
# workflow containers reach Gitea through the compose network.
# The browser still works at http://localhost:3000 via the port
# mapping below; UI-generated copy URLs may show "gitea:3000",
# which is harmless for local dev.
GITEA__server__ROOT_URL: http://gitea:3000/
GITEA__server__SSH_PORT: "2222"
GITEA__actions__ENABLED: "true"
GITEA__security__INSTALL_LOCK: "true"
GITEA__service__DISABLE_REGISTRATION: "true"
ports:
- "3000:3000"
- "2222:22"
volumes:
- gitea-data:/data
networks:
- gitea-net
healthcheck:
test:
- CMD-SHELL
- wget -q -O- http://localhost:3000/api/v1/version >/dev/null || exit 1
interval: 5s
timeout: 3s
retries: 30
start_period: 5s
runner:
image: gitea/act_runner:0.6.1
container_name: galaxy-local-runner
restart: unless-stopped
depends_on:
gitea:
condition: service_healthy
environment:
CONFIG_FILE: /config/config.yaml
GITEA_INSTANCE_URL: http://gitea:3000
# Provided by bootstrap.sh in the .env file. After the first
# successful registration, act_runner persists credentials in
# /data/.runner and ignores this token on subsequent restarts.
GITEA_RUNNER_REGISTRATION_TOKEN: ${RUNNER_TOKEN:-}
GITEA_RUNNER_NAME: galaxy-local
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- runner-data:/data
- ./config.yaml:/config/config.yaml:ro
networks:
- gitea-net
networks:
gitea-net:
name: galaxy-local-gitea-net
volumes:
gitea-data:
name: galaxy-local-gitea-data
runner-data:
name: galaxy-local-runner-data
+17 -6
View File
@@ -5,9 +5,16 @@
COMPOSE := docker compose
REPO_ROOT := $(realpath $(CURDIR)/../..)
ENGINE_IMAGE := galaxy-engine:local-dev
# Label set by the engine `Dockerfile` runtime stage; used to find
# engine containers spawned by backend's runtime that fall outside
# `docker compose down`'s scope.
# Engine containers spawned by backend's runtime fall outside the
# compose project. We identify them by two labels:
# STACK_LABEL — backend stamps this on every engine it spawns from
# this stack (see BACKEND_STACK_LABEL env in the
# compose file);
# ENGINE_LABEL — image-level OCI title baked into the engine
# Dockerfile.
# Both filters together select exactly this stack's engine containers
# and never compose-managed services or unrelated workloads.
STACK_LABEL := galaxy.stack=local-dev
ENGINE_LABEL := org.opencontainers.image.title=galaxy-game-engine
help:
@@ -65,9 +72,11 @@ clean: stop-engines
# cascade the game to `cancelled`. We only remove them as part of
# `clean`, where the whole DB is wiped anyway.
stop-engines:
@ids=$$(docker ps -aq --filter label=$(ENGINE_LABEL)); \
@ids=$$(docker ps -aq \
--filter "label=$(STACK_LABEL)" \
--filter "label=$(ENGINE_LABEL)"); \
if [ -n "$$ids" ]; then \
echo "stopping engine containers"; \
echo "stopping engine containers for $(STACK_LABEL)"; \
docker rm -f $$ids >/dev/null; \
fi
@@ -87,7 +96,9 @@ stop-engines:
# cycles.
prune-broken-engines:
@ids=""; \
for cid in $$(docker ps -aq --filter label=$(ENGINE_LABEL) 2>/dev/null); do \
for cid in $$(docker ps -aq \
--filter "label=$(STACK_LABEL)" \
--filter "label=$(ENGINE_LABEL)" 2>/dev/null); do \
state=$$(docker inspect -f '{{.State.Status}}' $$cid 2>/dev/null); \
case "$$state" in \
running|restarting) ;; \
+13 -12
View File
@@ -15,10 +15,10 @@ This stack is **not** a CI gate (the per-stage CI gate now lives on
the **long-lived dev environment** at
[`tools/dev-deploy/`](../dev-deploy/README.md), which is redeployed on
every merge into `development` and is reachable as
`https://www.galaxy.lan` / `https://api.galaxy.lan`. The three stacks
(`tools/local-dev/`, `tools/dev-deploy/`, and the fallback
`tools/local-ci/`) coexist on the same host because every name —
compose project, container, network, volume — is distinct.
`https://www.galaxy.lan` / `https://api.galaxy.lan`. 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
@@ -203,8 +203,8 @@ make status docker compose ps
images built on alpine (so `wget` is available for the compose
healthchecks). The build stage mirrors `backend/Dockerfile` and
`gateway/Dockerfile` exactly.
- `Makefile` — wrapper over `docker compose` that keeps the muscle
memory close to `tools/local-ci/`'s Makefile.
- `Makefile` — wrapper over `docker compose` with 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
@@ -290,12 +290,13 @@ make status docker compose ps
## Relationship to other infrastructure
- `tools/local-ci/` — Gitea + Actions runner, replays
`.gitea/workflows/*` against a pushed branch. Different stack,
different purpose; coexists with local-dev on the same machine.
- `tools/dev-deploy/` — long-lived dev environment redeployed on every
merge into `development`; reachable at `https://www.galaxy.lan` /
`https://api.galaxy.lan`. Distinct compose project, container names,
network and volumes.
- `integration/testenv/` — testcontainers harness used by
`make -C integration integration`. Uses the same images
(`backend/Dockerfile`, `gateway/Dockerfile`) at production
defaults; do not confuse with this local-dev stack, which carries
`make -C integration integration`. Uses the canonical
`backend/Dockerfile` / `gateway/Dockerfile` at production defaults;
do not confuse with this local-dev stack, which carries
alpine-runtime images for ergonomics and the dev-mode auth
override.
+17
View File
@@ -19,11 +19,15 @@
# 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
@@ -45,6 +49,8 @@ services:
image: redis:7-alpine
container_name: galaxy-local-dev-redis
restart: unless-stopped
labels:
galaxy.stack: local-dev
command:
- redis-server
- --requirepass
@@ -68,6 +74,8 @@ services:
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:
@@ -86,6 +94,8 @@ services:
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:
@@ -102,6 +112,7 @@ services:
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
@@ -144,6 +155,8 @@ services:
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
@@ -205,7 +218,11 @@ services:
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