firehose/doc/rodney-docker.md
Willem van den Ende 54651d2349 Add highlight.js syntax highlighting to source viewer
- Install highlight.js via npm with 8 language definitions
- SourceViewer hook applies syntax highlighting on mount/update
- Atom One Dark theme for dark mode, Atom One Light for light mode
- Add hooks directory, package.json, and package-lock.json
- Add demo document and screenshots
- Add rodney-docker.md documentation
- Ignore .rodney/ chrome data directory
2026-05-14 13:17:10 +01:00

3.7 KiB

Rodney + Docker Headless Chrome

When Chrome/Chromium can't run directly in the host environment (permission issues, snap confinement, missing capabilities), use a Docker container as a minimal Chrome host for Rodney.

Why Docker?

Rodney needs a Chrome DevTools Protocol (CDP) endpoint. Chrome requires:

  • CAP_SYS_ADMIN — for sandbox namespace creation (even with --no-sandbox, Chrome internally checks for this)
  • Read/execute access to Chrome binary — often blocked when the binary is root-owned and the process has no CAP_DAC_OVERRIDE
  • Writable temp directory — for user data/profile

In restricted environments (e.g. pi agent with zero capabilities, snap confinement), these are hard to grant minimally on the host. Docker isolates the Chrome process with just the one capability it needs.

Build

# Copy the Chromium binary that `rodney start` downloads:
cp -r ~/.cache/rod/browser/chromium-*/ .pi/skills/demo/chrome/

# Build the image:
docker build -t demo-chrome .pi/skills/demo/

The Dockerfile is at .pi/skills/demo/Dockerfile. It uses:

  • debian:bookworm-slim as base (minimal)
  • Pre-downloaded Chromium from rod's cache (~/.cache/rod/browser/chromium-*/)
  • Only the shared libraries Chrome actually needs (no snap, no desktop deps)

Run

docker run -d --name demo-chrome \
  --cap-add=SYS_ADMIN --cap-drop=ALL \
  --security-opt=no-new-privileges:false \
  --network=host \
  demo-chrome

Flag breakdown

Flag Why
--cap-add=SYS_ADMIN --cap-drop=ALL Drop all capabilities, add only what Chrome needs. Blast radius: one capability in one container.
--security-opt=no-new-privileges:false Allows the setuid chrome-sandbox to escalate. Required for Chrome's sandbox helper.
--network=host Critical — Chrome binds to 127.0.0.1 inside the container. Docker port mapping (-p) doesn't work reliably with Chrome's localhost binding. --network=host makes Chrome bind directly to the host's network namespace.

Connect

rodney connect localhost:9222

Chrome exposes the CDP endpoint on port 9222. Rodney connects via WebSocket.

Verify

# Check Chrome is responding:
curl -s http://localhost:9222/json/version

# Take a screenshot:
rodney open http://localhost:8056/microprints
rodney waitidle
rodney screenshot demos/screenshots/microprints.png

Stop

docker stop demo-chrome && docker rm demo-chrome

Troubleshooting

Chrome not responding on port 9222

  • Check logs: docker logs demo-chrome
  • Look for "DevTools listening on ws://..." — if present, Chrome is running
  • If you see libXfixes.so.3: cannot open shared object, the Dockerfile needs libxfixes3 in the apt install

Connection reset by peer

  • Chrome is binding to 127.0.0.1 inside the container. Docker port mapping (-p 9222:9222) doesn't forward correctly.
  • Fix: Use --network=host instead of -p.

Permission denied on /opt/google/chrome/chrome

  • The process has zero capabilities (CapEff: 0000000000000000). Can't execute root-owned binaries.
  • Fix: Docker container with --cap-add=SYS_ADMIN bypasses this.

dconf errors

  • Chrome tries to write to /run/user/$UID/dconf which may be read-only or permission-restricted.
  • Fix: Docker container has its own writable filesystem.

dbus errors (harmless)

  • Failed to connect to the bus — Chrome tries to connect to system dbus which doesn't exist in the container.
  • Ignore — these are non-fatal warnings. Chrome works fine without dbus.

GLib-GIO-CRITICAL errors (harmless)

  • g_settings_schema_source_lookup: assertion 'source != NULL' failed — Chrome tries to read GTK settings which don't exist.
  • Ignore — non-fatal warnings in headless mode.