---
title: "Docker Multi-Stage Builds: Smaller, Safer Images for Production"
lang: "en"
author: "Mohammad Abu Mattar"
canonical: https://mkabumattar.com/post/docker-multi-stage-builds-smaller-production-images
---

![Blog post image for Docker Multi-Stage Builds: Smaller, Safer Images for Production - Ship lean, secure containers by splitting your build from your runtime. This dev tip shows how Docker multi-stage builds drop the compilers and dev dependencies, so your production image is smaller and has far less to attack.](/_astro/hero.CI-H9NMO_ZwWGpb.webp)

[Home](/)›[Devtips](/devtips)›[All Categories](/devtips/categories)›[Kubernetes & Containers](/devtips/categories/kubernetes--containers)

Devtips

[Prev in Kubernetes & ContainersArgoCD GitOps: Sync Kubernetes Deployments Automatically from Git](/devtips/post/argocd-gitops-kubernetes-deployments-git-sync)

[Kubernetes & Containers](/devtips/categories/kubernetes--containers)

# Docker Multi-Stage Builds: Smaller, Safer Images for Production

[Mohammad Abu Mattar](/authors/mohammad-abu-mattar)Published: 27 Jul 2026Updated: 27 Jul 202604 Mins read06 Mins listen

[Markdown for AI(opens in a new tab)](/post/docker-multi-stage-builds-smaller-production-images/index.md "Open the plain-Markdown version of this page, for pasting into an AI tool")

TL;DR

Ship lean, secure containers by splitting your build from your runtime. This dev tip shows how Docker multi-stage builds drop the compilers and dev dependencies, so your production image is smaller and has far less to attack.

Series

[DevOps & CI/CD Pipelines](/series/devops--cicd-pipelines)3/4

[PreviousArgoCD GitOps: Sync Kubernetes Deployments Automatically from Git](/devtips/post/argocd-gitops-kubernetes-deployments-git-sync)[NextGitHub Actions Secrets and Environment Variables: Handle Config the Right Way](/devtips/post/github-actions-secrets-environment-variables-guide)

All posts in this series (4)

DevTips4

1.  [Securing CI/CD with IAM Roles](/devtips/post/securing-cicd-with-iam-roles)
2.  [ArgoCD GitOps: Sync Kubernetes Deployments Automatically from Git](/devtips/post/argocd-gitops-kubernetes-deployments-git-sync)
3.  [Docker Multi-Stage Builds: Smaller, Safer Images for ProductionYou are here](/devtips/post/docker-multi-stage-builds-smaller-production-images)
4.  [GitHub Actions Secrets and Environment Variables: Handle Config the Right Way](/devtips/post/github-actions-secrets-environment-variables-guide)

### Docker Multi-Stage Builds: Smaller, Safer Images for Production

Contents

[Why multi-stage builds matter](#why-multi-stage-builds-matter)[Image size is really about what is inside](#image-size-is-really-about-what-is-inside)[Build tools and runtime are different jobs](#build-tools-and-runtime-are-different-jobs)[The problem with single-stage Dockerfiles](#the-problem-with-single-stage-dockerfiles)[What's the issue?](#whats-the-issue)[Real-world consequences](#real-world-consequences)[The multi-stage fix](#the-multi-stage-fix)[Here's how to fix it](#heres-how-to-fix-it)[Implementing it](#implementing-it)[Tools and platforms](#tools-and-platforms)[Quick implementation steps](#quick-implementation-steps)[Copy only what runs](#copy-only-what-runs)[Measure the before and after](#measure-the-before-and-after)[Benefits of multi-stage builds](#benefits-of-multi-stage-builds)[Why it helps](#why-it-helps)[Faster and safer deploys](#faster-and-safer-deploys)[What's your approach?](#whats-your-approach)[Community discussion](#community-discussion)[Share your experience](#share-your-experience)[References](#references)

## [Why multi-stage builds matter](#why-multi-stage-builds-matter)

### [Image size is really about what is inside](#image-size-is-really-about-what-is-inside)

**Hey, want to stop shipping a toolshed to production?** If your Dockerfile builds and runs the app in one stage, your final image is carrying everything you used to build it: the compiler, the package manager caches, the dev dependencies, all of it. None of that runs in production, but all of it ships anyway. That means slower pulls, slower cold starts, and a lot more code that a scanner has to worry about. Multi-stage builds let you build with all your tools, then throw the tools away and keep only the finished app.

### [Build tools and runtime are different jobs](#build-tools-and-runtime-are-different-jobs)

Building your app and running it need completely different things. The build needs compilers, headers, and dev packages. The runtime just needs your binary or bundled code and maybe a couple of shared libraries. Mixing the two into one image is the root of most bloated, insecure containers. Separating them is the whole idea here.

## [The problem with single-stage Dockerfiles](#the-problem-with-single-stage-dockerfiles)

### [What’s the issue?](#whats-the-issue)

A single-stage Dockerfile does everything in one place. You start from a full base image like `node:20` or `golang:1.22`, install dependencies, compile or bundle, and that same fat image becomes what you deploy. So your production container includes gcc, npm, git, and every dev dependency you only needed for a few seconds during the build. You are shipping a build machine and calling it a runtime.

### [Real-world consequences](#real-world-consequences)

The costs pile up quietly. Images balloon to a gigabyte or more, which slows every deploy and every autoscale event. Worse, every extra package is another thing that can have a CVE. When your scanner flags fifty vulnerabilities, most of them are usually in build tools your app never even calls at runtime. You end up patching things that should not have been in the image in the first place.

## [The multi-stage fix](#the-multi-stage-fix)

### [Here’s how to fix it](#heres-how-to-fix-it)

A multi-stage build splits your Dockerfile into named stages using `FROM ... AS`. You do all the heavy work in a build stage, then start a fresh, minimal final stage and pull across only the artifact you need with `COPY --from`. Everything left behind in the build stage, the compilers and dev dependencies, never makes it into the image you ship.

### [Implementing it](#implementing-it)

Here is a Go example. The first stage compiles the binary, and the second stage starts from a tiny base and copies just that binary.

Dockerfile

```
1# Build stage: has the full Go toolchain2FROM golang:1.22 AS build3WORKDIR /src4COPY . .5RUN CGO_ENABLED=0 go build -o /app ./cmd/server6
7# Production stage: minimal, no compiler8FROM gcr.io/distroless/static9COPY --from=build /app /app10ENTRYPOINT ["/app"]
```

The final image has no Go toolchain, no shell, and no package manager. It is just your binary on a distroless base. The same pattern works for Node: build and bundle in a `node:20` stage, then copy the `dist` folder and production `node_modules` into a slim runtime stage.

### [Tools and platforms](#tools-and-platforms)

BuildKit is the modern Docker builder and it runs independent stages in parallel and caches them well, so multi-stage builds stay fast. For final bases, reach for `distroless` images from Google or an `alpine` variant when you need a shell. Pair the build with a scanner like Trivy or Grype so you can see the vulnerability count drop after you strip the build tools out.

## [Quick implementation steps](#quick-implementation-steps)

**Quick takeaways** to slim down your images:

-   Split your Dockerfile into a build stage and a final stage with `FROM ... AS`.
-   Do all compiling and bundling in the build stage.
-   Start the final stage from a minimal base like distroless or alpine.
-   Copy only the finished artifact across with `COPY --from`.
-   Enable BuildKit so stages build in parallel and cache well.
-   Scan the final image and compare the size and CVE count before and after.

### [Copy only what runs](#copy-only-what-runs)

The trick is being strict about what crosses the `COPY --from` line. Bring over the compiled binary, the bundled assets, and production dependencies only. Leave source files, test files, and dev dependencies in the build stage where they belong. If you are not sure something is needed at runtime, it probably is not.

### [Measure the before and after](#measure-the-before-and-after)

Run `docker images` to see the size difference, and run your scanner on both versions. It is not unusual to go from over a gigabyte down to under a hundred megabytes, with the vulnerability count dropping right along with it. That comparison is the easiest way to convince a team the change is worth it.

## [Benefits of multi-stage builds](#benefits-of-multi-stage-builds)

### [Why it helps](#why-it-helps)

You get a production image that only contains what it needs to run. That means faster pulls, faster cold starts, and a much smaller attack surface. With no compilers, package managers, or dev dependencies inside, there is simply less for an attacker to use and less for a scanner to flag.

### [Faster and safer deploys](#faster-and-safer-deploys)

Smaller images move faster through your whole pipeline. Registries store less, nodes pull quicker, and autoscaling responds sooner because there is less to download before a pod starts. On the security side, a distroless final stage with no shell makes a compromised container far harder to pivot from, since there is barely anything in there to run.

## [What’s your approach?](#whats-your-approach)

### [Community discussion](#community-discussion)

**What’s your take?** Multi-stage builds are one of those changes that feel small but pay off every single deploy. If you have converted an old single-stage Dockerfile, how much smaller did the image get, and did your scan results improve?

### [Share your experience](#share-your-experience)

If you are running multi-stage builds in production, I would love to hear which final base you settled on. Distroless, alpine, or something custom, and how you handle the cases where you still need a shell for debugging.

## [References](#references)

-   [Docker multi-stage builds documentation](https://docs.docker.com/build/building/multi-stage/)
-   [Docker BuildKit](https://docs.docker.com/build/buildkit/)
-   [Distroless base images](https://github.com/GoogleContainerTools/distroless)
-   [Trivy vulnerability scanner](https://trivy.dev/)
-   [Best practices for writing Dockerfiles](https://docs.docker.com/build/building/best-practices/)

Was this useful?

## Tags

[#Docker](/devtips/tags/docker)[#Multi Stage Build](/devtips/tags/multi-stage-build)[#Container Image](/devtips/tags/container-image)[#Distroless](/devtips/tags/distroless)[#BuildKit](/devtips/tags/buildkit)[#Security](/devtips/tags/security)[#Image Optimization](/devtips/tags/image-optimization)

## Share

[Facebook](https://facebook.com/sharer/sharer.php?u=https%3A%2F%2Fmkabumattar.com%2Fdevtips%2Fpost%2Fdocker-multi-stage-builds-smaller-production-images "Share on Facebook")[Twitter](https://twitter.com/intent/tweet/?text=Docker%20Multi-Stage%20Builds%3A%20Smaller%2C%20Safer%20Images%20for%20Production&url=https%3A%2F%2Fmkabumattar.com%2Fdevtips%2Fpost%2Fdocker-multi-stage-builds-smaller-production-images "Share on Twitter")[LinkedIn](https://www.linkedin.com/shareArticle?mini=true&url=https%3A%2F%2Fmkabumattar.com%2Fdevtips%2Fpost%2Fdocker-multi-stage-builds-smaller-production-images&title=Docker%20Multi-Stage%20Builds%3A%20Smaller%2C%20Safer%20Images%20for%20Production&summary=Ship%20lean%2C%20secure%20containers%20by%20splitting%20your%20build%20from%20your%20runtime.%20This%20dev%20tip%20shows%20how%20Docker%20multi-stage%20builds%20drop%20the%20compilers%20and%20dev%20dependencies%2C%20so%20your%20production%20image%20is%20smaller%20and%20has%20far%20less%20to%20attack.&source=https://mkabumattar.com "Share on LinkedIn")[WhatsApp](https://wa.me/?text=Docker%20Multi-Stage%20Builds%3A%20Smaller%2C%20Safer%20Images%20for%20Production%20https%3A%2F%2Fmkabumattar.com%2Fdevtips%2Fpost%2Fdocker-multi-stage-builds-smaller-production-images "Share on WhatsApp")[Telegram](https://t.me/share/url?url=https%3A%2F%2Fmkabumattar.com%2Fdevtips%2Fpost%2Fdocker-multi-stage-builds-smaller-production-images&text=Docker%20Multi-Stage%20Builds%3A%20Smaller%2C%20Safer%20Images%20for%20Production "Share on Telegram")[Reddit](https://www.reddit.com/submit?url=https%3A%2F%2Fmkabumattar.com%2Fdevtips%2Fpost%2Fdocker-multi-stage-builds-smaller-production-images&title=Docker%20Multi-Stage%20Builds%3A%20Smaller%2C%20Safer%20Images%20for%20Production "Share on Reddit")[Hacker News](http://news.ycombinator.com/submitlink?u=https%3A%2F%2Fmkabumattar.com%2Fdevtips%2Fpost%2Fdocker-multi-stage-builds-smaller-production-images&t=Docker%20Multi-Stage%20Builds%3A%20Smaller%2C%20Safer%20Images%20for%20Production "Share on Hacker News")[Pinterest](https://pinterest.com/pin/create/button/?url=https%3A%2F%2Fmkabumattar.com%2Fdevtips%2Fpost%2Fdocker-multi-stage-builds-smaller-production-images&media=&description=Ship%20lean%2C%20secure%20containers%20by%20splitting%20your%20build%20from%20your%20runtime.%20This%20dev%20tip%20shows%20how%20Docker%20multi-stage%20builds%20drop%20the%20compilers%20and%20dev%20dependencies%2C%20so%20your%20production%20image%20is%20smaller%20and%20has%20far%20less%20to%20attack. "Share on Pinterest")[Email](<mailto:?subject=Docker%20Multi-Stage%20Builds%3A%20Smaller%2C%20Safer%20Images%20for%20Production&body=Check out this article: https%3A%2F%2Fmkabumattar.com%2Fdevtips%2Fpost%2Fdocker-multi-stage-builds-smaller-production-images>)

## Comments

## You might also enjoy

More posts on similar topics

[![ArgoCD GitOps: Sync Kubernetes Deployments Automatically from Git](/_astro/hero.D4Pcicvc_VlKXt.webp)](/devtips/post/argocd-gitops-kubernetes-deployments-git-sync)

## [ArgoCD GitOps: Sync Kubernetes Deployments Automatically from Git](/devtips/post/argocd-gitops-kubernetes-deployments-git-sync)

-   [Mohammad Abu Mattar](/authors/mohammad-abu-mattar)
-   [Kubernetes & Containers](/devtips/categories/kubernetes--containers)

Why GitOps for Kubernetes? From kubectl apply to Git as the source of truth Hey, want to stop deploying to Kubernetes by hand? If your releases still come from someone running \`kubectl ap

[#ArgoCD](/devtips/tags/argocd)[#GitOps](/devtips/tags/gitops)[#Kubernetes](/devtips/tags/kubernetes)+4 tags

[read more](/devtips/post/argocd-gitops-kubernetes-deployments-git-sync)

[![GitHub Actions Secrets and Environment Variables: Handle Config the Right Way](/_astro/hero.DSfz34Ly_ZH1GxC.webp)](/devtips/post/github-actions-secrets-environment-variables-guide)

## [GitHub Actions Secrets and Environment Variables: Handle Config the Right Way](/devtips/post/github-actions-secrets-environment-variables-guide)

-   [Mohammad Abu Mattar](/authors/mohammad-abu-mattar)
-   [DevOps & DevSecOps](/devtips/categories/devops--devsecops)

Why secrets handling matters Most CI leaks are config mistakes, not attacks Hey, want to stop leaking credentials in your pipelines? Most secret leaks in CI are not the result of some cle

[#GitHub Actions](/devtips/tags/github-actions)[#Secrets](/devtips/tags/secrets)[#Environment Variables](/devtips/tags/environment-variables)+4 tags

[read more](/devtips/post/github-actions-secrets-environment-variables-guide)

[![Securing CI/CD with IAM Roles](/_astro/hero.Bl9B2DZz_ZDIuXQ.webp)](/devtips/post/securing-cicd-with-iam-roles)

## [Securing CI/CD with IAM Roles](/devtips/post/securing-cicd-with-iam-roles)

-   [Mohammad Abu Mattar](/authors/mohammad-abu-mattar)
-   [DevOps & DevSecOps](/devtips/categories/devops--devsecops)

Why secure your CI/CD pipeline? Why pipeline security matters Your pipeline holds credentials for every environment you deploy to, which makes it one of the most valuable targets you own. A s

[#CICD Security](/devtips/tags/cicd-security)[#IAM Roles](/devtips/tags/iam-roles)[#Least Privilege](/devtips/tags/least-privilege)+4 tags

[read more](/devtips/post/securing-cicd-with-iam-roles)

[![Docker Is Eating Your Disk Space (And How PruneMate Fixes It)](/_astro/hero.BD8-gtdG_26qrXW.webp)](/devtips/post/docker-disk-space-prunemate)

## [Docker Is Eating Your Disk Space (And How PruneMate Fixes It)](/devtips/post/docker-disk-space-prunemate)

-   [Mohammad Abu Mattar](/authors/mohammad-abu-mattar)
-   [Containers & Docker](/devtips/categories/containers--docker)

The problem: Docker is eating your disk space What it looks like when it happens Your Docker host is running out of space. Again. You've been spinning up containers, testing new services

[#Docker](/devtips/tags/docker)[#Containers](/devtips/tags/containers)[#Home Lab](/devtips/tags/home-lab)+5 tags

[read more](/devtips/post/docker-disk-space-prunemate)

[![Container Image Vulnerability Scanning in CI/CD with Trivy](/_astro/hero.yY1orHlw_2oq3jw.webp)](/devtips/post/container-image-vulnerability-scanning-trivy)

## [Container Image Vulnerability Scanning in CI/CD with Trivy](/devtips/post/container-image-vulnerability-scanning-trivy)

-   [Mohammad Abu Mattar](/authors/mohammad-abu-mattar)
-   [DevOps & DevSecOps](/devtips/categories/devops--devsecops)

Why container security matters Where the vulnerabilities hide A container image is one of the largest pieces of untrusted code you ship. Every image you build carries the base OS layer,

[#Container Security](/devtips/tags/container-security)[#Vulnerability Scanning](/devtips/tags/vulnerability-scanning)[#Trivy](/devtips/tags/trivy)+4 tags

[read more](/devtips/post/container-image-vulnerability-scanning-trivy)

[![Terraform Workspaces vs. Directory-Based Environments: What Actually Scales](/_astro/hero.utyGomxG_Z2cpb8C.webp)](/devtips/post/terraform-workspaces-vs-directory-environments)

## [Terraform Workspaces vs. Directory-Based Environments: What Actually Scales](/devtips/post/terraform-workspaces-vs-directory-environments)

-   [Mohammad Abu Mattar](/authors/mohammad-abu-mattar)
-   [Cloud & Infrastructure Automation](/devtips/categories/cloud--infrastructure-automation)

Why this choice matters Hey, want to stop sweating every prod apply? The way you split dev, staging, and prod in Terraform decides how much damage a single mistake can do. Get it right and a

[#Terraform](/devtips/tags/terraform)[#Workspaces](/devtips/tags/workspaces)[#Environments](/devtips/tags/environments)+3 tags

[read more](/devtips/post/terraform-workspaces-vs-directory-environments)

6 related posts
