Local dev
hearth up starts the local Docker Compose stack defined in hearth/compose.yaml. Everything talks over a single shared Docker network called katforge, fronted by Traefik with TLS.
Services
| Service | Image | Role |
|---|---|---|
traefik | traefik:v3.1 | Ingress, TLS, host routing |
db | postgres:16-alpine | Postgres 16, dev data only |
api | built from ../api.katforge.com | Symfony API |
cron | built from ../api.katforge.com | Background commands (Stumper challenge gen, lobby cleanup) |
pgadmin | dpage/pgadmin4:latest | Pre-wired pgAdmin pointing at db |
mercure | dunglas/mercure:v0.16 | SSE hub for realtime fan-out |
deps | oven/bun:1 | One-shot bun install for the workspace |
katforge | node:22 | Nuxt dev server |
stumper | oven/bun:1 | Vite dev server |
lextris | oven/bun:1 | Vite dev server |
geargoblins | oven/bun:1 | Vite dev server |
The frontend services depend on deps having completed successfully — that runs bun install for the entire workspace once per hearth up so subsequent frontends start instantly.
flowchart LR
deps["deps<br/>bun install"]
db["db · mercure · traefik"]
api["api · cron"]
front["katforge · stumper · lextris · geargoblins"]
db --> api --> front
deps --> front
classDef infra fill:#0f172a,stroke:#334155,color:#cbd5e1
classDef hot fill:#0f1f0f,stroke:#16a34a,color:#86efac
class db,api infra
class deps,front hotThe katforge Docker network
All services join an external network called katforge. App stacks outside this repo can attach to it to reach the API and database without exposing host ports.
networks:
katforge:
external: true
name: katforge
services:
ui:
networks:
- katforge
hearth up creates the network if it doesn't exist; hearth down does not delete it (other compose stacks might still need it).
Env injection
Every env var on every service is marked required in compose.yaml:
environment:
APP_SECRET: ${APP_SECRET:?required}
DATABASE_URL: ${DATABASE_URL:?required}
OAUTH_DISCORD_ID: ${OAUTH_DISCORD_ID:?required}
# ...
hearth up decrypts hearth/env/<stage>/*.enc.yaml with sops, merges with the plaintext *.yaml siblings, and exports every key into the process environment before invoking compose. Missing values surface as a fast failure rather than silently defaulting to empty strings.
The
:?form fails if the var is empty or undefined. The?form (no colon) only fails if the var is undefined — empty is fine. The compose file uses both depending on whether the empty case is meaningful (e.g. an optional OAuth provider).
Persistent volumes
| Volume | Holds |
|---|---|
db_data | Postgres data |
mercure_data | Mercure bolt transport (SSE history) |
pgadmin_data | pgAdmin config |
bun_modules | Workspace node_modules for the frontends |
katforge_modules | Nuxt's node_modules |
Test accounts
All accounts use password abc123. The dev stage seeds these on first boot.
| Username | Purpose | |
|---|---|---|
admin@test.local | admin | Has characters, populated |
user@test.local | user | Empty state, new player |
tmarcus@katforge.com | tmarcus | Dev account |
Direct port mappings
In addition to Traefik routing, each service publishes a host port for direct access (useful when bypassing the proxy):
| Service | Internal | External |
|---|---|---|
| API | api:8000 | localhost:8100 |
| Postgres | db:5432 | localhost:5433 |
| Mercure | mercure:80 | localhost:8181 |
| pgAdmin | pgadmin:80 | localhost:5050 |
Resetting state
hearth down
docker volume rm katforge-hearth_db_data # nuke the DB
docker volume rm katforge-hearth_mercure_data # reset SSE history
hearth up
Resetting mercure_data is a useful trick when SSE replay history gets weird; subscribers reconnect fresh.