Back to articles
Technology Insight

Optimizing Docker Builds: Leveraging BuildKit and Multi-Stage Builds to Reduce Image Size by 90%

June 1, 2026

Introduction: The Cost of Bloated Docker Images

In modern cloud-native architecture, containerization has become the standard for deploying applications. However, a common anti-pattern plaguing development teams is the creation of bloated Docker images. It is not uncommon to see a simple Node.js or Go production application packaged into an image exceeding 1 GB in size. This inflation impacts cloud storage costs, slows down CI/CD pipelines, increases deployment latency, and significantly expands the attack surface of your production environment.

To achieve high-velocity software delivery, optimizing Docker images is no longer optional; it is a business imperative. This comprehensive guide explores two powerful native Docker features—BuildKit and Multi-stage Builds—and demonstrates how combining them can reduce your production image size by up to 90% while simultaneously boosting build performance.

The Core Challenge: Why Do Docker Images Get So Big?

To fix image bloat, we must first understand its root causes. Every instruction in a standard Dockerfile (such as RUN, COPY, and ADD) creates a new immutable layer. If you download a dependency, compile your code, and then delete the temporary installers in a subsequent layer, those files still occupy space in the underlying layers of the final image.

Furthermore, development environments require comprehensive toolchains. To build a Java or Go application, you need SDKs, compilers, package managers, and debugging utilities. However, your production runtime environment only needs the compiled binary or the minimized runtime files. Carrying heavy build dependencies into production is the primary culprit behind oversized images.

The Solution Part 1: Revolutionizing Builds with BuildKit

Introduced to accelerate the image creation process, BuildKit is Docker's next-generation build engine. Replacing the legacy builder, BuildKit introduces a highly efficient, concurrent execution engine that fundamentally changes how Dockerfiles are parsed and processed.

Key Advantages of BuildKit

  • Concurrent Execution: Unlike the legacy builder which processes instructions strictly linearly, BuildKit analyzes the dependency graph of your build stages and executes independent stages in parallel.
  • Advanced Caching Mechanics: BuildKit tracks build definitions and file contents more precisely, ensuring that cache hits are maximized even across distributed CI/CD runners.
  • Skip Unused Stages: If a specific stage in a multi-stage build does not contribute to the final target image, BuildKit intelligently skips executing it entirely, saving massive amounts of compute time.
  • Secure Secret Mounting: BuildKit allows you to safely mount build-time secrets (like SSH keys or NPM tokens) without baking them into the final image layers via the --mount=type=secret flag.

To enable BuildKit, you can simply set an environment variable before running your build command:

export DOCKER_BUILDKIT=1
docker build -t my-app:optimized .

The Solution Part 2: Architecting Multi-Stage Builds

While BuildKit optimizes the execution of the build, Multi-stage Builds allow you to optimize the structure of the resulting image. Introduced in Docker 17.05, multi-stage builds enable you to use multiple FROM statements in a single Dockerfile.

Each FROM instruction begins a brand-new stage of the build, utilizing a completely different base image if necessary. This architecture allows you to separate the build lifecycle into distinct phases:

  1. The Build Stage: A heavy, fully-equipped environment containing all SDKs, compilers, development headers, and testing libraries needed to compile the application.
  2. The Production/Runtime Stage: A minimal, stripped-down environment containing only the bare essentials required to execute the pre-compiled application artifacts generated in the build stage.

Artifacts are selectively transferred from the build stage to the runtime stage using the COPY --from syntax, completely discarding the heavy compilation layers and tools.

Step-by-Step Practical Implementation: Node.js Optimization

Let us look at a practical, real-world comparison using a standard Node.js application to see how these concepts translate into drastic space savings.

The Traditional (Anti-Pattern) Dockerfile

Consider a standard setup where developers use a single heavy image for everything:

FROM node:20
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
EXPOSE 3000
CMD ["npm", "start"]

Resulting Image Size: Approximately 1.2 GB. This image contains the full Ubuntu-based Node image, build tools, development dependencies (like TypeScript, linters, and testing frameworks), and the local source cache.

The Optimized Multi-Stage Dockerfile with BuildKit Optimization

Now, let us restructure this utilizing Multi-stage builds and BuildKit's advanced caching mechanisms:

# Stage 1: The Builder Environment
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
# Using BuildKit cache mount for npm cache to accelerate subsequent builds
RUN --mount=type=cache,target=/root/.npm npm ci
COPY . .
RUN npm run build
# Prune node_modules to keep only production dependencies
RUN npm prune --production

# Stage 2: The Slim Production Environment
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
# Copy only the compiled production artifacts and pruned node_modules
COPY --from=builder /app/package.json ./package.json
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist

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

Resulting Image Size: Approximately 120 MB. By adopting this multi-stage approach, we have successfully achieved a 90% reduction in image size.

Deep Dive into the Benefits of the 90% Size Reduction

Shrinking your production images yields massive downstream operational benefits that directly impact your organization's bottom line and security posture.

1. Enhanced Security and Vulnerability Mitigation

Production environments should adhere to the principle of least privilege. Heavy base images bundle system packages, shells (like Bash), and package managers (like apt or apk). If an attacker compromises your application, they can exploit these built-in utilities to perform lateral movement or download malicious payloads. By stripping the final runtime image down to just your application binary and minimal runtime libraries (or using distroless images), you radically minimize the attack surface. Security scanners will report significantly fewer CVEs (Common Vulnerabilities and Exposures), simplifying compliance auditing.

2. Accelerated CI/CD Pipelines

In modern automated pipelines, images must be pushed to a container registry (such as AWS ECR, Docker Hub, or GitHub Packages) and subsequently pulled by orchestrators like Kubernetes during deployment. Transferring a 1.2 GB image takes considerable time, bottlenecking your deployment frequency. Transferring a 120 MB image happens in seconds. This speed drastically reduces the feedback loop for development teams and enables rapid auto-scaling responses during traffic spikes.

3. Drastic Reduction in Cloud Infrastructure Costs

While disk storage is relatively inexpensive, maintaining thousands of historical image tags in private container registries scales costs quickly. Furthermore, cross-availability zone network data transfer charges incurred when pulling large images into cluster nodes can quietly become a massive component of your cloud bill. Reducing image mass directly scales down network and storage expenses.

Best Practices for Writing High-Efficiency Dockerfiles

To consistently hit the 90% reduction mark across all your engineering teams, institutionalize the following architectural practices:

  • Leverage Minimal Base Images: Always prefer specific minimal tags such as -alpine or Google's distroless images for your final production stages instead of default generic OS tags.
  • Order Layers Strategically: Put instructions that change frequently (like copying source code) as low down in the Dockerfile as possible. Place instructions that rarely change (like installing OS dependencies) at the top to optimize layer caching.
  • Utilize .dockerignore Explicitly: Prevent unnecessary local files, such as .git directories, local build logs, documentation, and local node_modules, from entering the build context by creating an exhaustive .dockerignore file.
  • Consolidate RUN Instructions: If you are not utilizing BuildKit cache mounts, make sure to chain related commands together using && and clean up temporary caches within the exact same RUN layer to avoid orphan data leakage.

Conclusion: Start Optimizing Today

Combining Docker BuildKit and Multi-stage builds represents the gold standard for container optimization. It allows developers to maintain robust, fully-equipped development ecosystems without compromising the agility, security, and performance of production environments. Implementing these architectural changes requires minimal engineering effort but provides immediate structural returns in infrastructure performance, cloud savings, and deployment reliability.

Optimizing Docker Builds: Leveraging BuildKit and Multi-Stage Builds to Reduce Image Size by 90% | DPTCloud