Skip to main content

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 every npm run build in ui-kit
  • packages/ui-kit/node_modules/ — present in some local dev states
  • packages/*/.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​

RepoPatternAffected
hmp-web-dashboardcopies 4 shared pkgs into deps stageyes
hmp-admin-portalcopies 1 shared pkg into deps stageyes (smaller blast radius)
hmp-booking-engineCOPY . ./ before npm installyes (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):

  1. Apply the change locally.
  2. docker compose build hmp-web-dashboard — first build (cold). Note duration.
  3. Make a no-op change to hmp-web-dashboard/src/app/page.tsx (e.g. add a space in a comment).
  4. docker compose build hmp-web-dashboard — second build. Expect: npm install layer = CACHED, only next build re-runs. Duration ~1-2 min.
  5. Make a real source change to hmp-shared-packages/packages/ui-kit/src/Button.tsx. Rebuild. Expect: npm install STILL cached, only the builder-stage shared-pkg COPY invalidates. Duration ~1-2 min.
  6. Bump hmp-web-dashboard/package.json version. Rebuild. Expect: npm install invalidates (correctly), --mount=type=cache still 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​

ScenarioTodayAfter fix
No code changes (rebuild)11-12 min5-10 sec (all cached)
Dashboard src change11-12 min~1-2 min (only next build)
Shared pkg src change11-12 min~1-2 min (next build only)
Real lockfile change11-12 min1-3 min (cache mount serves tarballs)
--no-cache (release verify)11-12 min11-12 min (unchanged)

Same fix template applies to​

  • hmp-admin-portal/Dockerfile
  • hmp-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.