Container Architecture

Docker Multi-Stage Builds: Reducing Next.js & Node Container Images by 80%

Packaging full-stack web applications into monolithic container images leads to gigabyte-sized deployments, sluggish CI/CD pipelines, and broad attack surfaces. Here is how multi-stage builds decouple build tooling from lean production artifacts.

Saud Siraj• Published Sep 2026• 8 min read

Need an optimized multi-stage Dockerfile now?

Generate production Dockerfiles for Next.js, FastAPI, Django, and Node with non-root security.

Open Dockerfile Generator

1. The Problem with Single-Stage Builds

In a naive single-stage Dockerfile, developers install everything needed to compile their source code:

  • Build tools (C/C++ compilers, Python, git).
  • Heavy development dependencies (TypeScript compiler, Webpack, ESLint, Jest test suites).
  • Temporary build caches and package manager tarballs.

Once compiled, those build tools remain permanently embedded inside the final Docker image. A basic Node.js or Next.js app frequently swells to 1.2GB – 1.8GB. This wastes bandwidth on every push, slows down Kubernetes rolling updates, and introduces hundreds of unneeded OS packages with potential security vulnerabilities (CVEs).

2. How Multi-Stage Builds Work

Docker multi-stage builds (introduced in Docker 17.05) allow you to use multiple FROM statements in a single Dockerfile. Each FROM line begins a fresh, isolated build stage using a different base image.

You can compile your assets in a heavy build stage, and selectively copy only the compiled output files into a minimal runtime container using the COPY --from=<stage> directive:

// Conceptual Flow of Multi-Stage Architecture
Stage 1: [deps] Installs full dependencies (including devDependencies)
Stage 2: [builder] Compiles TypeScript/Next.js output (.next/standalone)
Stage 3: [runner] Alpine/Distroless base. Copies ONLY compiled JS + server
>> Final Container Size: ~120 MB (90% reduction!)

3. Optimizing Docker Layer Caching

Docker caches intermediate image layers based on whether the input files have changed. A frequent mistake is copying the entire application directory before running npm install:

# ❌ Anti-pattern: Invalidates the npm install cache on EVERY code edit
COPY . .
RUN npm install

By copying only package.json and package-lock.json first, Docker skips npm install entirely unless dependencies actually changed:

# ✅ Best practice: Cache dependencies independently of source code
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build

4. Production-Ready Next.js Multi-Stage Dockerfile

Here is a production-grade multi-stage Dockerfile utilizing Next.js's standalone output mode (configured via output: 'standalone' in next.config.js):

# ── STAGE 1: Install dependencies ──
FROM node:20-alpine AS deps
WORKDIR /app
RUN apk add --no-cache libc6-compat
COPY package.json package-lock.json ./
RUN npm ci

# ── STAGE 2: Build the application ──
FROM node:20-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
ENV NEXT_TELEMETRY_DISABLED=1
RUN npm run build

# ── STAGE 3: Production runner ──
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1

# Security: Never run containers as root!
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs

# Copy ONLY compiled standalone bundle and static assets
COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static

USER nextjs
EXPOSE 3000
ENV PORT=3000
ENV HOSTNAME="0.0.0.0"

CMD ["node", "server.js"]

5. The Non-Root Security Mandate

By default, Docker processes execute as root (UID 0) inside the container. If a remote code execution vulnerability (RCE) occurs in an application dependency, an attacker who breaks out of container isolation inherits root privileges on the underlying host kernel.

Always create an unprivileged system user and switch to it using the USER instruction (e.g. USER nextjs) before starting the server process.