Skip to content

fix(demo): serve the static demo from a slim base, not the builder - #508

Merged
thedancingdeveloper merged 1 commit into
devfrom
fix/demo-libexpat
Sep 2, 2026
Merged

fix(demo): serve the static demo from a slim base, not the builder#508
thedancingdeveloper merged 1 commit into
devfrom
fix/demo-libexpat

Conversation

@thedancingdeveloper

Copy link
Copy Markdown
Contributor

The failure

dev's build workflow has failed on four consecutive pushes2d0e3d5, 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:

libexpat1      CVE-2026-56408  HIGH  fixed  2.5.0-1+deb12u2 → 2.5.0-1+deb12u3
libexpat1-dev

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-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, including compilers and -dev headers, behind a server whose complete dependency set is four builtins:

node:fs   node:fs/promises   node:http   node:path

Why not just patch libexpat

Upgrading the package would leave the next advisory against the other ~400 to break dev again — which is exactly what already happened once: the rm -rf /usr/local/lib/node_modules/npm in 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:

Probe Base Trivy exit Findings
fat node:22-bookworm 1 2 HIGH — libexpat1, libexpat1-dev
slim node:22-bookworm-slim 0 none

That 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):

healthz          200
root has #root   true
manifest schema  true   (full-estate-v1)
api status       404  | no server-side API: true

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 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_build pinned FROM ${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 pytest 1315 passed / 25 skipped / 91% · mypy clean · ruff clean · check_docs.py clean.

🤖 Generated with Claude Code

https://claude.ai/code/session_01RzFCTnWGff1k4FyX1HQCYM

`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.
@thedancingdeveloper
thedancingdeveloper merged commit f6f1e30 into dev Sep 2, 2026
16 checks passed
@thedancingdeveloper
thedancingdeveloper deleted the fix/demo-libexpat branch September 2, 2026 04:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant