Guidance for Claude Code on building/testing this image and the Dockerfile/scripts architecture. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RZjg4yXCYacriPz3euenQF
3.7 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 a headless X server exposed over noVNC (VNC-over-websockets in the browser) plus raw X11-over-TCP. It builds on the external base image jayfield/novnc-baseimage, which already provides TurboVNC, websockify, and noVNC — this repo only adds the window manager, a TLS cert for noVNC, and the entrypoint/lifecycle scripts.
Commands
There is no build system, linter, or test suite — this is a Dockerfile plus a handful of shell scripts. Common workflows:
# Build the image
docker build -t jayfield/novnc-server .
# 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 (
/opt/TurboVNC), websockify + noVNC (/tmp/websockify,/tmp/noVNC). - This layer adds
net-toolsandsocat, generates a self-signed TLS cert/key at build time (/tmp/novnc.pem,/tmp/novnc.key) for noVNC's HTTPS/WSS listener, and copiesscripts/to/opt/scripts/. ENTRYPOINTis/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 viasocatonto 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):
- Start
Xvnc(TurboVNC's X/VNC server) on$DISPLAYat${SCREEN_W}x${SCREEN_H}x24with security disabled (-securitytypes none— auth is expected to happen at the noVNC/TLS layer or via network isolation, not VNC auth). - Start
fluxboxas the window manager against that display. - Start
websockifyin the foreground (-Ddaemonizes 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. - Start
socatto 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.