Skip to content

Running Locally

Kurt edited this page Jul 21, 2026 · 2 revisions

Running Locally

Audience: Sysadmin Status: ✅ Ready

How to stand up a TelosMUD stack on a developer workstation: what Docker Compose brings up, the make targets that drive it, the configuration surface, the ports each service listens on, the migrate + seed bootstrap, and how to connect a client. Two authentication paths are covered in their own sections near the end.

The quickest path

From a checkout, one command builds and starts the whole fleet in Docker:

make up          # build + start everything: the stack AND the Grafana LGTM backend
telnet localhost 4000

make up builds the images, brings up the backing services, runs the one-shot migrate + seed, starts the world shards, the account service, and the gate, and the Grafana LGTM observability stack (Grafana on http://localhost:3000). Once it settles, connect a telnet client to port 4000. Tear it down with make down. If you want the leaner stack without observability (no Grafana, no exporter noise — the same base CI uses), run make up-base instead.

What Compose brings up

There are two Compose topologies under deploy/, and they are meaningfully different.

Multi-shard — deploy/docker-compose.yml (+ the observability overlay)

The realistic topology: multiple world shards, an account service, and the full backing stack. make up starts this base file plus deploy/docker-compose.obs.yml (the Grafana LGTM overlay); make up-base (and CI, which references the base file directly) starts the base alone.

Service Image / binary Port(s) Notes
postgres postgres:16-alpine 5432 Durable state.
redis redis:7-alpine 6379 Directory / session routing / leases.
nats nats:2.10-alpine 4222 (client), 8222 (monitoring) Cross-shard bus.
otel-collector otel/opentelemetry-collector-contrib 4317 (OTLP gRPC), 8889 (Prometheus scrape) Metrics pipeline.
migrate telos-migrate One-shot: applies migrations, then exits.
seed telos-seed One-shot: imports the demo pack, then exits.
world telos-world 9090 (internal) Hosts the midgaard zone.
world-darkwood telos-world 9090 (internal) Hosts the darkwood zone.
world-crypt telos-world 9090 (internal) Hosts the crypt zone.
account telos-account 9100 (gRPC, internal), 8080 (web) OAuth / accounts / characters.
gate telos-gate 4000 (telnet) Primary edge; uses the dev bypass (type a name). Published on 127.0.0.1:4000 only.
gate-auth telos-gate 4001 (telnet) Account-backed edge; real GitHub OAuth via the browser device bridge.

Three separate telos-world services each own one zone, and players are moved between them across the market ↔ grove boundary by the cross-shard handoff. This is the topology that exercises the real distributed paths.

Single-box — deploy/docker-compose.single.yml

A deliberately minimal stack for a fast, self-contained look. One world process hosts all three zones (midgaard, darkwood, crypt), so cross-zone moves like the market ↔ grove crossing happen entirely in-process — no handoff, no directory re-dial. Run it explicitly:

docker compose -f deploy/docker-compose.single.yml up --build
telnet localhost 4000
Service Port(s) Notes
postgres (internal) Durable state.
redis 6380 → 6379 Separate host port so it won't collide with make up.
migrate / seed One-shot bootstrap.
world 9090 (internal) One shard, zones midgaard,darkwood,crypt.
gate 4000 (telnet) Plaintext, bare-name login.

Two things are notable about the single-box stack: it runs no NATS (nothing needs the cross-shard bus when everything is one process) and no account service (it uses the bare-name dev login). It is the smallest thing that boots a walkable world.

Neither Compose file runs a telos-director process. In these dev topologies zone placement is served directly from the Redis-backed directory (each zone is leased to its shard), so the director — which coordinates dynamic placement, scope leadership, and coordinated pull/reload — is not required to get a world running. See Orchestration & Directors for when you do want one.

The make targets you'll use

The Makefile is the front door. The targets relevant to running locally:

Target What it does
make up Build & start the full multi-shard stack + the Grafana LGTM backend (Grafana on http://localhost:3000).
make up-base Same, but without observability — the lean path CI/smoke use.
make deps Start only the backing services (Postgres, Redis, NATS) — the base for running binaries directly.
make down Stop and remove the stack, including the observability overlay.
make logs Tail stack logs, including observability.
make build Build all cmd/ binaries into ./bin.
make migrate Apply DB migrations (go run ./cmd/telos-migrate up; uses TELOS_POSTGRES_DSN).
make migrate-status Show migration status.
make seed Import the demo content pack into Postgres (go run ./cmd/telos-seed, pack='demo').

There is no single "run the binaries" target: the direct-process path is make deps to bring up the dependencies, then make build and launch the individual ./bin/telos-* binaries yourself (pointing each at your config). Most people use make up and never touch the raw binaries.

Configuration

Every service reads one YAML config, and any field can be overridden by a TELOS_* environment variable. Copy the example and point TELOS_CONFIG at it:

# config.example.yaml
service: telos-world
env: dev
log_level: info

postgres:
  dsn: postgres://telos:telos@localhost:5432/telosmud?sslmode=disable
redis:
  addr: localhost:6379
nats:
  url: nats://localhost:4222

Common overrides (the Compose files set these as env): TELOS_POSTGRES_DSN, TELOS_REDIS_ADDR, TELOS_NATS_URL, TELOS_SERVICE, TELOS_ENV, TELOS_ZONES (which zones a world shard hosts), TELOS_WORLD_LISTEN / TELOS_WORLD_TARGET, TELOS_GATE_LISTEN, TELOS_ACCOUNT_TARGET, and TELOS_CONTENT_PACKS (the enabled pack set — see Content Packs Intro). Placement is configuration, not code: which zones a shard owns is just its TELOS_ZONES list.

Bootstrapping the database

Before a world can serve content, the schema must exist and a pack must be loaded. Compose does this for you via the one-shot migrate and seed services; to do it by hand against a make deps stack:

make migrate     # apply db/migrations (embedded goose)
make seed        # import the demo pack into pack='demo' rows

telos-migrate applies the SQL migrations under db/migrations. telos-seed merges the embedded demo pack tree and writes it into the pack='demo' definition rows; it is idempotent (re-running replaces the pack's rows) and safe to run twice on the same volume. If NATS is reachable it also publishes a hot-reload invalidation so a running shard picks up the change without a restart; if NATS is down the rows are still written and the shard reloads on its next boot.

Connecting a client

Point a telnet client at the gate:

telnet localhost 4000

On the multi-shard stack, port 4000 is the primary gate and port 4001 is the account-backed gate. On the single-box stack there is just 4000. Once connected you spawn into the demo world's home zone (midgaard); walking north out of the temple takes you to the market, and north again crosses into the darkwood grove. How you authenticate on connect depends on which path you chose — see the two sections below.

Authentication: dev bypass

The dev bypass replaces the browser OAuth flow with a "type a name" login, so you can get into the world without an identity provider. It is what port 4000 uses on the dev stack. It is deliberately hard to enable by accident: all three of the following must be true, or the bypass is simply not there.

  1. Build tag telos_devauth. The bypass code path only compiles into the binary when built with -tags telos_devauth. A release build (built without it) compiles the path out entirely and forces the flag off — there is no runtime way to switch it on in a release binary. In the dev stack the Dockerfile sets BUILD_TAGS and Compose passes the tag.
  2. TELOS_DEV_AUTOAUTH=1. With the tag compiled in, this env var actually activates the bypass at runtime.
  3. A loopback bind. The gate must be listening on loopback (e.g. 127.0.0.1:4000). If the bypass is active but the listener is bound to a non-loopback address, the gate refuses to boot — a guard against ever exposing a passwordless login off-box. (There is an explicit escape hatch, TELOS_DEV_AUTOAUTH_ALLOW_REMOTE_BIND=1, for the rare case you know you want a non-loopback bind; do not use it on anything reachable.)

When active, connecting prompts "By what name shall you be known?" and drops you straight into the world under that name with no account record behind it. This is telos-gate-only, and in the dev Compose stack it is wired to 127.0.0.1:4000. The tests behind this path run via make test-devauth.

The account-less fallback (accountAuthGate)

Separately from the compiled bypass, a gate that is started without a TELOS_ACCOUNT_TARGET has no account service to talk to, so it also falls back to bare-name login. Because that is still a passwordless path, such a gate refuses to boot unless TELOS_ALLOW_INSECURE=1 is set to acknowledge it. This is why the single-box stack (which runs no account service) sets TELOS_ALLOW_INSECURE=1. Never set it on anything public.

Authentication: real OAuth (local)

To exercise the real login path locally, run telos-account and authenticate through GitHub — the only OAuth provider implemented today. The flow is broker-mediated: telnet in, the gate prints a clickable URL, you approve in the browser, and the gate picks up the result. (There is no typed link code and no connect <code> command — ignore any stale Compose comments that say otherwise.)

1. Register a GitHub OAuth App. In GitHub → Developer settings → OAuth Apps, create an app with:

  • Homepage URL: http://localhost:8080
  • Authorization callback URL: http://localhost:8080/auth/github/callback

Note its Client ID and generate a Client Secret.

2. Provide the credentials. Copy the example env file and fill it in:

cp deploy/auth.env.example deploy/auth.local.env
# then set, in deploy/auth.local.env:
#   TELOS_GITHUB_CLIENT_ID=...
#   TELOS_GITHUB_CLIENT_SECRET=...

3. Configure telos-account. The account service hosts the OAuth broker website and needs Redis (it stores the short-lived device sessions there). The relevant settings:

  • TELOS_WEB_LISTEN=:8080 and TELOS_WEB_PUBLIC_URL=http://localhost:8080
  • TELOS_REDIS_ADDR=… (required — without Redis the broker and device auth are disabled)
  • TELOS_WEB_SECURE_COOKIES=0 for local plain-HTTP development (cookies default to secure)

4. Point the gate at the account service and connect on :4001. The account-backed gate sets TELOS_ACCOUNT_TARGET=account:9100 and must not carry the devauth tag or env. Then:

telnet localhost 4001

The gate prints a http://localhost:8080/login/<device_code> URL. Open it, approve the GitHub authorization (the broker uses PKCE and CSRF-protected state), and the gate — which has been polling — sees the completed authorization, resolves or creates your account by your GitHub identity (never by email), and hands you to character select/create. The 4000 gate remains the bypass path; 4001 is the real one.

For the production version of this setup — public URLs, TLS, secure cookies, the caller token between gate and account — see OAuth Setup.

Troubleshooting

  • "This world has no rooms yet" / immediate login refusal — you booted a world before seeding content. Run make seed (or let the Compose seed one-shot complete). The embedded core bootstrap pack guarantees at least a lobby even before real content is seeded.
  • Gate refuses to boot — the dev bypass and other insecure conveniences require an explicit opt-in; without it the gate fails closed. See the dev-bypass section above.
  • Port already in use on 4000/6379 — you have both stacks up at once. The single-box stack intentionally uses Redis host port 6380 to avoid colliding with make up; stop one stack with make down (or the single-box docker compose … down).
  • Migrations pending — run make migrate-status to see the applied/pending list.

Clone this wiki locally