Skip to content
All notes

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.

docker
devops
nextjs

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 1

The 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.