How to Reduce Docker Image Size with Multi-Stage Builds

Khimananda Oli 7 min read Database
How to Reduce Docker Image Size with Multi-Stage Builds

By Khimananda Oli | Last reviewed: August 2026

Bloated containers are a silent tax on your infrastructure budget and deployment velocity. If you are still shipping gigabyte-sized images containing compilers, package managers, and source code, you are increasing attack surface and slowing down CI/CD pipelines unnecessarily. Learning how to reduce Docker image size with multi-stage builds is the single most effective optimization for modern application delivery, transforming 1GB+ artifacts into lean, secure runtime containers under 100MB.

How does multi-stage build architecture actually work?

Multi-stage builds solve the "dependency paradox": you need heavy tools to compile software but only need the resulting binary to run it. Before this feature existed, engineers had to maintain two separate Dockerfiles or write complex shell scripts to extract artifacts. Now, a single Dockerfile handles the entire lifecycle. When you understand this flow, optimizing becomes intuitive rather than experimental.

Build Stage (Discarded)Go Compiler / Node ModulesSource Code & HeadersBuild Cache (~800MB)Runtime Stage (Shipped)Static Binary Only (~15MB)COPY --from=build
Visualizing how to reduce Docker image size with multi-stage builds: only the artifact crosses the boundary between stages.

The mechanism relies on the COPY --from instruction. Each FROM directive starts a new stage. Previous stages remain accessible by index or alias during the build process but are completely excluded from the final image manifest. This means you can install terabytes of build dependencies in stage one, and if you never copy them forward, they contribute zero bytes to your deployable artifact. For teams managing containerized Laravel applications or Go microservices, this distinction is what separates professional-grade infrastructure from hobbyist setups.

How do you write an optimized multi-stage Dockerfile?

Theory matters less than correct syntax. A common mistake I see in code reviews is copying entire directories instead of specific artifacts, which defeats the purpose of optimization. Below is a production-grade pattern for a Go application, though the principles apply identically to Rust, Java, or compiled TypeScript.

# Stage 1: Build environment
FROM golang:1.23-alpine AS builder
WORKDIR /app

# Cache dependency downloads separately from source code
COPY go.mod go.sum ./
RUN go mod download

# Copy source and compile with static linking
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /server .

# Stage 2: Minimal runtime
FROM alpine:3.20
RUN apk add --no-cache ca-certificates tzdata
COPY --from=builder /server /usr/local/bin/server

EXPOSE 8080
USER nonroot:nonroot
ENTRYPOINT ["/usr/local/bin/server"]

Several critical details make this effective beyond just having two stages:

  • Dependency caching: Copying go.mod before source code ensures that unchanged dependencies don't trigger re-downloads when only application logic changes. This accelerates CI significantly.
  • Static linking: CGO_ENABLED=0 produces a binary with no libc dependency, allowing you to use scratch or distroless bases safely.
  • Strip symbols: The -ldflags="-s -w" flag removes debug tables, often reducing binary size by 30% without affecting functionality.
  • Non-root user: Security isn't optional. Running as root inside a container is a compliance failure waiting to happen, especially for SOC 2 environments.

Which base image should you choose for smallest footprint?

Selecting the right base image determines your floor size. No amount of multi-stage optimization will shrink an Ubuntu base below 70MB because of its fundamental filesystem structure. Here's how the options compare in 2026:

Base ImageSizeSecurity ProfileBest Use Case
scratch0 MBMaximum (empty FS)Statically linked Go/Rust binaries
gcr.io/distroless/static~3 MBHigh (no shell/pkgs)Production apps needing CA certs
alpine:3.20~8 MBMedium (musl libc)Apps requiring shell debugging
debian:bookworm-slim~75 MBStandard (glibc)Dynamic binaries, compatibility
ubuntu:24.04~78 MBStandard (glibc)Legacy apps, team familiarity

In my experience helping Nepali fintech companies achieve ISO 27001 certification, distroless images consistently pass vulnerability scans with zero findings because there's literally no package manager to exploit. Alpine remains popular but requires awareness of musl vs glibc differences that can cause subtle runtime bugs in DNS resolution or cryptography libraries. Always test thoroughly before switching bases in production.

How do layer caching and ordering affect final size?

Docker caches layers sequentially. Any change to a layer invalidates all subsequent layers. Poor ordering causes unnecessary rebuilds and occasionally results in larger images when failed builds leave orphaned cache entries. Structure your Dockerfile so that frequently changing content appears last within each stage.

✓ Stable: Base + Deps✓ Cached: Mod Files⚠ Rebuild: Source Copy✗ Invalidated: CompileOptimization Rules1. Dependencies before source2. Combine RUN commands3. Clean cache in same layer4. Use .dockerignore strictly5. Specific COPY paths only
Correct layer ordering prevents cache invalidation cascades when learning how to reduce Docker image size with multi-stage builds.

A frequent anti-pattern is running apt-get update and apt-get install in separate RUN instructions. If the install fails or you modify packages later, the update layer remains cached but stale, causing version mismatches. Always combine them: RUN apt-get update && apt-get install -y pkg && rm -rf /var/lib/apt/lists/*. The cleanup must happen in the same layer because Docker stores each layer immutably; deleting files in a subsequent layer only adds whiteout markers without reclaiming space.

Also enforce a strict .dockerignore file. Including .git, node_modules, or local environment files in your build context wastes transfer time and risks leaking secrets into image history. I've audited containers at Kathmandu startups where AWS credentials were baked into images because someone forgot to exclude .env files. Treat build context hygiene as a security control, not just a performance optimization.

How do you measure and validate image size reductions?

You cannot optimize what you do not measure. After implementing multi-stage builds, verify results quantitatively rather than assuming success. Use docker images for quick checks, but prefer dive for layer-by-layer analysis that reveals hidden bloat.

  1. Baseline measurement: Record original image size and layer count before refactoring.
  2. Implement multi-stage: Apply patterns above with appropriate base selection.
  3. Analyze with dive: Run dive your-image:tag to inspect each layer's contents and efficiency score.
  4. Check for wasted space: Look for modified files across layers (indicates poor cleanup) or unnecessary duplicates.
  5. Validate functionality: Run integration tests against the slimmed image to catch missing runtime dependencies.
  6. Automate in CI: Add size gates to your pipeline. Fail builds exceeding thresholds. This connects directly to broader cloud cost optimization tactics since ECR storage and data transfer fees scale with image volume.

For teams deploying to Kubernetes, smaller images mean faster pod scheduling and reduced node provisioning latency. When scaling events trigger during traffic spikes, pulling a 50MB image versus a 500MB image can be the difference between seamless autoscaling and user-visible errors. This matters especially for Kubernetes deployments where image pull time directly impacts horizontal pod autoscaler responsiveness.

Before: Single StageCompiler + Runtime: 850MBDev Dependencies: 320MBSource + Cache: 180MB1.35 GB TotalAfter: Multi-StageBinary + Certs: 45MB45 MB Total97% Reduction
Real-world results demonstrating how to reduce Docker image size with multi-stage builds typically achieve 90-97% size reduction.

Start Shipping Leaner Containers Today

Mastering how to reduce Docker image size with multi-stage builds isn't just about saving disk space—it's about building systems that are faster to deploy, cheaper to operate, and harder to compromise. Every megabyte you eliminate reduces your blast radius during incidents and shrinks your compliance audit scope. Start with your largest, most frequently deployed service. Measure the baseline, implement the patterns above, and validate both size and functionality before moving to the next workload. If your team needs hands-on guidance optimizing container workflows or preparing infrastructure for security audits, reach out to discuss your specific architecture.

Frequently Asked Questions

It uses multiple FROM statements to separate build dependencies from runtime artifacts, copying only necessary files to the final image.

Reductions of 80-95% are common, shrinking gigabyte build images to under 100MB by excluding compilers and dev libraries.

Initial builds take slightly longer due to extra stages, but cached layers and smaller push sizes usually improve total pipeline time.

Yes, compile assets and install Composer deps in stage one, then copy only vendor and public directories to a php-fpm-alpine final stage.

Alpine or distroless variants minimize attack surface and size, often resulting in final images under 50MB for Go or Node apps.

Name your build stages explicitly using AS syntax and verify source paths exist in that specific stage context before copying.

Yes, they exclude build tools, package managers, and source code from production images, significantly reducing the vulnerability footprint.

Structure Dockerfiles to install dependencies before copying source code so layer caching persists when application logic changes frequently.

Intermediate stages are discarded after the build completes; only the final stage or specifically targeted stages remain in the output.

Use build arguments for compile-time configuration and runtime environment variables for deployment settings to keep images generic and reusable.

Yes, specify target stage in your compose file to build development or production variants from the same multi-stage Dockerfile.

Check for unnecessary files copied via wildcard patterns, verify you are using slim base images, and inspect layers with dive tool.

Copy compiled binaries or vendor directories directly between stages instead of reinstalling, ensuring architecture compatibility across all stages.

Pass credentials as build secrets or mount SSH agents during build stages, never embedding tokens in final image layers.

Compare docker history output and use docker images to verify layer sizes before pushing to registry.