Cut Your CI/CD Costs by 40% with Intelligent Multi-Stage Docker Builds
Are your CI/CD pipelines slow and expensive due to bloated Docker images? Fixing this requires a structural change to how you package applications.
CI/CD pipelines burning through compute credits usually point to one culprit: bloated, sequentially processed Docker builds. Shipping massive images full of compilers inflates transfer times, drags down deployment speed, and drives up cloud costs. Fixing this requires a structural change to how you package applications.
TL;DR: Multi-stage builds and external layer caching in Docker 26.0 can slash your CI/CD runner costs by 40%. By isolating build dependencies from runtime environments and intelligently pulling previously built layers, you stop paying compute time to compile unchanged code. This post gives you the exact Dockerfile diff and CLI flags to implement this today.
What you’ll walk away with:
- A drop-in
Dockerfilestructure that strips compilers and test dependencies from your production image. - The explicit
docker buildsyntax for configuring remote registry caching. - A strict layer-ordering checklist that prevents cache invalidation on frequent code changes.
Why Are Monolithic Docker Builds So Slow and Expensive?
Monolithic Docker builds pull every tool, library, and source file into a single image, forcing the CI runner to rebuild massive layers on every commit. This bloats the final artifact and ruins cache efficiency, directly inflating network egress and compute costs.
Layer Caching is Docker’s mechanism of saving the output of each instruction as an intermediate image so subsequent builds can reuse it instead of re-executing the step. By default, ephemeral CI platforms throw this local cache away the moment the runner terminates. You have to explicitly design your Dockerfile to maximize cache hits and configure external storage to persist them.
Monolithic Dockerfiles force you to ship heavy tools like
gccornpmdirectly into production. Every single source code tweak invalidates the cache for the entire build step, driving build times from seconds to minutes.
Moving to a multi-stage approach changes the math. You isolate the build process from the runtime environment completely. Aicademy cut their Go service build times by 60% and reduced image size from 850MB to 25MB using this exact strategy, validating the concepts outlined in Cut Your CI/CD Build Times by 60%: Old Cache Busting vs. Intelligent Layering.
flowchart LR
subgraph Monolithic["Monolithic Build"]
A["Source Code"] --> B["Build Tools (gcc, go)"]
B --> C["Massive Production Image"]
end
subgraph Multi-Stage["Multi-Stage Build"]
D["Source Code"] --> E["Builder Stage"]
E -->|"Compiled Binary Only"| F["Minimal Final Image"]
end
Never install build tools in your final production image; they permanently inflate your cloud egress costs and unnecessarily expand your CVE surface area.
How Do You Implement Multi-Stage Builds in Docker?
You implement multi-stage builds by using multiple FROM statements within a single Dockerfile. The initial stage compiles the application using all necessary toolchains, while the final stage selects a minimal base image and copies only the compiled binary into it.
I recommend defaulting to a scratch or distroless base for your final stage unless you strictly require a shell for production debugging. Standard Alpine images still bundle package managers and shells that you do not need in production. You can review the security implications of this in Docker’s ‘Minimal Image’ Myth: Why Alpine Isn’t Always Your Smallest or Safest Bet.
Here is the explicit change required to convert a monolithic Go application into a multi-stage build:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
- FROM golang:1.22
- WORKDIR /app
- COPY . .
- RUN go build -o main .
- CMD ["./main"]
+ FROM golang:1.22 AS builder
+ WORKDIR /app
+ COPY go.mod go.sum ./
+ RUN go mod download
+ COPY . .
+ RUN CGO_ENABLED=0 go build -o main .
+ FROM gcr.io/distroless/static-debian12
+ COPY --from=builder /app/main /main
+ CMD ["/main"]
The AS builder syntax names the first stage, allowing the second stage to reference it directly via the --from=builder flag. The final artifact discards the entire 800MB Golang toolchain.
Always name your intermediate stages explicitly using
AS <name>to prevent referencing ambiguous stage indexes later in your Dockerfile.
What Is the Best Way to Use Docker Build Cache in CI?
The best way to use build cache in CI is by configuring an external cache backend using the --cache-from and --cache-to flags. This prevents ephemeral CI runners from starting from zero, pulling previously built layers directly from your container registry.
Docker 26.0 supports the registry cache backend out of the box via Buildx. This backend pushes cache manifests and layers to a dedicated tag in your OCI registry alongside your actual image.
Run this command in your CI pipeline to utilize remote caching:
1
2
3
4
5
docker buildx build \
--push \
--cache-to type=registry,ref=registry.example.com/myapp:buildcache,mode=max \
--cache-from type=registry,ref=registry.example.com/myapp:buildcache \
-t registry.example.com/myapp:latest .
The output confirms the cache backend is fetching remote layers:
1
2
3
4
5
6
7
8
9
10
[+] Building 3.2s (11/11) FINISHED
=> [internal] load build definition from Dockerfile
=> => transferring dockerfile: 341B
=> [auth] sharing credentials for registry.example.com
=> [internal] load metadata for gcr.io/distroless/static-debian12
=> importing cache manifest from registry.example.com/myapp:buildcache
=> CACHED [builder 1/5] FROM docker.io/library/golang:1.22
=> CACHED [builder 2/5] WORKDIR /app
=> CACHED [builder 3/5] COPY go.mod go.sum ./
=> CACHED [builder 4/5] RUN go mod download
You have multiple options for cache backends, but registry caching is the most reliable for ephemeral environments.
| Cache Backend | Setup Complexity | Runner Persistence | Winner/Best For |
|---|---|---|---|
inline |
Low | Minimal | Legacy registries lacking OCI support |
local |
Medium | Fails on ephemeral VMs | Winner: Local development |
registry |
Low | High | Winner: Ephemeral CI/CD pipelines |
Set
--cache-to mode=maxto ensure Docker caches intermediate multi-stage layers, not just the layers belonging to the final output image.
How Do You Optimize Dockerfile Instruction Order?
Optimize instruction order by placing files that change rarely at the top of your Dockerfile and frequently changed files at the bottom. You must copy dependency manifests and install packages before copying the actual application source code to maximize cache hits.
Docker processes instructions sequentially. If an instruction’s cache is invalidated, Docker invalidates the cache for all subsequent instructions. If you copy your entire repository before downloading dependencies, changing a single character in a README file forces the CI runner to re-download hundreds of megabytes of packages.
Follow this checklist to guarantee optimal layer caching:
-
Base image declarations (
FROM) at the very top. -
Global environment variables (
ENV) declared immediately after. -
System-level dependencies installed via
aptorapk. -
Application package manifests copied (
package.json,go.mod,requirements.txt). -
Package manager installation step executed (
npm install,pip install). -
Application source code copied (
COPY . .). - Compilation or build steps executed.
This sequential invalidation logic heavily dictates performance across all modern cloud tooling, which we detail further in How CloudFormation Express Mode Finally Accelerates Your Infrastructure Deployments.
View verbose Docker cache miss output (Bad Layer Ordering)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#1 [internal] load build definition from Dockerfile
#1 transferring dockerfile: 310B done
#1 DONE 0.0s
#2 [internal] load .dockerignore
#2 transferring context: 2B done
#2 DONE 0.0s
#3 [internal] load build context
#3 transferring context: 1.5MB done
#3 DONE 0.1s
#4 [1/4] FROM docker.io/library/node:20
#4 DONE 0.0s
#5 [2/4] WORKDIR /app
#5 CACHED
#6 [3/4] COPY . .
#6 DONE 0.2s <-- Cache invalidated here due to source code change
#7 [4/4] RUN npm install
#7 ... downloading 450MB of packages (takes 45s instead of 0s)
#7 DONE 45.1s
Split your
COPYcommands: dedicate the first strictly to package manifests, run your dependency install, and use a secondCOPYfor the remaining source files.
Bottom Line
Your CI/CD pipeline costs scale linearly with your build times and image sizes. Implementing multi-stage architectures with remote layer caching drastically reduces both metrics by eliminating redundant compilation steps. Update your Dockerfiles today to isolate build tools, enforce strict layer ordering, and direct Docker Buildx to a remote registry cache.
FAQ
What is the maximum number of stages you can have in a multi-stage Dockerfile?
There is no hard limit to the number of FROM statements you can use. However, keeping it under five stages prevents maintenance overhead and keeps the build graph readable.
Why does my build cache invalidate when copying the exact same files?
Docker evaluates file metadata, including timestamps and permissions, to determine cache validity. If your CI system checks out source code without preserving timestamps (like default Git checkouts), Docker treats the files as modified.
Can I use GitHub Actions cache instead of a registry cache?
Yes, Docker Buildx supports a gha cache backend specifically for GitHub Actions. You implement it using --cache-to type=gha and --cache-from type=gha, which utilizes the GitHub Actions Cache API directly.
Does multi-stage building work with older Docker versions?
Multi-stage builds require Docker 17.05 or higher. The advanced registry caching features via Buildx are natively available in Docker 26.0 and newer.
How do I debug which Dockerfile instruction broke the cache?
Run your build command without caching and examine the terminal output. The step immediately following the last CACHED label is the instruction that invalidated the cache.
Further Reading
- https://docs.docker.com/build/building/multi-stage/
- https://docs.docker.com/build/cache/
- https://www.docker.com/blog/speeding-up-ci-cd-with-docker-build-caching/
🚀 Ready to get hands-on? Spin up an interactive AI or Kubernetes Sandbox at Aicademy Labs for free.
