Files
jensandClaude Sonnet 5 0266c882c7 Fix stale jayfield/novnc-server image tag
create.sh and the CLAUDE.md build example still referenced
jayfield/novnc-server, left over from before the image was renamed
to jayfield/xserver-novnc (the tag it's actually published under).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018QuKZRefXR3hVaUiXgeu6i
2026-07-27 23:43:50 +02:00

50 lines
4.1 KiB
Markdown

# 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 a headless X server exposed over noVNC (VNC-over-websockets in the browser) plus raw X11-over-TCP. It builds on the public base image `ich777/novnc-baseimage`, which already provides TurboVNC, websockify, fluxbox, and noVNC — this repo only adds `net-tools`/`socat`, a TLS cert for noVNC, and the entrypoint/lifecycle scripts. Published as `jayfield/xserver-novnc` on Docker Hub.
## Commands
There is no build system, linter, or test suite — this is a Dockerfile plus a handful of shell scripts. Common workflows:
```bash
# Build the image
docker build -t jayfield/xserver-novnc .
# Create a container the way it's intended to be run (fixed name/ports/env)
./create.sh
# Run an arbitrary built image interactively for debugging (drops into bash, does not start server.sh)
./run.sh <image-name-or-id>
```
There's no automated way to exercise `scripts/server.sh` / `scripts/server_start.sh` outside a running container — to test changes to them, rebuild the image and start a real container (or use `run.sh` and invoke the script manually inside the shell).
## Architecture
**Image layering** (`Dockerfile`):
- Base image supplies TurboVNC (`Xvnc` on `$PATH`), websockify (`websockify` on `$PATH`), and noVNC's web client at `/usr/share/novnc/` — all installed at standard FHS paths via Debian packages, not under `/opt` or `/tmp`.
- This layer adds `net-tools` and `socat`, 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/`.
- `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 create`/`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 (websockify proxies this to the RFB port above). This is the port users actually connect to from a browser.
- `60${DISPLAY_NUM}` — raw X11 protocol, tunneled over TCP via `socat` onto the Unix socket at `/tmp/.X11-unix/X${DISPLAY_NUM}`, for clients that want a plain X11 connection instead of VNC.
`create.sh` and `run.sh` hardcode `DISPLAY_NUM=99` and map host ports accordingly (`5910:5999` for VNC, `8099:8099` for noVNC); update these together if `DISPLAY_NUM` ever changes.
**Startup sequence** (`scripts/server.sh`, run inside the container):
1. Remove any stale `/tmp/.X${DISPLAY_NUM}-lock` and `/tmp/.X11-unix/X${DISPLAY_NUM}` before starting Xvnc. `/tmp` survives `docker stop`/`docker start` on the same container, and Xvnc doesn't get a chance to clean these up when killed, so without this a restart fails to rebind the display.
2. Start `Xvnc` (TurboVNC's X/VNC server) on `$DISPLAY` at `${SCREEN_W}x${SCREEN_H}x24` with security disabled (`-securitytypes none` — auth is expected to happen at the noVNC/TLS layer or via network isolation, not VNC auth).
3. Start `fluxbox` as the window manager against that display.
4. Start `websockify` in the foreground (`-D` daemonizes websockify itself, but the script's own process still ends up blocking via the wrapper below), serving noVNC's web client and proxying to the local VNC port, using the cert generated in the Dockerfile.
5. Start `socat` to bridge the X11 Unix socket to TCP.
**Signal handling** (`scripts/server_start.sh` + `scripts/signals.sh`): Docker sends `SIGTERM` to PID 1 on `docker stop`. `server_start.sh` traps `SIGTERM`, forwards it to the child process tree, waits for the child's descendants to exit, and re-raises the correct exit code (`128 + signal number`) so the container's exit status reflects how it was stopped. When touching this wrapper, keep in mind it's generic (`$APP` is passed in as an argument) — it isn't specific to `server.sh` and could wrap a different entrypoint script.