Bake the browser's runtime libraries into the base image
Build App / compute-version (pull_request) Successful in 4s
Build App / build-macos (pull_request) Successful in 2m28s
Build App / build-windows (pull_request) Successful in 5m13s
Build Container / build-container (pull_request) Successful in 13m11s
Build App / build-linux (pull_request) Successful in 6m53s
Build App / create-tag (pull_request) Skipped
Build App / sync-to-github (pull_request) Skipped
Build App / compute-version (pull_request) Successful in 4s
Build App / build-macos (pull_request) Successful in 2m28s
Build App / build-windows (pull_request) Successful in 5m13s
Build Container / build-container (pull_request) Successful in 13m11s
Build App / build-linux (pull_request) Successful in 6m53s
Build App / create-tag (pull_request) Skipped
Build App / sync-to-github (pull_request) Skipped
`npx playwright install chromium` downloaded ~150 MB of browser that then died with "error while loading shared libraries: libglib-2.0.so.0" — verified, not inferred, against the current image. The image shipped none of Chromium's shared libraries, which is why `apt install google-chrome-stable` looked like the cure: apt was quietly installing the same set as Chrome's own dependencies. Installing them at runtime instead converges on the worst possible state. The libraries land in the container's writable layer, so they are re-paid after every Reset and *lost* on base-image migration, which replays apt from a manifest. The browsers ride in ~/.cache/ms-playwright, inside the home volume, and survive both — leaving a 400 MB browser present with its libraries gone. So the libraries are baked and the browsers are not: each half now lives where it already persists. The layer runs `npx --yes playwright@latest install-deps chromium` rather than a hand-written apt list. Ubuntu 24.04's 64-bit-time_t transition renamed a swathe of these packages (libasound2t64, libatk1.0-0t64, libglib2.0-0t64, …) and a new Chromium dependency would drift straight back into the launch failure this exists to prevent; letting Playwright name its own dependencies is self-maintaining. It sits immediately after Node — npx is its only prerequisite — and well above the shim COPYs, so editing a shim does not re-run it. The `--dry-run` that follows is a build-time assertion, not decoration: on a platform Playwright has no list for, `install-deps` prints a warning and returns having installed **nothing, with exit status 0**. Without the assertion that ships a broken image behind a clean build log. Measured, on a build of this file with the layer applied over an otherwise identical image: +99 packages, +334 MiB unpacked and +119 MiB compressed (2950 → 3284 MiB, 759 → 878 MiB). Two thirds of that is not reachable by trimming — libgbm1, which Chromium needs, pulls mesa-libgallium, which pulls libllvm20. A chromium-only apt list measures 247 MiB against install-deps' 341 MiB; the ~94 MiB difference is xvfb and the CJK/emoji fonts, kept because the base ships no fonts at all and every page this feature exists to display would otherwise render as tofu. Verified on real builds, both architectures: a `--platform linux/arm64` build of this file installs the same 99 packages and passes the same assertion. On the new amd64 image, `playwright install chromium` with no `--with-deps` and no `install-deps` launches headless Chromium 151.0.7922.34 and loads a page; on the old image the identical script fails on libglib-2.0.so.0. `install.rs` no longer runs `install-deps` unconditionally — that would be a minutes-long apt run for nothing on a current image. It asks `install-deps --dry-run` first and skips the install when everything is present, saying which of the two happened on the progress stream. The check is Playwright's rather than a probe of our own for library names, so check and fix cannot disagree about what the dependency set is. Note that `--dry-run` exits 0 both when everything is installed and when Playwright has no list for the platform, so the verdict is read from its output. Containers on older images stay the normal case until people migrate, and they still work: on such an image the simulation cannot even resolve the package names (the index is cleaned in every base image), which reports as "couldn't tell" and installs — the right answer. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KSP2KNPhuWKQ4DL5TZEn3k
This commit is contained in:
@@ -118,11 +118,18 @@ docker exec stdout → tokio task → emit("terminal-output-{sessionId}") → li
|
||||
touches nothing of the user's, needs no sudo (npm's prefix is `/usr`, which is root-owned),
|
||||
and is on the module resolution path for scripts in the project. Browsers go to
|
||||
`~/.cache/ms-playwright` as `claude`, i.e. the home volume.
|
||||
- **The base image ships none of Chromium's shared libraries.** `playwright install chromium`
|
||||
therefore downloads a browser that cannot launch, which is why installing Chrome via apt
|
||||
looks like a fix. The install action runs `install-deps` as root first and then *actually
|
||||
launches* the browser to verify. `@playwright/mcp` wants the `chrome` **channel**
|
||||
specifically, so both browsers are offered.
|
||||
- **Current base images ship Chromium's shared libraries; older ones do not** — and a project
|
||||
keeps the base image it was first built from until it is migrated, so "older" is the normal
|
||||
case. Without them `playwright install chromium` downloads a browser that cannot launch, which
|
||||
is why installing Chrome via apt looks like a fix. `install.rs` asks
|
||||
`install-deps --dry-run` first and skips the apt step when the answer is "all present",
|
||||
*saying so* in the progress stream. Do not decide this by probing for library names: the
|
||||
dry-run simulates the same `apt-get install` the fix would run, so check and fix cannot
|
||||
disagree about what the dependency set is. Note that `--dry-run` exits **0** both when
|
||||
everything is installed and when Playwright has no list for the platform — match on its
|
||||
output, not its exit code. Either way the action ends by *actually launching* the browser to
|
||||
verify. `@playwright/mcp` wants the `chrome` **channel** specifically, so both browsers are
|
||||
offered.
|
||||
- **`docker/`** — Docker API layer using bollard:
|
||||
- `client.rs` — Singleton Docker connection via `OnceLock`
|
||||
- `container.rs` — Container lifecycle (create, start, stop, remove, inspect)
|
||||
@@ -153,7 +160,27 @@ docker exec stdout → tokio task → emit("terminal-output-{sessionId}") → li
|
||||
|
||||
### Container (`container/`)
|
||||
|
||||
- **`Dockerfile`** — Ubuntu 24.04 base with Claude Code, Node.js 22, Python 3.12, Rust, Docker CLI, git, gh, AWS CLI v2, ripgrep, pnpm, uv, ruff pre-installed
|
||||
- **`Dockerfile`** — Ubuntu 24.04 base with Claude Code, Node.js 22, Python 3.12, Rust, Docker CLI, git, gh, AWS CLI v2, ripgrep, pnpm, uv, ruff pre-installed, plus the shared
|
||||
libraries a browser links against (see below)
|
||||
- **Browser runtime libraries are baked in; browser *binaries* are not.** A layer runs
|
||||
`npx --yes playwright@latest install-deps chromium` as root, so Playwright names its own
|
||||
dependencies and the list cannot rot against Ubuntu 24.04's `t64` renames or a new Chromium
|
||||
dependency. Measured: +99 packages, +334 MiB unpacked / +119 MiB compressed, on both arches. Do
|
||||
not replace it with a hand-written apt list without pinning the Playwright version you derived
|
||||
it from — a `chromium`-only list saves ~94 MiB (Playwright's `tools` group: xvfb and the CJK
|
||||
fonts) and nothing more, because `libgbm1` → `mesa-libgallium` → `libllvm20` is ~213 MiB that
|
||||
no trimming removes.
|
||||
- The `install-deps --dry-run` call after it is a **build-time assertion, not decoration**: on a
|
||||
platform Playwright's table does not cover, `install-deps` prints a warning and returns having
|
||||
installed nothing **with exit status 0**. Without the assertion that ships a broken image
|
||||
behind a clean build log.
|
||||
- Baking the libraries but not the browsers is the whole point of the split. Browsers live in
|
||||
`~/.cache/ms-playwright` (home volume) and already survive recreation *and* migration; a
|
||||
runtime `apt-get install` of the libraries lands in the writable layer, is re-paid after every
|
||||
Reset, and is **lost on base-image migration**, which replays apt from a manifest. The runtime
|
||||
approach converges on the worst state: a 400 MB browser present with its libraries gone.
|
||||
- The layer sits immediately after Node (npx is its only prerequisite) and well above the shim
|
||||
`COPY`s, so editing a shim does not re-run a multi-hundred-megabyte apt install.
|
||||
- **`entrypoint.sh`** — UID/GID remapping to match host user, SSH key setup, git config, docker socket permissions, Claude Code settings.json injection, then `sleep infinity`
|
||||
- **`triple-c-scheduler`** — Bash-based scheduled task system for recurring Claude Code invocations
|
||||
|
||||
|
||||
Reference in New Issue
Block a user