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:8888against 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
vscodeuser 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-freefontin the Layer A package block — unpinned, tracking thealpine:3.24base like the other apk packages.chromedrivercomes from the same apk snapshot aschromium, 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-headlesswrapper 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_BINis 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.