Multi-Stage Builds & Image Optimization
In modern software engineering, the toolchains required to build code (TypeScript compilers, C++ build-essentials, Git, Python dev headers) are vastly different from what is needed to run it.Multi-stage builds enable you to maintain multiple FROM stages in a single Dockerfile, discarding gigabytes of intermediate build artifacts and delivering razor-thin, production-hardened images.
1. The Anti-Pattern: Single-Stage Image Bloat
When building applications in a single stage, all build tooling remains permanently frozen in the production image layers:
Sluggish Deployments
Pulling a 1.5 GB image across Kubernetes worker nodes slows down auto-scaling by minutes.
Storage & Egress
Cloud registries charge for stored gigabytes and internet egress bandwidth.
Vulnerability Surface
Unused package managers, compilers, and curl binaries expose your container to CVE exploits.
2. Base Image Spectrum: From Ubuntu to Scratch
Selecting the right runtime base image dictates performance, memory footprint, and security posture:
| Image Base | Compressed Size | C Standard Library | Shell / Package Manager | Best Fit Workload |
|---|---|---|---|---|
ubuntu:24.04 | ~75 MB | glibc | Bash, apt, coreutils | Legacy monolithic tools requiring full OS packages. |
alpine:3.19 | ~5 MB | musl libc | BusyBox sh, apk | Lightweight Node.js, Python, and shell automation scripts. |
gcr.io/distroless/static | ~2 MB | Minimal | NO SHELL, NO PACKAGE MANAGER | High-security production runtimes. Hackers cannot launch a shell! |
scratch | 0 MB | None (Static) | None (Empty filesystem) | Statically compiled Go, Rust, or C++ binaries. |
3. Production Statically Compiled Go / Rust Service (10 MB Image)
Compile your backend code inside a heavyweight Golang builder stage, and copy only the compiled binary into an empty scratch image:
# ---------------------------------------------
# STAGE 1: Compilation Environment (Heavy: ~800 MB)
# ---------------------------------------------
FROM golang:1.22-alpine AS builder
WORKDIR /src
# Cache dependencies
COPY go.mod go.sum ./
RUN go mod download
# Copy source code and build statically linked CGO-disabled binary
COPY . .
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -o /bin/api-server .
# ---------------------------------------------
# STAGE 2: Minimal Zero-Overhead Production (Tiny: ~12 MB)
# ---------------------------------------------
FROM scratch
# Copy SSL root certificates for outbound HTTPS calls
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
# Copy only the compiled binary
COPY --from=builder /bin/api-server /bin/api-server
# Expose service port and set entrypoint
EXPOSE 8080
ENTRYPOINT ["/bin/api-server"]4. Production Next.js Standalone Multi-Stage Build
By enabling output: 'standalone' in next.config.js, Next.js automatically traces your imports and copies only required node_modules into a minimal bundle:
# Stage 1: Install all dependencies (including devDependencies for build)
FROM node:20-alpine AS deps
RUN apk add --no-cache libc6-compat
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
# Stage 2: Build Next.js 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: Minimal Production Runner Stage
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV PORT=3000
# Create dedicated unprivileged runtime user
RUN addgroup --system --gid 1001 nodejs && adduser --system --uid 1001 nextjs
# Copy static assets and traced standalone bundle
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
CMD ["node", "server.js"]Next Up: Container Security Practices
In Chapter 15, explore non-root users, dropping Linux capabilities, read-only root filesystems, and vulnerability scanning.
Chapter 15: Container Security Practices →