Mar 30, 2026 · 1 min read
Docker multi-stage builds, explained by example
A pragmatic walkthrough of multi-stage Dockerfiles for Next.js apps — what each stage is for and why your image is 10x bigger than it needs to be.
If your production image weighs 1.2 GB, it's almost always because you copied
your node_modules into the runtime stage. Multi-stage builds fix this in 20
lines.
The shape
# 1. install — only deps, cached on lockfile
FROM node:20-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
# 2. build — compile the app
FROM node:20-alpine AS build
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build
# 3. runtime — only what's needed to serve
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
COPY --from=build /app/.next/standalone ./
COPY --from=build /app/.next/static ./.next/static
COPY --from=build /app/public ./public
USER node
EXPOSE 3000
CMD ["node", "server.js"]Why each stage exists
- deps — installing dependencies is the slow, cacheable step. We isolate it so a code change doesn't bust the install layer.
- build — runs
next build, producing the standalone output and static assets. Heavy tooling lives here, never escapes. - runner — nothing but the compiled output, the static assets, and a non-root user. This is what ships to production.
Two settings that matter
In next.config.mjs:
output: "standalone"This is what makes the runtime stage tiny. Next emits a self-contained server
that only needs the .next/standalone and .next/static folders.
And in your .dockerignore:
node_modules
.next
.git
Otherwise you'll ship node_modules from your laptop into the build context
and undo all the work.
Healthcheck
Add one. Without it, Compose and Kubernetes have no idea your container is actually serving:
HEALTHCHECK --interval=30s --timeout=3s \
CMD wget -qO- http://localhost:3000/api/health || exit 1The result
A 1.2 GB image becomes ~150 MB. Faster pulls, faster cold starts, smaller attack surface. There's no downside; if you're not doing this yet, today is a good day.