Dockerfile cache investigation — 2026-04-26
Symptom observed today: docker compose build hmp-web-dashboard (without --no-cache) re-ran RUN npm install for 675 seconds even though package.json and package-lock.json had not changed since the previous build.
Root cause
hmp-web-dashboard/Dockerfile deps stage (lines 4-13):
FROM node:20-alpine AS deps
WORKDIR /app
COPY hmp-shared-packages/packages/api-contracts /hmp-shared-packages/packages/api-contracts
COPY hmp-shared-packages/packages/constants /hmp-shared-packages/packages/constants
COPY hmp-shared-packages/packages/ui-kit /hmp-shared-packages/packages/ui-kit
COPY hmp-shared-packages/packages/design-tokens /hmp-shared-packages/packages/design-tokens
COPY hmp-web-dashboard/package.json hmp-web-dashboard/package-lock.json* ./
COPY hmp-web-dashboard/.npmrc* ./
RUN npm install --legacy-peer-deps
The 4 shared-package COPY statements are cache-keyed on the full content of those directories. That includes:
packages/ui-kit/dist/— build artifact, changes everynpm run buildin ui-kitpackages/ui-kit/node_modules/— present in some local dev statespackages/*/.tsbuildinfo— TypeScript incremental compile cache, changes every TS compile- All test files, source
.ts/.tsx,README.md, etc.
When any of those bytes change, the COPY layer hash differs → RUN npm install is invalidated → re-runs from scratch (8-12 min depending on network).
What npm install actually needs from those packages is far smaller: just the package.json files (so the lockfile resolution can verify name/version of the file: deps).
Verified by inspecting the root .dockerignore
**/node_modules ← excluded — good
**/.next ← excluded — good
hmp-web-dashboard/dist ← excluded — good
hmp-shared-packages/packages/*/dist ← NOT excluded — by design
**/.tsbuildinfo ← NOT excluded — invalidates on every TS compile
The intentional retention of hmp-shared-packages/packages/*/dist is what makes the dashboard's file:-resolved deps work at builder-stage build time. So we can't simply add it to .dockerignore — the builder stage genuinely needs it.
Three repos affected
| Repo | Pattern | Affected |
|---|---|---|
hmp-web-dashboard | copies 4 shared pkgs into deps stage | yes |
hmp-admin-portal | copies 1 shared pkg into deps stage | yes (smaller blast radius) |
hmp-booking-engine | COPY . ./ before npm install | yes (entire repo invalidates) |
Backend services (auth, pos, frontdesk, etc.) are NOT affected — they consume @hmp/* from Azure Artifacts (npm registry), so the shared packages aren't part of their build context.
Proposed fix — two layered changes
Change 1 — split deps stage to copy only what npm install reads (highest impact)
FROM node:20-alpine AS deps
WORKDIR /app
# Only package.json files from shared pkgs — npm install doesn't read their src.
# This means src/dist/.tsbuildinfo changes in shared pkgs don't invalidate npm install.
COPY hmp-shared-packages/packages/api-contracts/package.json /hmp-shared-packages/packages/api-contracts/
COPY hmp-shared-packages/packages/constants/package.json /hmp-shared-packages/packages/constants/
COPY hmp-shared-packages/packages/ui-kit/package.json /hmp-shared-packages/packages/ui-kit/
COPY hmp-shared-packages/packages/design-tokens/package.json /hmp-shared-packages/packages/design-tokens/
COPY hmp-web-dashboard/package.json hmp-web-dashboard/package-lock.json* ./
COPY hmp-web-dashboard/.npmrc* ./
# BuildKit cache mount: keep npm tarball cache across builds even when the
# install layer DOES invalidate (e.g. real lockfile change). This stops the
# 463-sec re-download on npm registry throttle that we hit today.
RUN --mount=type=cache,target=/root/.npm,sharing=locked \
npm install --legacy-peer-deps
FROM node:20-alpine AS builder
WORKDIR /app
# Builder stage gets the FULL shared package source (this layer DOES invalidate
# whenever shared pkg src changes — which is correct, the build needs that source).
COPY hmp-shared-packages/packages/api-contracts /hmp-shared-packages/packages/api-contracts
COPY hmp-shared-packages/packages/constants /hmp-shared-packages/packages/constants
COPY hmp-shared-packages/packages/ui-kit /hmp-shared-packages/packages/ui-kit
COPY hmp-shared-packages/packages/design-tokens /hmp-shared-packages/packages/design-tokens
# node_modules + symlinks come from cached deps stage — survives source changes.
COPY --from=deps /app/node_modules ./node_modules
COPY hmp-web-dashboard/ ./
ENV NEXT_TELEMETRY_DISABLED=1
RUN rm -f .env.local .env.development.local
RUN npm run build
# (runner stage unchanged)
Change 2 — exclude .tsbuildinfo from build context
Append to root C:\CLProjects\HMP\.dockerignore:
# TypeScript incremental compile cache — changes every compile, never relevant to runtime
**/*.tsbuildinfo
**/tsconfig.tsbuildinfo
Risk + verification plan
Risk: the npm install symlinks node_modules/@hmp/ui-kit -> /hmp-shared-packages/packages/ui-kit. If, in the deps stage, that target directory only has package.json (no dist/), the symlink still resolves to a valid path. At builder-stage build time, the full source (including dist/) is COPY-d into the same path, so import '@hmp/ui-kit' resolves to the dist files exactly as before.
Verification (run before merging):
- Apply the change locally.
docker compose build hmp-web-dashboard— first build (cold). Note duration.- Make a no-op change to
hmp-web-dashboard/src/app/page.tsx(e.g. add a space in a comment). docker compose build hmp-web-dashboard— second build. Expect:npm installlayer =CACHED, onlynext buildre-runs. Duration ~1-2 min.- Make a real source change to
hmp-shared-packages/packages/ui-kit/src/Button.tsx. Rebuild. Expect:npm installSTILL cached, only the builder-stage shared-pkg COPY invalidates. Duration ~1-2 min. - Bump
hmp-web-dashboard/package.jsonversion. Rebuild. Expect:npm installinvalidates (correctly),--mount=type=cachestill serves from npm tarball cache, full duration ~1-3 min instead of 8-12.
Reversibility: git revert if any of the assumptions above turn out wrong. Backend services and other apps are not touched.
Expected speedup
| Scenario | Today | After fix |
|---|---|---|
| No code changes (rebuild) | 11-12 min | 5-10 sec (all cached) |
| Dashboard src change | 11-12 min | ~1-2 min (only next build) |
| Shared pkg src change | 11-12 min | ~1-2 min (next build only) |
| Real lockfile change | 11-12 min | 1-3 min (cache mount serves tarballs) |
--no-cache (release verify) | 11-12 min | 11-12 min (unchanged) |
Same fix template applies to
hmp-admin-portal/Dockerfilehmp-booking-engine/Dockerfile(different shape — needs separate analysis)
Recommendation
Ship Change 1 + Change 2 together for hmp-web-dashboard first; verify via the 6-step plan above; then apply the same template to hmp-admin-portal and hmp-booking-engine. All three repos can then drop --no-cache from their daily build flow.