Lesson 75 of 81
A naive Dockerfile copies your entire node_modules into the image and calls it done — a multi-stage build using standalone output produces something ten times smaller, and skips the two mistakes that break almost every first attempt.
15 min read
Every option explains itself — why the right one is right, and why each wrong one is wrong. Miss one and it comes back later on a spaced-repetition schedule.
This topic is about judgment and practical tradeoffs more than a single right answer — a mock interview is the better way to test yourself on it.
Be interviewed on this →Hints without spoilers, and a plain explanation of why a wrong answer was wrong — answered only from this lesson. Free account, no card.
Sign up freeWhat interviewers actually ask about this, and what a strong answer has to cover.
A strong answer covers
Likely follow-ups
Containerizing a Next.js app means producing a Docker image that can run next start (or the standalone server) without needing your whole development environment inside it. The naive approach — COPY . ., npm install, npm run build, CMD npm start — works, but ships an image bloated with dev dependencies, the entire node_modules tree, and source files the running server never touches.
The output: 'standalone' build mode (the same mode covered in the deploying-a-next-app lesson) exists partly for this: Next.js traces exactly which files the app needs at runtime — a subset of node_modules, the compiled server, nothing else — and puts them in .next/standalone. A Dockerfile built around that traced output, using a multi-stage build, ends up copying only what actually runs, not everything that was needed to build it.
# syntax=docker/dockerfile:1
FROM node:22-alpine AS deps
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN corepack enable && pnpm install --frozen-lockfile
FROM node:22-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
# Build-time env vars have to be present HERE, not just at runtime — see below
ARG NEXT_PUBLIC_SITE_URL
ENV NEXT_PUBLIC_SITE_URL=$NEXT_PUBLIC_SITE_URL
RUN corepack enable && pnpm build
FROM node:22-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
# Output File Tracing already picked exactly what's needed — copy only that
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /app/public ./public
EXPOSE 3000
CMD ["node", "server.js"]
The deps stage installs dependencies once and gets cached across builds when the lockfile hasn't changed. The builder stage does the actual next build. The runner stage — the one that becomes the final, shipped image — never runs pnpm install at all; it just copies the three things the standalone server actually needs: the traced server bundle, the static assets, and the public folder.
.next/standalone is a server bundle, not the whole app. Both public/ and .next/static need their own explicit COPY into the runner stage, every time.NEXT_PUBLIC_* env vars are inlined into the JavaScript bundle at build time, not read at runtime. Setting them only in the container's runtime environment (e.g. a Kubernetes env: block, or docker run -e) has no effect — by the time the container starts, the build already happened and the value is either baked in or missing. They have to be passed as build ARGs during the builder stage, as shown above.docker run -e, a .env file mounted in, orchestrator secrets), never baked into the image, since anyone with the image can extract build-time values but a runtime-only secret never touches the image layers at all.RUN addgroup -S nodejs && adduser -S nextjs -G nodejs then USER nextjs) before the CMD, which this minimal example skips for clarity but a real deployment shouldn't.node:*-alpine images are the popular small-image choice, but they use musl libc instead of glibc — a native dependency that only ships prebuilt glibc binaries (rare, but it happens) will fail to install or run under Alpine specifically, which is worth knowing before debugging a "works on my machine" native-module error."Our Docker image builds fine but every page is missing its CSS and images in production" is a debugging scenario that tests whether a candidate actually understands what output: 'standalone' traces and what it deliberately leaves out — the fix is two COPY lines, but only once you know static assets were never part of the trace to begin with.