Skip to content

Development Environment

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:

  1. Local Docker — your laptop runs everything. Lowest setup; highest hardware demand.
  2. Codespaces — GitHub-hosted devcontainer. Zero setup; the URL changes per session.
  3. 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 a version: field — it relies on modern Compose behavior.
  • A .env file at the repo root. Start from Sectorwars2102/.env.example and 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.
  • 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' — the heavy lifting is in dev-scripts/setup.sh, not the devcontainer hook.

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/ holds the unified entry points.

Script Purpose
start-unified.sh Detects environment, optionally switches between development/production/test env files, then runs docker compose up. Accepts --no-host-check and --production-db.
setup.sh Idempotent first-run setup: Node + npm, Python deps, frontend installs, .env scaffolding.
startup-health-check.sh Pings each service after start to verify they are healthy.
reset-first-login.sh Clears the first-login flag on a player so you can replay the AI dialogue flow.
debug-player-auth.js Standalone Node script that exercises the player auth flow end-to-end.
vm-start.sh / vm-stop.sh / vm-sync.sh Remote-host lifecycle helpers (see "Remote dev host" below). The host's address and any access credentials live in gitignored files.

The README at Sectorwars2102/dev-scripts/README.md documents script-level behavior.


Common workflows

Bring the stack up (default profile)

docker compose up                 # foreground
docker compose up -d              # detached
./dev-scripts/start-unified.sh    # equivalent, with environment auto-detection

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"

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 same compose stack supports multiple isolated tiers (e.g. dev and stage) on the same host by combining a base file with a per-tier override and a unique project name.

# Dev tier
docker compose -p sw2102-dev \
  -f docker-compose.yml -f docker-compose.dev.yml up -d

# Stage tier (different ports, no source mounts, separate volumes)
docker compose -p sw2102-stage \
  -f docker-compose.yml -f docker-compose.stage.yml up -d

Conventions:

  • -p sw2102-<tier> namespaces containers, networks, and volumes — tiers cannot collide.
  • Each tier has its own .env (with its own DB password, JWT secret, OAuth client_id/secret).
  • Each tier binds host ports in its own range (e.g. dev = 80xx, stage = 90xx) so both can run simultaneously.
  • Dev mounts ./<service>/src into containers for hot reload; stage and prod build images and don't mount source.

When tiers live on different hosts (dev/stage on one host, prod on another), the project-name namespacing still applies — the host topology is described in the operator-side hosting plan, which lives outside this repo.


URLs at a glance (default ports, dev tier)

For the stage tier, shift each port by +1000 (3000 → 4000, 8080 → 9080, etc.) — the per-tier override file owns those mappings.


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:

  1. Connect over a private overlay network (Tailscale, WireGuard, ZeroTier, or a VPN of your choice). The host should not be publicly reachable.
  2. VS Code Remote-SSH to the host. The editor backend, language servers, and integrated terminal all run remotely; your laptop is just the UI.
  3. Run Compose on the host with the multi-tier layering above. Bring up sw2102-dev for active work, sw2102-stage for invited-tester previews.
  4. Bind container ports to 127.0.0.1 on the host (not 0.0.0.0). The overlay network reaches them; the public internet does not.
  5. 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.

The vm-start.sh / vm-stop.sh / vm-sync.sh helpers in dev-scripts/ automate this lifecycle for the team's preferred remote host. The host address itself lives in gitignored files; clone those locally to your dev-scripts/ directory if they're shared with you out-of-band.

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_secret leaks 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

  • gameserver won't start, complains about JWT_SECRET: your JWT_SECRET is missing or shorter than 32 characters. Generate one with openssl rand -hex 32.
  • /docs returns 404: DEBUG is not true. 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_PORT in .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_modules volume; docker compose down -v and bring back up.
  • OAuth redirect loop in Codespaces: API_BASE_URL / FRONTEND_URL may have stale values. Empty them in .env so detect_environment() can reconstruct them from CODESPACE_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:8080 to 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.