Context
The runtime stages of apps/hub/Dockerfile and apps/watchtower/Dockerfile end with:
COPY --from=builder /repo /repo
WORKDIR /repo/apps/hub # or .../watchtower
CMD ["node", "dist/server.js"]
That COPY --from=builder /repo /repo drags the entire workspace into the runtime image — including devDependencies (happy-dom, turbo, fast-uri, etc.), the full pnpm-managed node_modules tree, source files, test files, and lockfiles. The runtime only needs the compiled dist/ plus the runtime subset of node_modules.
This is visible in Trivy: even after PR #161 stripped npm install -g pnpm@9.15.0 from the runtime stage, the SARIF still flags happy-dom@15.11.7 (3 CVEs), turbo@2.9.6 (1), fast-uri@3.1.0 (2) — all devDeps that have no business being in a production image.
Why this matters
Approach options
Three reasonable paths, in increasing order of intrusiveness:
pnpm deploy. pnpm has a built-in pnpm deploy --filter <pkg> --prod /out command that copies just the deployment artifacts for one workspace package into a destination directory, hoisting only its production dependency graph. The runtime stage becomes COPY --from=builder /out /app and CMD ["node", "dist/server.js"]. Cleanest, idiomatic for pnpm workspaces.
pnpm prune --prod. Run pnpm prune --prod in the builder against the workspace, then copy /repo. Simpler diff, but pnpm prune --prod semantics in workspaces are not as clean — verify it actually walks per-package devDep removal correctly.
- Selective copy. Manually
COPY --from=builder /repo/apps/hub/dist /app/dist plus COPY --from=builder /repo/apps/hub/node_modules /app/node_modules and the same for the workspace packages that hub depends on (protocol, state-machine, sdk). Most explicit but brittle as deps shift.
Recommendation: try option 1 first. It's the pattern pnpm publishes for exactly this use case.
Acceptance criteria
Pointers
- Current Dockerfiles:
apps/hub/Dockerfile, apps/watchtower/Dockerfile
- pnpm deploy docs: https://pnpm.io/cli/deploy
- Trivy SARIF baseline: GitHub Security tab →
trivy-hub / trivy-watchtower for v2.2.0
- Workspace structure:
pnpm-workspace.yaml, apps/hub/package.json, apps/watchtower/package.json
Severity
Medium — operational hardening. Not user-facing, but blocks restoring the blocking Trivy gate. Best done in the same window as #163 (litestream upgrade) so we can re-enable exit-code: "1" in one commit afterward.
Context
The runtime stages of
apps/hub/Dockerfileandapps/watchtower/Dockerfileend with:That
COPY --from=builder /repo /repodrags the entire workspace into the runtime image — including devDependencies (happy-dom,turbo,fast-uri, etc.), the full pnpm-managednode_modulestree, source files, test files, and lockfiles. The runtime only needs the compileddist/plus the runtime subset ofnode_modules.This is visible in Trivy: even after PR #161 stripped
npm install -g pnpm@9.15.0from the runtime stage, the SARIF still flagshappy-dom@15.11.7(3 CVEs),turbo@2.9.6(1),fast-uri@3.1.0(2) — all devDeps that have no business being in a production image.Why this matters
exit-code: "1"on the Trivy gate (Restore Trivy gate to exit-code: "1" in gke-images workflow #165).Approach options
Three reasonable paths, in increasing order of intrusiveness:
pnpm deploy. pnpm has a built-inpnpm deploy --filter <pkg> --prod /outcommand that copies just the deployment artifacts for one workspace package into a destination directory, hoisting only its production dependency graph. The runtime stage becomesCOPY --from=builder /out /appandCMD ["node", "dist/server.js"]. Cleanest, idiomatic for pnpm workspaces.pnpm prune --prod. Runpnpm prune --prodin the builder against the workspace, then copy/repo. Simpler diff, but pnpmprune --prodsemantics in workspaces are not as clean — verify it actually walks per-package devDep removal correctly.COPY --from=builder /repo/apps/hub/dist /app/distplusCOPY --from=builder /repo/apps/hub/node_modules /app/node_modulesand the same for the workspace packages thathubdepends on (protocol, state-machine, sdk). Most explicit but brittle as deps shift.Recommendation: try option 1 first. It's the pattern pnpm publishes for exactly this use case.
Acceptance criteria
pnpm deploy).apps/hub/Dockerfileso the runtime image contains onlydist/and productionnode_modules. Verifynode dist/server.jsstill starts (usedocker runlocally with the required env vars fromapps/hub/src/config-validate.ts).apps/watchtower/Dockerfile.du -sh(ordocker image inspect) that the runtime image is meaningfully smaller than v2.2.0.happy-dom,turbo,fast-urino longer appear in the SARIF for the hub image.Pointers
apps/hub/Dockerfile,apps/watchtower/Dockerfiletrivy-hub/trivy-watchtowerfor v2.2.0pnpm-workspace.yaml,apps/hub/package.json,apps/watchtower/package.jsonSeverity
Medium — operational hardening. Not user-facing, but blocks restoring the blocking Trivy gate. Best done in the same window as #163 (litestream upgrade) so we can re-enable
exit-code: "1"in one commit afterward.