← Back to DevBytes

Docker Multi-Stage Builds: Production Guide

Understanding Docker Multi-Stage Builds

Docker multi-stage builds are a powerful feature that lets you use multiple FROM statements inside a single Dockerfile. Each FROM instruction begins a new build stage, and you can selectively copy artifacts—such as compiled binaries, minified bundles, or production-only dependencies—from one stage to another. The final image contains only what you explicitly copy from previous stages, plus the base image of the last stage. This keeps your production images lean, secure, and free of build-time tools, temporary files, or secrets.

Why Multi-Stage Builds Matter for Production

Traditional Dockerfiles often mix build dependencies and runtime code in the same image. This leads to bloated containers, slower deployments, and increased attack surfaces. Multi-stage builds solve these problems by separating the build environment from the runtime environment. Here are the key benefits:

How Multi-Stage Builds Work: Syntax and Mechanics

A multi-stage build begins with multiple FROM instructions. You can name stages with the AS keyword, then copy files between stages using COPY --from=<stage>. If you don't name a stage, you can reference it by its numeric index (starting at 0), but named stages are clearer and more robust.

Here is the fundamental pattern:

FROM base-image AS stage-name
# ... build commands ...

FROM another-base AS another-stage
# ... other commands ...

FROM final-base
COPY --from=stage-name /path/in/stage /destination/in/final
COPY --from=another-stage /some/artifact /destination
CMD ["run", "production-app"]

The final FROM statement defines the image that will be produced. Only the files you explicitly copy into that final stage end up in the shipped container. All intermediate layers from earlier stages are discarded from the final image, though they remain in the build cache for subsequent runs.

Practical Example: Node.js Application

Consider a typical Express backend written in TypeScript. Without multi-stage builds, you might install all dependencies, run tsc, and ship the entire workspace—including TypeScript source files, development-only packages, and the compiler itself. With multi-stage builds, you can produce a final image that contains only the compiled JavaScript, production node_modules, and the package manifest.

# Stage 1: Build environment
FROM node:18-alpine AS builder
WORKDIR /app

# Install dependencies (production + development)
COPY package*.json ./
RUN npm ci

# Copy source code and compile TypeScript
COPY tsconfig.json ./
COPY src ./src
RUN npm run build   # e.g., npx tsc

# Stage 2: Production image
FROM node:18-alpine AS production
WORKDIR /app

# Copy only the compiled output and production dependencies
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY package.json ./

EXPOSE 3000
USER node
CMD ["node", "dist/index.js"]

The builder stage uses npm ci to install all dependencies (including TypeScript), compiles the source code, and then the production stage copies only dist/ and node_modules/. The TypeScript compiler and source files never appear in the final image, reducing the image size by hundreds of megabytes.

Example: Go Application with a Static Binary

Go applications compile to a single static binary. The ideal production image often starts from scratch (an empty base) or a minimal alpine image. A multi-stage build makes this effortless.

# Stage 1: Build the Go binary
FROM golang:1.21-alpine AS builder
WORKDIR /workspace

# Cache dependencies
COPY go.mod go.sum ./
RUN go mod download

# Copy source and compile a static binary
COPY . .
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o server .

# Stage 2: Minimal production image
FROM scratch
COPY --from=builder /workspace/server /server
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
EXPOSE 8080
USER 1000:1000
CMD ["/server"]

The final image contains nothing but the compiled binary and the CA certificates needed for HTTPS calls. It has no shell, no package manager, and no unnecessary libraries. This drastically reduces the attack surface and produces an image often under 10 MB.

Best Practices for Production Dockerfiles

Conclusion

Docker multi-stage builds transform how you prepare production images. By splitting the build into distinct stages and copying only essential artifacts into a minimal final base, you achieve containers that are smaller, faster to deploy, and far more secure. This approach is not a niche trick—it’s the recommended pattern for modern containerized applications, whether you’re shipping a Node.js API, a Go microservice, a Java JAR, or a compiled frontend bundle. Start with named stages, pick a lean production base image, and let the Docker build cache work for you. Once you adopt multi-stage builds, you’ll find that maintaining clean, efficient, and secure Dockerfiles becomes second nature.

🚀 Need a reliable AI agent for your project?

Deploy Hermes Agent in 10 minutes. Managed hosting, zero DevOps.

Get Started — $23.99/mo
← Back to all articles