Docker Multi-Stage Builds for Production Images

Learn how to create secure, efficient Docker images using Multi-Stage Builds. Optimize your CI/CD pipeline and reduce image sizes.

If you’re still shipping bloated Docker images or leaking secrets in your build pipeline, you’re setting yourself up for slow deployments and security headaches. Docker Multi-Stage Builds are the answer: they let you create production-ready images that are small, fast, and secure—without sacrificing your build-time convenience.

Key Takeaways:

  • Understand the core problem Multi-Stage Builds solve in Docker workflows
  • Use real-world Dockerfile examples to build secure, minimal production images
  • Apply best practices for reducing image size and attack surface
  • Learn common mistakes and how to avoid them in your CI/CD pipeline
  • Compare Multi-Stage Builds with other image optimization strategies

Why Multi-Stage Builds Matter

Traditional Docker builds often result in large images packed with build tools, source code, and sensitive files—none of which are needed at runtime. This leads to:

  • Bloated images: Slow pulls, longer deploys, increased storage costs
  • Security risks: Exposed credentials or build artifacts in production images
  • Hard-to-maintain Dockerfiles: Complex, error-prone cleanup steps

Docker introduced Multi-Stage Builds (since Docker 17.05) to solve these problems. With this feature, you can:

  • Separate build-time and runtime environments
  • Copy only the final executable and necessary files into the production image
  • Use different base images for building and running your app

This is a game-changer for production deployments—especially in CI/CD pipelines or Kubernetes clusters where every megabyte and security hole counts.

How Multi-Stage Builds Work

With Multi-Stage Builds, a single Dockerfile can define multiple FROM statements—each starting a new stage. You can name each stage and selectively copy artifacts from one stage to another using the –from flag in COPY commands.

# Example structure
FROM golang:1.21-alpine AS builder
WORKDIR /src
COPY . .
RUN go build -o myapp .

FROM alpine:3.18
COPY --from=builder /src/myapp /usr/local/bin/myapp
ENTRYPOINT ["myapp"]

Here’s what’s happening:

You landed the Cloud Storage of the future internet. Cloud Storage Services Sesame Disk by NiHao Cloud

Use it NOW and forever!

Support the growth of a Team File sharing system that works for people in China, USA, Europe, APAC and everywhere else.
  • The first stage uses a full Go build environment to compile the app.
  • The second stage starts from a tiny Alpine Linux image, copying only the built binary.
  • The final image is ~10MB, instead of hundreds of MB if you shipped all the build tools.

You can add as many stages as needed (for testing, code linting, etc.), but only the last stage ends up in your production image.

Step-by-Step: Building a Production Image

1. The Basic Multi-Stage Pattern

Let’s use a real-world Node.js app that needs to transpile TypeScript and run on Node in production.

# Stage 1: Build
FROM node:20-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci          # Use ci for reproducible, clean installs
COPY . .
RUN npm run build   # Transpile TypeScript, bundle assets

# Stage 2: Production
FROM node:20-alpine AS production
WORKDIR /app
COPY --from=build /app/dist ./dist
COPY --from=build /app/package*.json ./
RUN npm ci --omit=dev # Only install prod dependencies
USER node             # Drop root privileges for security
EXPOSE 3000
CMD ["node", "dist/server.js"]

2. Build and Run the Image

# Build the image (replace myapp:prod with your tag)
docker build -t myapp:prod .

# Run the container, mapping port 3000
docker run --rm -p 3000:3000 myapp:prod

This approach ensures:

  • No dev dependencies or build tools are present in the final image
  • Production runs as a non-root user
  • Resulting image is much smaller and safer

3. Adding a Test Stage (Optional, but Recommended)

# Stage 1: Build
FROM node:20-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

# Stage 2: Test
FROM build AS test
RUN npm run test

# Stage 3: Production
FROM node:20-alpine AS production
WORKDIR /app
COPY --from=build /app/dist ./dist
COPY --from=build /app/package*.json ./
RUN npm ci --omit=dev
USER node
EXPOSE 3000
CMD ["node", "dist/server.js"]

By adding a test stage, you can run tests during CI/CD (docker build --target test .) without polluting the production image.

Optimizing for Production: Security, Size, and Speed

Security Hardening

  • Always drop root privileges: use USER to run as a non-root user
  • Use minimal base images (alpine, distroless, or scratch)
  • Never copy .env files, secrets, or build tools into the final image
  • Pin base image versions (e.g., node:20-alpine instead of node:alpine)

Reducing Image Size

  • Use npm ci --omit=dev instead of npm install to avoid unnecessary dependencies
  • Copy only what you need: avoid COPY . . in production stages
  • Leverage .dockerignore to exclude test data, docs, and local config

Performance Tuning

  • Order Dockerfile instructions to maximize build layer caching
  • Keep runtime image layers minimal for faster startup and replication
  • Scan images for vulnerabilities using Trivy or Docker Scan

Sample .dockerignore for Node.js

node_modules
test
*.log
.env
.git
Dockerfile

This prevents accidental leakage of sensitive files or cache pollution.

Comparison Table: Image Optimization Strategies

StrategySecurityImage SizeEase of UseBest Use Case
Multi-Stage BuildsHighMinimalModerateProduction containers, CI/CD pipelines
Single-Stage with CleanupsLowMediumEasyPrototyping, development
Distroless ImagesVery HighSmallestAdvancedSecurity-critical workloads

Troubleshooting Common Issues

Build Fails Due to Missing Files

If you see no such file or directory errors when copying files between stages, double-check your WORKDIR and COPY paths. Remember, each stage starts with a clean filesystem and only gets what you explicitly copy.

Image is Still Too Large

  • Are you copying only the necessary files into the production image?
  • Did you prune unused dependencies (npm ci --omit=dev or equivalent)?
  • Is your .dockerignore file excluding unnecessary files?

Secrets Leaked in Production Images

Never COPY or ADD secrets into intermediate stages unless you’re 100% sure they aren’t referenced in the final image. Use Docker BuildKit secrets for passing credentials at build time without persisting them in any layer.

Multi-Stage Builds Not Supported

If your CI/CD system or Docker version is older than 17.05, Multi-Stage Builds won’t work. Upgrade Docker everywhere or use external build scripts as a workaround.

Pinning Versions for Reproducibility

If your builds suddenly break or behave differently, check that you’re pinning all base image and dependency versions. Floating tags (node:alpine) can change underneath you.

Pro Tips and Pitfalls

  • Explicitly name your stages (e.g., AS builder, AS production) for clarity and maintainability.
  • Use BuildKit for advanced features (like secrets and parallel builds). Enable with DOCKER_BUILDKIT=1.
  • Don’t rely on implicit context: always specify exactly what you copy between stages.
  • Scan your images before pushing to production. Even minimal images can inherit vulnerabilities from base layers.
  • Test your image locally before deploying—don’t assume a successful build means a working app.
  • Layer caching: Re-order COPY and RUN instructions so that infrequently changing dependencies are installed before frequently changing app code. This speeds up rebuilds dramatically.
  • Distroless and scratch images: For ultimate minimalism and security, consider distroless images. But expect extra debugging effort—there’s no shell or package manager.

Conclusion and Next Steps

Docker Multi-Stage Builds should be standard for any production container workflow. They make your images smaller, faster, and more secure—without adding much complexity to your build process.

Next, review your existing Dockerfiles for unused build tools or missed cleanup steps. Convert them to Multi-Stage Builds and measure the difference. For advanced hardening, try distroless base images or integrate image scanning in your CI/CD pipeline.

For more details, consult the official Docker documentation or experiment with distroless containers for even leaner deployments.

Start Sharing and Storing Files for Free

You can also get your own Unlimited Cloud Storage on our pay as you go product.
Other cool features include: up to 100GB size for each file.
Speed all over the world. Reliability with 3 copies of every file you upload. Snapshot for point in time recovery.
Collaborate with web office and send files to colleagues everywhere; in China & APAC, USA, Europe...
Tear prices for costs saving and more much more...
Create a Free Account Products Pricing Page