Development Environment¶
Status: 🚧 Partial — devcontainer, Codespaces URL detection, Playwright, and core docker compose commands are all in place;
dev-scripts/tooling is gitignored and no longer ships in this repo. (re-verified 2026-08-21 vs origin/feat46bce720)
How to run SectorWars 2102 for development. The contract is the same in every environment: Docker Compose brings the stack up, source is bind-mounted so edits hot-reload.
Three documented environments, in order of friction:
- Local Docker — your laptop runs everything. Lowest setup; highest hardware demand.
- Codespaces — GitHub-hosted devcontainer. Zero setup; the URL changes per session.
- Remote dev host — a Linux host (yours, the team's, or any cloud VM) accessed over SSH. Highest fidelity for OAuth and multi-tier work.
All three speak the same Compose stack with the same .env shape; only the where it runs differs.
Prerequisites¶
- Docker (Desktop or Engine) with Compose v2 (
docker compose ...). The compose file does not pin aversion:field — it relies on modern Compose behavior. - A
.envfile at the repo root. Start fromSectorwars2102/.env.exampleand fill in: DATABASE_URL(use the local container's connection string for fully-local dev).JWT_SECRET— must be at least 32 characters, otherwise the gameserver refuses to boot (services/gameserver/src/core/config.py).ADMIN_USERNAME/ADMIN_PASSWORD.- Any OAuth keys you want to test (
CLIENT_ID_GITHUB,GOOGLE_CLIENT_ID,STEAM_API_KEY, …). Use per-tier OAuth apps so dev secrets are isolated from stage / prod. - Optional: Node 18 and Python 3.11 if you want to run lint/build outside containers. The devcontainer image bundles both.
Devcontainer (Codespaces / VS Code Remote)¶
Sectorwars2102/.devcontainer/devcontainer.json defines a universal devcontainer with:
- Base image
mcr.microsoft.com/devcontainers/universal:2. - Features: Docker-in-Docker, Node 18, Python 3.11, Tailscale.
- Forwarded ports:
3000(player),3001(admin),8080(gameserver),5433(Postgres),6379(Redis),80/443(Nginx). - VS Code extensions: Python, TypeScript, Tailwind, JSON.
postCreateCommand:echo 'Devcontainer setup complete'— no external setup script runs automatically; bring the stack up yourself with thedocker composecommands below.
In Codespaces, settings.detect_environment() (in core/config.py) inspects CODESPACE_NAME / GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN and rewrites API_BASE_URL / FRONTEND_URL to the Codespaces-forwarded hostnames automatically.
dev-scripts¶
Sectorwars2102/dev-scripts/ is gitignored at the repo root (removed from tracking because the scripts carried personal VM credentials and usernames). No unified entry-point scripts (start-unified.sh, setup.sh, startup-health-check.sh, reset-first-login.sh, debug-player-auth.js, vm-*.sh) ship in this repo — bring the stack up directly with the docker compose commands below. The one file present locally under dev-scripts/ is an operator-side breadcrumb pointing at a private, out-of-repo hosting doc; it carries no setup logic and isn't referenced by anything in this repo's tracked source.
Common workflows¶
Bring the stack up (default profile)¶
docker compose up # foreground
docker compose up -d # detached
This starts: database, redis-cache, gameserver, player-client, admin-ui, nginx-gateway, region-manager.
Multi-regional topology¶
docker compose --profile multi-regional up
Adds: central-nexus-db, redis-nexus, central-nexus-server. The region-manager is shared with the dev profile.
Tear down¶
docker compose down # stop containers
docker compose down -v # also delete named volumes (DB wipe)
Inspect logs¶
docker compose logs -f gameserver
docker compose logs -f player-client admin-ui
Run commands inside containers¶
# Gameserver tests (Poetry-managed)
docker compose exec gameserver poetry run pytest
docker compose exec gameserver poetry run pytest tests/unit/
# Frontend type-check / build
docker compose exec player-client npm run build
docker compose exec admin-ui npm run lint
# Alembic migrations
docker compose exec gameserver poetry run alembic upgrade head
docker compose exec gameserver poetry run alembic revision --autogenerate -m "description"
Current head (as of 2026-07-11): b9a7404a2c20 — add unique (owner_player_id, contract_id) index on storage_lockers — see ../DATA_MODELS/migrations.md for the full revision inventory.
Dev-drift reconcile guidance: Base.metadata.create_all is removed from the gameserver's startup path per ADR-0093 item 13 — Alembic is the sole schema authority, in dev as well as stage/prod. A fresh dev DB is schema-populated by running alembic upgrade head before first boot, not by the server's own startup. If alembic current reports a revision behind head, the schema is genuinely behind — resolve it by applying migrations, never by inferring the schema is already current:
- Run
alembic history --verboseto see what migrations are between the stamped version and head. alembic upgrade head(oralembic upgrade <rev>one step at a time when verifying a risky migration, e.g. one withALTER COLUMN,DROP, or a data backfill).- Do not run
alembic stamp headto skip past unapplied migrations — stamping without applying leaves the schema itself behind even thoughalembic_versionclaims otherwise.
The CI schema-parity gate is required (blocking): a PR whose migrations don't produce a schema matching the models fails CI, so drift of this kind should surface there before it ever reaches a dev machine.
End-to-end tests¶
npx playwright test -c e2e_tests/playwright.config.ts
npx playwright test --reporter=html
Screenshots land in e2e_tests/screenshots/.
Multi-tier compose layering¶
The single docker-compose.yml selects which service set runs via Compose profiles, not per-tier override files:
development/default— the default local dev stack (database,redis-cache,gameserver,player-client,admin-ui,nginx-gateway,region-manager).multi-regional/production— addscentral-nexus-db,redis-nexus,central-nexus-server. Used both for running the multi-regional topology locally and for any non-dev deployment (stage, prod) via profile selection plus a tier-specific.env.regional-template— the reciperegion-managerclones per provisioned region; not started directly.monitoring—prometheus+grafana, layered on top ofproduction.
docker-compose.dev.yml is a narrow override, not a general per-tier file: it pins player-client's and admin-ui's VITE_API_URL / VITE_WS_URL to empty (same-origin) so a host .env copied from another environment can't leak a foreign API origin into the dev client build. It applies only to the development profile:
docker compose -f docker-compose.yml -f docker-compose.dev.yml --profile development up -d
There is no docker-compose.stage.yml or other per-tier override file in this repo. A stage or production deployment runs the same base docker-compose.yml with the multi-regional / production profile, its own .env (DB password, JWT secret, OAuth client_id/secret), and its own Compose project name (-p <name>) so containers, networks, and volumes don't collide with other tiers on a shared host. Tier-specific hostnames, ports, and access details live in the operator's private hosting docs, outside this repo.
URLs at a glance (default ports, dev tier)¶
- Player client: http://localhost:3000
- Admin UI: http://localhost:3001
- Gameserver API: http://localhost:8080/api/v1/
- Swagger / OpenAPI: http://localhost:8080/docs (only when
DEBUG=true) - Region manager: http://localhost:8081/health
- Postgres:
postgresql://postgres@localhost:5433/sectorwars_dev - Nginx (if in production-style profile): https://localhost/
Non-dev tiers (stage, prod) bind their own host ports via their own .env (each service's port is a ${..._PORT:-default} Compose variable) and their own Compose project name — there is no fixed cross-tier port-numbering convention checked into this repo.
Remote dev host (optional pattern)¶
For laptops that can't comfortably run the full stack — or when you want a stage tier on the same machine as dev — running everything on a remote Linux host is the common path. The pattern:
- Connect over a private overlay network (Tailscale, WireGuard, ZeroTier, or a VPN of your choice). The host should not be publicly reachable.
- VS Code Remote-SSH to the host. The editor backend, language servers, and integrated terminal all run remotely; your laptop is just the UI.
- Run Compose on the host with the multi-tier layering above. Bring up
sw2102-devfor active work,sw2102-stagefor invited-tester previews. - Bind container ports to
127.0.0.1on the host (not0.0.0.0). The overlay network reaches them; the public internet does not. - For OAuth in dev, use SSH local port forwarding from your laptop:
ssh -L 8080:localhost:8080 -L 8081:localhost:8081 -L 8083:localhost:8083 <dev-host>
GitHub, Google, and Steam all allow http://localhost:* callbacks. Your laptop browser hits http://localhost:8080, the SSH tunnel forwards to the dev host's container, and OAuth redirects back through localhost cleanly. The provider never directly reaches your dev host.
No lifecycle-automation scripts for this pattern ship in this repo — connect, run Compose, and tear down using the commands above directly. The host address and any access credentials live entirely outside this repo, in the operator's private hosting docs.
For Apple Sign-In (which forbids localhost callbacks), use the team's stage tier instead — its hostname is publicly reachable behind operator-defined access control.
OAuth callback URLs per tier¶
Register separate OAuth apps at each provider for each tier you run. Per-tier apps mean:
- Dev
client_id/client_secretleaks can't grant access to prod. - Independent rate limits — dev iteration churn doesn't burn prod's quota.
- Clean revocation — kill the dev app entirely without touching users.
| Tier | Callback URL pattern (GitHub example) |
|---|---|
| Local / dev (with SSH port-forward) | http://localhost:8080/auth/github/callback |
| Stage | https://<stage-host>/auth/github/callback |
| Prod | https://<prod-host>/auth/github/callback |
Steam uses OpenID 2.0 rather than OAuth 2.0 — register a Web API key per tier and configure the return_to / realm URLs analogously. Steam allows http://localhost:* for the return_to, so the SSH-port-forward pattern above works for it too.
Quick troubleshooting¶
gameserverwon't start, complains about JWT_SECRET: yourJWT_SECRETis missing or shorter than 32 characters. Generate one withopenssl rand -hex 32./docsreturns 404:DEBUGis nottrue. The OpenAPI surface is intentionally hidden in production (services/gameserver/src/main.py).- Port 5433 already in use: another Postgres instance is bound. Either stop it, or change
DB_PORTin.env. If you're running multiple tiers on one host, give each tier its own port range via the per-tier compose override. - Hot reload not working: confirm the bind mount didn't get shadowed by a stale
node_modulesvolume;docker compose down -vand bring back up. - OAuth redirect loop in Codespaces:
API_BASE_URL/FRONTEND_URLmay have stale values. Empty them in.envsodetect_environment()can reconstruct them fromCODESPACE_NAME. - OAuth callback fails on remote dev host: you forgot the SSH port forward. Open the tunnel before clicking "Login with GitHub" — the provider's redirect goes through your laptop's browser, which needs
localhost:8080to resolve to the forwarded socket. - Two tiers fighting for the same host port: each tier's compose override must pick a unique port range. Convention: dev = 80xx, stage = 90xx.