fix(demo): serve the static demo from a slim base, not the builder - #508
Merged
Conversation
`dev`'s build has failed on four consecutive pushes — 2d0e3d5, d2c73de, c1fc614, ba6baf1 — always the same job, always Trivy, and never because anything in the tree changed. CVE-2026-56408 was published against `libexpat1`/`libexpat1-dev` 2.5.0-1+deb12u2 with a fix in deb12u3, and the gate is `--ignore-unfixed`, so a fixed CVE is exactly what it refuses. Neither package is used by the demo. They are there because `demo-runtime` reused `NODE_IMAGE`, the *builder* base: building the PWA needs a toolchain, serving the built bytes does not. `node:22-bookworm` derives from `buildpack-deps` and carries 413 packages of compilers and `-dev` headers behind a server whose entire dependency set is four `node:` builtins (`node:fs`, `node:fs/promises`, `node:http`, `node:path`). So the fix is not to chase the patch. Upgrading libexpat would leave the next advisory against the other 400 packages to break `dev` again, the same way npm's transitive deps did in #454 — the `rm -rf npm` above this line is that scar. The runtime now has a base of its own at `node:22-bookworm-slim`: 88 packages, no libexpat, no compilers. Measured with two images identical but for the base, scanned with the workflow's exact Trivy invocation: fat (node:22-bookworm) exit 1, 2 HIGH (libexpat1, libexpat1-dev) slim (node:22-bookworm-slim) exit 0, 0 findings and the slim image serves correctly under the same hardened flags CI runs (`--read-only --cap-drop ALL --security-opt no-new-privileges`): healthz 200, root document carries `id="root"`, manifest keeps `full-estate-v1`, and `POST /api/sessions` still refuses with 404 and "no server-side API". No new plumbing: the mirror workflow discovers bases by reading `ARG *IMAGE=` lines, and Renovate's `registryAliases` maps the whole `vogt-base` prefix, so the new base is mirrored and tracked upstream without touching either config. The test that pinned `FROM ${NODE_IMAGE} AS demo-runtime` now pins the property that actually matters — a runtime base separate from the builder, and a slim one — with both halves verified to fail on the old shape.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The failure
dev'sbuildworkflow has failed on four consecutive pushes —2d0e3d5,d2c73de,c1fc614,ba6baf1— always the same job (build, smoke, and publish the static public demo), always at the Trivy step, and never because anything in the tree changed:The gate runs
--ignore-unfixed, so a CVE with a fix is precisely what it refuses. Every substantive job in that workflow passes; only the demo image is red.Why those packages are there at all
demo-runtimereusedNODE_IMAGE— the builder base. Building the PWA needs a toolchain; serving the built bytes does not.node:22-bookwormderives frombuildpack-depsand carries 413 packages, including compilers and-devheaders, behind a server whose complete dependency set is four builtins:Why not just patch libexpat
Upgrading the package would leave the next advisory against the other ~400 to break
devagain — which is exactly what already happened once: therm -rf /usr/local/lib/node_modules/npmin that stage is the scar from npm's transitive deps tripping the same gate (#454). Treating the surface, not the symptom, is the fix.The runtime gets its own base:
node:22-bookworm-slim, 88 packages, no libexpat, no compilers.Measured, not assumed
Two images identical but for the base, scanned with the workflow's exact Trivy invocation:
node:22-bookwormlibexpat1,libexpat1-devnode:22-bookworm-slimThat reproduces the CI failure locally and demonstrates the fix removes it.
The slim image also serves correctly under the same hardened flags CI uses (
--read-only --cap-drop ALL --security-opt no-new-privileges --tmpfs /tmp):No new plumbing
The mirror workflow discovers bases by reading
ARG *IMAGE=lines, and Renovate'sregistryAliasesmaps the wholevogt-baseprefix — so the new base is mirrored and tracked upstream with no change to either config. Verified by running the workflow's own "no build stage pulls from Docker Hub" check locally.Test
test_demo_image_branches_from_the_normal_web_buildpinnedFROM ${NODE_IMAGE} AS demo-runtime, which was incidental to what it was protecting. It now pins the property that matters — a runtime base separate from the builder, and a slim one — and I confirmed both new assertions fail against the old shape before keeping them.Verification
uv run pytest1315 passed / 25 skipped / 91% ·mypyclean ·ruffclean ·check_docs.pyclean.🤖 Generated with Claude Code
https://claude.ai/code/session_01RzFCTnWGff1k4FyX1HQCYM