Skip to content

Restructure hub + watchtower Dockerfile runtime stage to ship prod-only deps #164

Description

@dantaik

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:

  1. 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.
  2. 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.
  3. 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

  • Pick an approach (default: pnpm deploy).
  • Update apps/hub/Dockerfile so the runtime image contains only dist/ and production node_modules. Verify node dist/server.js still starts (use docker run locally with the required env vars from apps/hub/src/config-validate.ts).
  • Repeat for apps/watchtower/Dockerfile.
  • Confirm with du -sh (or docker image inspect) that the runtime image is meaningfully smaller than v2.2.0.
  • Confirm in Trivy that happy-dom, turbo, fast-uri no longer appear in the SARIF for the hub image.
  • Smoke-test the resulting image end-to-end against staging: hub health endpoint responds, watchtower scrapes work, litestream replication still ships snapshots.

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions