Skip to content

0016. Headless browser for HTML testing

Status

Accepted

Context

Agents working in a consuming project sometimes need to render or test an HTML file — screenshot it, inspect the post-JavaScript DOM, or script interactions — and reported that they "couldn't test the HTML file due to lack of a browser." The image shipped no browser.

Constraints specific to this image:

  • Alpine/musl base (ADR 0001). Precompiled glibc binaries segfault on musl. Playwright's and Puppeteer's bundled browser downloads are glibc builds and do not run on Alpine — the single biggest Alpine browser footgun.
  • Security-hardened, default-deny egress (ADR 0009): all traffic routes through mitm:8080 → privoxy:8888 against a project-defined allowlist. Chromium phones home on startup (component/variations/safebrowsing), which would be blocked and would spam the firewall logs.
  • Runs as the non-root vscode user in a rootless-podman container (ADR 0007).

Decision

Install a musl-native headless Chromium from Alpine's community repo, plus a matching WebDriver and a Python client:

  • apk add chromium chromium-chromedriver font-noto ttf-freefont in the Layer A package block — unpinned, tracking the alpine:3.24 base like the other apk packages. chromedriver comes from the same apk snapshot as chromium, so their versions stay locked (eliminates the #1 WebDriver failure mode). Fonts prevent tofu/blank glyphs.
  • selenium (PyPI, Renovate-pinned ARG) installed in Layer E — pure-Python, no musl exposure — for scripted click/wait/assert.
  • A scripts/chromium-headless wrapper baking in container-safe and telemetry-off flags: --headless=new --no-sandbox --disable-gpu --disable-dev-shm-usage, --disable-background-networking / --disable-component-update / --safebrowsing-disable-auto-update / etc. so the browser stays quiet behind the egress firewall. CHROME_BIN is exported so Selenium and other tooling find the system binary.

Rejected: Playwright / Puppeteer as the sanctioned path — officially unsupported on musl, and their bundled-browser downloads segfault. If a Node consumer needs one, puppeteer-core (never plain puppeteer) pointed at the system chromium via PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium-browser + PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=1 is best-effort only, not baked in.

Consequences

Accepted: - Agents can render, screenshot, dump the DOM of, or script a local HTML file out of the box. Testing local file:// needs no egress-allowlist changes. - --no-sandbox disables the renderer sandbox (unavoidable for non-root headless in this container). Acceptable only for rendering trusted, local HTML you control — do not point the wrapper at untrusted web content. Any remote resource a page fetches is still subject to the egress firewall. - Image grows ~350–450 MB (chromium + deps + fonts). This is the whole cost; chromedriver and selenium are rounding error on top. Watch CI build disk on ubuntu-latest (no free-space step today) — add one if a build hits no space left on device. - Chromium's version follows the Alpine base (no per-tool Renovate PR); it moves when the alpine:3.24 pin is bumped. Alpine's chromium can trail upstream by a few majors — fine for local HTML rendering.