Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions engine/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,17 @@
# these build args back to their Docker Hub names still works and is the
# escape hatch for a builder that cannot reach GHCR.
ARG NODE_IMAGE=ghcr.io/thedancingdeveloper-org/vogt-base/node:22-bookworm
# The static demo's *runtime* base, deliberately not `NODE_IMAGE`. Building the
# PWA needs a toolchain; serving the built bytes does not — `demo-server.mjs`
# imports only `node:` builtins. `node:22-bookworm` derives from
# `buildpack-deps` and carries 413 packages including compilers and `-dev`
# headers, every one of which is attack surface the demo never uses and a
# fatal-gated Trivy finding waiting to be published against it. The `rm -rf npm`
# in the demo stage is the scar from the last round of that (#454);
# `libexpat1`/`libexpat1-dev` (CVE-2026-56408) was the next, and it failed
# `dev`'s build on four consecutive pushes while nothing in the tree had
# changed. The slim base carries 88 packages and neither of those.
ARG DEMO_RUNTIME_IMAGE=ghcr.io/thedancingdeveloper-org/vogt-base/node:22-bookworm-slim
ARG RUST_IMAGE=ghcr.io/thedancingdeveloper-org/vogt-base/rust:1-bookworm
# uv builds the core's virtualenv from the committed lockfile, so the image
# contains exactly what CI tested (NFR-Q5). Same pin as the root Dockerfile;
Expand Down Expand Up @@ -157,7 +168,7 @@ RUN set -eu; \
VOGT_PRODUCT_VERSION="$product_version"; \
pnpm demo:augment

FROM ${NODE_IMAGE} AS demo-runtime
FROM ${DEMO_RUNTIME_IMAGE} AS demo-runtime
WORKDIR /app
COPY --from=demo-web /app/web/dist /app/dist
COPY engine/deploy/demo-server.mjs /app/demo-server.mjs
Expand All @@ -168,7 +179,8 @@ COPY engine/deploy/demo-server.mjs /app/demo-server.mjs
# npm CLI the base image bundles is dead weight — and its own bundled
# dependencies (tar, brace-expansion, pacote, sigstore, …) are what the fatal
# Trivy gate flags as CRITICAL/HIGH. Removing npm/npx clears every one of those
# findings without touching anything the demo runtime uses.
# findings without touching anything the demo runtime uses. The slim base still
# ships npm, so this stays load-bearing.
RUN chmod 0444 /app/demo-server.mjs \
&& rm -rf /usr/local/lib/node_modules/npm \
/usr/local/bin/npm \
Expand Down
22 changes: 20 additions & 2 deletions tests/test_demo_delivery.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,10 +70,28 @@ def test_demo_image_branches_from_the_normal_web_build() -> None:
assert "FROM web-build AS demo-web" in text
assert "pnpm demo:augment" in text
demo = text.split("FROM web-build AS demo-web", 1)[1].split("# ─── Stage 2:", 1)[0]
assert "FROM ${NODE_IMAGE} AS demo-runtime" in demo
# Not `NODE_IMAGE`, which is the *builder*. Building the PWA needs a
# toolchain; serving the built bytes does not, and `node:22-bookworm`
# derives from `buildpack-deps` — 413 packages of compilers and `-dev`
# headers the demo never executes, each one a fatal-gated Trivy finding
# waiting to be published against it. That is not hypothetical twice over:
# npm's transitive deps forced the removal below (#454), and then
# `libexpat1`/`libexpat1-dev` (CVE-2026-56408) failed `dev`'s build on four
# consecutive pushes while nothing in the tree had changed. The slim base
# carries 88 packages and neither.
assert "FROM ${DEMO_RUNTIME_IMAGE} AS demo-runtime" in demo
assert "FROM ${NODE_IMAGE} AS demo-runtime" not in demo, (
"the demo runtime must not reuse the builder base; that is what put "
"compilers and -dev headers in a static file server"
)
assert re.search(r"^ARG DEMO_RUNTIME_IMAGE=\S+-slim$", text, re.MULTILINE), (
"the demo runtime base must be a slim variant: the property is a small "
"surface, not merely a separate build arg pointing at the same image"
)
assert "chmod 0444 /app/demo-server.mjs" in demo
# The demo server uses only node builtins, so the bundled npm CLI is removed
# — its transitive deps are what the fatal Trivy gate flags (#454).
# — its transitive deps are what the fatal Trivy gate flags (#454). The slim
# base still ships npm, so this stays load-bearing.
assert "rm -rf /usr/local/lib/node_modules/npm" in demo
assert "COPY --from=server-build" not in demo
assert "COPY --from=core" not in demo
Expand Down
Loading