Skip to content

Add headless multi-stage Dockerfile.headless - #1

Merged
cboyd10 merged 2 commits into
mainfrom
issue-9-headless-dockerfile
Aug 20, 2026
Merged

Add headless multi-stage Dockerfile.headless#1
cboyd10 merged 2 commits into
mainfrom
issue-9-headless-dockerfile

Conversation

@cboyd10

@cboyd10 cboyd10 commented Aug 20, 2026

Copy link
Copy Markdown
Owner

Summary

Adds Dockerfile.headless, a non-interactive multi-stage build for Kubernetes deployment (issue LostCityRS#11's three pod containers), replacing the existing interactive-setup-wizard Dockerfile for that use case. The existing Dockerfile is untouched.

Changes

  • Dockerfile.headless: build stage (Bun) clones engine, content, and webclient (Client-TS) from their gamenight branches on cboyd10, builds webclient first and copies its output into engine/public/client/client.js (so fork customizations like issue start.ts support for 377-wip LostCityRS/Server#6's camera zoom are always baked in), then builds engine via bun run tools/pack/Build.ts directly (not the bun run build package-script alias — see Verification below for why). Runtime stage is node:lts-slim (not Bun — see below), installs the built engine/ tree's production deps plus tsx, bakes in a minimal data/config/world.json (node.production: true), runs as a non-root runner user, and boots straight into src/app.ts via npx tsx — the three pod roles (engine/login/logger) override CMD per container.

Verification (real docker build / docker run, not just reading code)

Both ACs pass against the current commit:

  • docker build -f Dockerfile.headless -t lostcity-test . succeeds end to end.
  • docker run --rm -e DB_BACKEND=sqlite -p 8888:8888 lostcity-test boots straight into the engine process ("World ready: Visit http://localhost:8888/rs2.cgi") with no interactive prompt, and stays up with no crash.

Three issues surfaced during verification, all fixed in this PR:

  1. bun run build fails under Bun (build stage): the engine's build package-script is tsx tools/pack/Build.ts; tsx's CLI shim fails to resolve its own relative import under Bun (error: Cannot find module './cjs/index.cjs'). Fixed by invoking bun run tools/pack/Build.ts directly, bypassing the tsx shim, with identical output.
  2. node:sqlite unsupported by Bun (runtime stage): engine/src/db/query.ts has a top-level, unconditional import { DatabaseSync } from 'node:sqlite', a Node builtin Bun 1.3.14 doesn't implement (error: No such built-in module: node:sqlite). This crashes module load for all three pod entrypoints regardless of DB_BACKEND (confirmed with both sqlite and mysql) — not a narrow sqlite-only edge case. Fixed by switching only the runtime stage to node:lts-slim + npx tsx, matching the existing interactive Dockerfile's precedent (it already uses node:lts-slim, not Bun, at runtime). Keeps the fix entirely within this repo/PR rather than requiring a change in the engine submodule.
  3. Dev file-watcher crash with no world.json (runtime stage): with no data/config/world.json present, loadWorldConfig() falls back to dev-oriented defaults (node.production: false, build.liveReload: true), which spins up a DevThread file watcher that crashes with EACCES: permission denied, mkdir '../content/maps' (that directory isn't present in the runtime image — only engine/ is copied in). Fixed by baking in a minimal data/config/world.json with node.production: true, which gates off DevThread creation (World.ts: if (!Environment.node.production && Environment.build.liveReload)).

Also note: the issue's exact AC2 command sets -e DB_BACKEND=sqlite, but the engine's config now comes entirely from data/config/world.json (introduced in feat: Support new world.json config) — Environment.ts doesn't read process.env directly anywhere, so that env var is currently a no-op. Harmless here since world.json's default db.backend is already 'sqlite', so AC2 passes as written either way; flagging in case DB_BACKEND is expected to be wired through for issue LostCityRS#11's pod configuration.

Notes on the issue's Implementation Notes

The issue flagged a likely data/config/world.json build-stage dependency (bun run tools/server/setup.ts, generating and COPYing the file into the build stage). Traced this: tools/pack/Build.ts never touches WorldConfig at all, so no config file is needed for the build to succeed — the verbatim build stage works as-is. However, a world.json is needed in the runtime stage, for the different reason above (disabling the dev file-watcher), which is what this PR adds. (Also: the issue named tools/server/setup.ts; the actual script is src/setup.ts, invoked via npm run setup — harmless naming slip, not otherwise relevant since this path isn't exercised by the build.)

Meta-repo submodule pointer

Per this project's established submodule PR cascade (see cboyd10/runescape's .claude/context/CONTEXT.md, "Submodule PR cascade" — used for issues #3/#4/LostCityRS#7): this PR does not close cboyd10/runescape#9 directly. Once this merges, a follow-up PR against cboyd10/runescape bumping the server submodule pointer to this PR's merge commit is needed, and that PR carries Closes #9. Merging this PR alone would not update the meta-repo's submodule pointer.

Implements cboyd10/runescape#9

Adds a non-interactive Dockerfile that clones and builds engine, content,
and webclient from their gamenight branches, bakes the webclient build
into engine/public/client/client.js, and boots straight into the server
process (src/app.ts by default) with no setup wizard prompt. Content and
Client-TS builds pull in each fork's customizations (e.g. issue LostCityRS#6's
camera zoom) rather than relying on whatever was pre-committed to engine.

Implements cboyd10/runescape#9
@cboyd10

cboyd10 commented Aug 20, 2026

Copy link
Copy Markdown
Owner Author

Status: parked mid-investigation by an unattended pickup — see cboyd10/runescape#9 for the full writeup and the self-downgrade (afk -> hitl) it triggered.

  • The tsx build-step fix in this PR's current commit is verified and good: docker build -f Dockerfile.headless -t lostcity-test . succeeds end to end.
  • docker run still fails (error: No such built-in module: node:sqlite) for all three pod-role entrypoints (src/app.ts, src/login.ts, src/logger.ts) — a Bun-vs-Node runtime incompatibility with engine/src/db/query.ts's static node:sqlite import, unrelated to this PR's diff. Needs a design decision (see the linked issue comment for options) before this can go ready for review.

The build-stage engine build used `bun run build`, which shells out to
`tsx` under the hood; tsx's CLI shim fails to resolve its own relative
import under Bun (`Cannot find module './cjs/index.cjs'`). Invoke the
same script directly through Bun's native TypeScript runtime instead
(`bun run tools/pack/Build.ts`), bypassing the broken shim.

The runtime stage used Bun (`oven/bun:1-slim`), but engine/src/db/query.ts
has a top-level, unconditional `import { DatabaseSync } from 'node:sqlite'`,
a Node builtin Bun 1.3.14 doesn't implement. This crashes module load
regardless of DB_BACKEND. Switch the runtime stage to node:lts-slim,
matching the existing interactive Dockerfile's precedent, and run via
`npx tsx`.

With no data/config/world.json present, the engine falls back to dev
defaults (production: false, build.liveReload: true), which spins up a
DevThread file watcher that crashes on `../content/maps` (EACCES) since
that directory isn't present in the runtime image. Bake in a minimal
world.json with node.production: true to disable it, matching the
headless/production intent of this image.

Verified with real `docker build` and `docker run` against this exact
Dockerfile: the image builds end to end and boots the world with no
crash and no interactive prompt.
@cboyd10
cboyd10 marked this pull request as ready for review August 20, 2026 20:42
@cboyd10
cboyd10 merged commit 5c05049 into main Aug 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant