A multi-stage build uses more than one FROM in a single Dockerfile, letting you use one stage to build/compile and a separate, much smaller stage to actually ship — the build toolchain never makes it into the final image.
# Stage 1: build — has the full Node toolchain, dev dependencies, source maps, etc.
FROM node:20 AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Stage 2: run — only the compiled output ships, not the whole toolchain
FROM node:20-alpine
WORKDIR /app
COPY --from=build /app/dist ./dist
COPY --from=build /app/node_modules ./node_modules
CMD ["node", "dist/server.js"]
COPY --from=build pulls specific files out of a previous stage by name, discarding everything else that stage produced — the compiler, dev dependencies, intermediate build artifacts, and the larger base image it used never appear in the final image's layers at all. This routinely takes a multi-gigabyte build environment down to a final image in the tens of megabytes, and it's the standard way to reconcile "I need a full toolchain to build this" with "I don't want to ship that toolchain to production."
A stage can also be used purely as a cache/reference without being the final output — you can have three or four named stages (AS deps, AS build, AS test) and only the last FROM in the file is what actually gets built and tagged by default, unless you target an earlier one explicitly with docker build --target test.