Layers & Caching
Each instruction in a Dockerfile creates a layer. The build cache reuses a layer when its instruction and the files it depends on are unchanged, so layer ordering is the single biggest lever you have over rebuild time and image size. The rule: put slow, rarely-changing steps before fast, often-changing ones.
A layer-aware Dockerfile for a Node app
EXAMPLE
# syntax=docker/dockerfile:1.7
ARG NODE_VERSION=20
# ---------- deps stage: only invalidated when lockfile changes ----------
FROM node:${NODE_VERSION}-alpine AS deps
WORKDIR /app
# Copy only manifests first so 'npm ci' is cached unless lockfile moves
COPY package.json package-lock.json ./
RUN --mount=type=cache,target=/root/.npm \
npm ci --omit=dev
# ---------- build stage: source code changes invalidate from here down ---
FROM node:${NODE_VERSION}-alpine AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN --mount=type=cache,target=/root/.npm npm ci
COPY tsconfig.json ./
COPY src ./src
RUN npm run build
# ---------- runtime: tiny final image, no build tools ----------
FROM node:${NODE_VERSION}-alpine AS runtime
WORKDIR /app
ENV NODE_ENV=production
COPY --from=deps /app/node_modules ./node_modules
COPY --from=build /app/dist ./dist
COPY package.json ./
USER node
EXPOSE 3000
CMD ["node", "dist/server.js"]
# Build: docker build -t app:dev .
# Inspect layer sizes: docker history app:dev
# Inspect contents: docker run --rm -it app:dev sh
Why it matters
Multi-stage builds keep your final image free of compilers, dev dependencies, and source code. Pair them with a BuildKit cache mount (--mount=type=cache) and a .dockerignore that excludes node_modules and .git, and warm rebuilds drop from minutes to seconds.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
# Each line in a Dockerfile = a cacheable layer. # Order matters: copy package.json BEFORE source, so npm install caches.Try it Yourself »
Discussion
Loading…