Files
docker-dosbox-novnc/CLAUDE.md
T
jensandClaude Sonnet 5 827f03f087 Drive games from per-game JSON manifests instead of raw zip scanning
Games are no longer discovered from *.zip files on the SMB share; each
game now has its own <name>.json manifest (name/title/zip/start_cmd)
living next to its zip, discovered dynamically via smbclient. Nothing
about a game's identity or how to install/start it is hardcoded in the
image anymore. The setup page also grew Start/Stop/Uninstall buttons,
backed by real process tracking (Popen + terminate/kill).

Piloted on stuntcar only (manifest already uploaded to the share); t7g
has no manifest yet so it won't appear until one's added. The old
game.sh/start_game.sh docker-exec launch path is left as-is for now and
overlaps with the new Start button - noted in TODO.md for later cleanup.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NiNnj78HGx1KWyCCo39HSz
2026-07-28 10:27:33 +02:00

8.1 KiB

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

What this is

A Docker image definition for headless DOSBox games exposed over noVNC (VNC-over-websockets in the browser). It builds on the public base image ich777/novnc-baseimage, which already provides TurboVNC, websockify, fluxbox, and noVNC — this repo adds dosbox/alsa-utils plus unzip/smbclient/python3 for game management, a TLS cert for noVNC, and the entrypoint/lifecycle scripts. Published as jayfield/dosbox-novnc on Docker Hub.

Game data (DOS game installs, sourced as zips from an internal SMB share //vlda-01/software/Games/dosbox/) is deliberately not baked into the image — one of the two games (t7g, "The 7th Guest") is ~930MB of CD-ROM ISOs on its own, more than the rest of the image combined. Instead the image ships empty and games are installed at runtime into a volume, via an HTML setup page (see below).

Commands

There is no build system, linter, or test suite — this is a Dockerfile plus a handful of shell scripts and a small Python HTTP server. Common workflows:

# Build the image
docker build -t jayfield/dosbox-novnc .

# Run it the way it's intended to be run (fixed name, volume, ports)
./run.sh

# Launch a specific installed game inside the running container
./game.sh <game-name>   # e.g. ./game.sh stuntcar

There's no create.sh in this project (unlike the other noVNC-family projects) — run.sh is the only launch script, and it's -d --rm (long-lived-but-disposable), not a persistent named container created once via docker create.

There's no automated way to exercise scripts/server.sh, scripts/setup_server.py, or scripts/start_game.sh outside a running container — to test changes, rebuild the image, start a real container, and either watch docker logs or docker exec in.

Architecture

Image layering (Dockerfile):

  • Base image supplies TurboVNC (Xvnc on $PATH), websockify (websockify on $PATH), and noVNC's web client at /usr/share/novnc/.
  • This layer adds dosbox/alsa-utils (the actual game runtime) and unzip/smbclient/python3 (needed permanently at runtime now, not just at build time — see below), generates a self-signed TLS cert/key at build time (/tmp/novnc.pem, /tmp/novnc.key) for noVNC's HTTPS/WSS listener, and copies scripts/ to /opt/scripts/.
  • ${GAMES_HOME} (/opt/games) is created and declared as a VOLUME — it's meant to be backed by a named volume at docker run time (run.sh uses -v dosbox-games:/opt/games) so installed games survive --rm container restarts instead of being re-fetched every time.
  • ENTRYPOINT is /opt/scripts/server_start.sh /opt/scripts/server.sh — the signal-handling wrapper invoked with the real startup script as its argument.

Runtime env vars (set at docker run time, not baked into the image): DISPLAY_NUM (X display number, default 99 → DISPLAY=:99), SCREEN_W, SCREEN_H (framebuffer geometry).

Port scheme, all derived from DISPLAY_NUM:

  • 59${DISPLAY_NUM} — raw VNC (RFB), only bound to localhost, not published directly.
  • 80${DISPLAY_NUM} — noVNC over HTTPS/WSS. This is the port users connect to from a browser to see/play the game.
  • 60${DISPLAY_NUM}EXPOSEd for consistency with the other noVNC-family projects, but nothing in server.sh actually bridges X11-over-TCP here (no socat call) — this project doesn't wire that up, unlike docker-xserver-novnc/docker-sdr-novnc.
  • 70${DISPLAY_NUM} — the game setup UI (scripts/setup_server.py), plain HTTP. run.sh publishes this as 7099:7099.

run.sh hardcodes DISPLAY_NUM=99 (implicitly, via the image default) and the 7099 port mapping; update it if DISPLAY_NUM ever changes. Note run.sh does not currently publish the 59xx/80xx noVNC ports to the host — only 7099 (setup) is mapped, plus whatever the container's on the same Docker network can reach directly.

Game installation & lifecycle (scripts/setup_server.py), the key thing that differs from the other noVNC-family projects:

  • A stdlib-only Python ThreadingHTTPServer (no third-party deps, no pip installs) started in the background by server.sh alongside Xvnc/fluxbox/websockify.
  • Games are not hardcoded anywhere in the image or discovered from raw zip files. Each game is described by its own <name>.json manifest living next to its zip on the SMB share (e.g. Games\dosbox\stuntcar.json), with name/title/zip/start_cmd fields — see the module docstring in setup_server.py for the exact schema. discover_games() lists *.json on the share (smbclient -N ... -c 'ls Games\dosbox\*.json'), smbclient gets each one into /tmp/manifests/, and parses it — so dropping a new <name>.json + <name>.zip pair on the share is enough to make a game appear, no image rebuild needed.
  • GET / renders one row per discovered manifest with its live status (Not installed / downloading / extracting / Installed / Running / error) and the matching action buttons.
  • POST /install: smbget -au (guest auth) the manifest's zip into a /tmp/setup-<name> scratch dir, then unzip -o -d ${GAMES_HOME}. smbget -a (guest) cannot be combined with -o (output-file) — they conflict on the underlying -U option — so the download cds into the scratch dir and lets smbget save under its default filename instead. Installed-ness is just "non-empty ${GAMES_HOME}/<name> directory exists"; a zip is expected to contain one top-level folder matching its own basename.
  • POST /uninstall: rm -rf ${GAMES_HOME}/<name>. Refused while the game is running (stop it first) — there's no auto-stop-then-delete.
  • POST /start: subprocess.Popen(manifest["start_cmd"], cwd=GAMES_HOME/<name>), tracked in an in-process dict keyed by game name (running_procs). This inherits setup_server.py's own environment, including $DISPLAY, so the launched process renders onto the same Xvnc display fluxbox/noVNC are already serving — no separate X setup needed.
  • POST /stop: terminate()s the tracked process, escalating to kill() after a 5s grace period.
  • Per-game install progress (install_state) and the running-process table (running_procs) are both guarded by one threading.RLock (reentrant — several code paths call is_running() from inside a block that already holds the lock). The page does a <meta http-equiv="refresh"> every 3s while any install is in flight; no client-side JS anywhere.
  • Only stuntcar.json exists on the share right now — this whole manifest/start/stop/uninstall mechanism was rolled out as a pilot on that one (small, fast-to-test) game before being extended to t7g. Until t7g.json is added, t7g simply won't appear on the setup page even though its zip is still there.

Playing a game — two overlapping paths right now:

  1. The setup page's Start/Stop buttons (above) — the current way, works for any manifest-driven game.
  2. game.sh <name>docker exec -it dosbox-novnc start_game.sh <name>, running inside the already-running container. scripts/start_game.sh hardcodes DOSBOX_VERSION="0.74-3" and the run.bat/dosbox-<version>.conf convention — this predates the manifest system and was deliberately left as-is during the pilot. The two paths don't know about each other (e.g. a game started via game.sh won't show as "Running" on the setup page). Worth reconciling once manifests cover every game — see TODO.md.

Signal handling (scripts/server_start.sh + scripts/signals.sh): identical pattern to the other noVNC-family projects — Docker sends SIGTERM to PID 1 on docker stop, server_start.sh traps it, forwards it to the child process tree, waits for the descendants to exit, and re-raises 128 + signal number as its own exit code. Generic ($APP passed as an argument), not specific to server.sh.

Sizing history

See TODO.md for the detailed before/after of the size-reduction work (2.31GB → 1.6GB → ~690MB) — the short version: merge build layers so intermediate files (downloaded zips, build-only packages) don't persist, then stop embedding the actual game data in the image at all and fetch it into a volume on demand instead.