Adetayo Akinsanya unkletayo.dev

Multi-Stage Build Architectures & Minimal Base Image Optimization

Reducing production image sizes from 1.4GB to 12MB while eliminating security vulnerabilities.

Adetayo Akinsanya (unkletayo) 2026-09-08

Part 7 in Series — Catch up on the previous article: Dockerfile Instructions and Layer Mechanics: How Build Caching Works Under the Hood (Part 6) before diving into this post.

A security audit scans a production Go microservice container image and flags 412 high-severity Common Vulnerabilities and Exposures (CVEs).

The image size is 1.4 Gigabytes.

Inspection reveals why: the production image contains the full Go compiler suite (go), gcc, make, header files (glibc-devel), package managers (apt), text editors, and debugging tools.

None of these build tools are needed to run the compiled binary in production. They exist in the image solely because the build step required them.

By refactoring the Dockerfile into a Multi-Stage Build Pipeline:

# Stage 1: Build Stage (Includes compiler)
FROM golang:1.21-alpine AS builder
WORKDIR /app
COPY . .
RUN CGO_ENABLED=0 go build -o server .

# Stage 2: Production Stage (Minimal Scratch Runtime)
FROM scratch
COPY --from=builder /app/server /server
ENTRYPOINT ["/server"]

The resulting production image size collapses from 1.4 Gigabytes down to 12 Megabytes, and the CVE count drops from 412 down to ZERO.

How do Multi-Stage builds and minimal base images eliminate bloat and harden container security?


1. The Single-Stage Anti-Pattern

In traditional single-stage Dockerfiles, every instruction (RUN apt-get install, COPY, RUN make) appends a new read-only layer to the final image.

+---------------------------------------------------------------+
| Layer 4: Compiled App Binary (12 MB)                          |
+---------------------------------------------------------------+
| Layer 3: Build Artifacts & Object Files (350 MB) [UNNEEDED]   |
+---------------------------------------------------------------+
| Layer 2: Go SDK & GCC Compilers (800 MB) [UNNEEDED]           |
+---------------------------------------------------------------+
| Layer 1: Ubuntu Base OS (75 MB) [UNNEEDED]                    |
+---------------------------------------------------------------+
FINAL IMAGE SIZE: 1.24 GB (Contains attack surface & bloat!)

Consequences of Large Images:

  • Increased Attack Surface: Extra utilities (curl, bash, python) give attackers post-exploitation tools inside compromised containers.
  • Slow Deployment Latency: Pulling 1.4GB images across Kubernetes clusters during autoscaling events delays pod startup times by minutes.
  • High Storage Costs: Storing thousands of 1.4GB image versions in container registries consumes terabytes of cloud storage.

2. Multi-Stage Build Architecture

Multi-Stage builds allow you to use multiple FROM instructions in a single Dockerfile.

Each FROM instruction begins a new, clean build stage with a fresh root filesystem. You selectively copy only the compiled binary artifacts from earlier build stages into the final runtime stage.

[ STAGE 1: Builder Stage ]                      [ STAGE 2: Runtime Stage ]

 FROM golang:1.21 AS builder                     FROM gcr.io/distroless/static
 - Installs dependencies                         - Clean minimal environment
 - Compiles source code                           
 - Generates /app/binary --( COPY --from=builder )--> Contains ONLY /app/binary
                                                 
                                                 FINAL IMAGE SIZE: 15 MB

Syntax and Workflow Example

# ===================================================
# STAGE 1: Compilation Stage
# ===================================================
FROM node:18-alpine AS build-stage
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build  # Generates static HTML/JS assets in /app/dist

# ===================================================
# STAGE 2: Production Web Server Stage
# ===================================================
FROM nginx:1.25-alpine AS production-stage

# Copy ONLY static web assets from build-stage
COPY --from=build-stage /app/dist /usr/share/nginx/html

EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

During execution, BuildKit compiles the application in build-stage, discards the 900MB Node.js SDK and node_modules directory, and copies only the 4MB /app/dist assets into the lightweight Nginx image.


3. Minimal Base Image Options

Selecting the right production base image determines container security, glibc compatibility, and final image size.

Image Size (MB)
  150MB +-----------------------------------------------------------+
        |  Debian / Ubuntu (~120MB)                                 |
  50MB  +-----------------------------------+                       |
        |  Alpine Linux (~5MB)              |                       |
  5MB   +-------------------+---------------+                       |
        | Distroless (~2MB) |               |                       |
  0MB   +-------------------+---------------+-----------------------+
        scratch (0MB)       Distroless      Alpine      Debian/Ubuntu

A. Full OS Images (ubuntu:22.04, debian:bookworm)

  • Size: 75MB – 150MB.
  • Pros: Includes full glibc, package managers (apt), and standard Linux CLI utilities. High developer familiarity.
  • Cons: Large memory footprint, higher CVE count.

B. Alpine Linux (alpine:3.18)

  • Size: ~5MB.
  • Pros: Ultra-lightweight, minimal default package set.
  • Cons: Uses musl libc instead of GNU glibc. C/C++ or Python C-extensions compiled against glibc may encounter runtime segmentation faults or performance degradation unless recompiled.

C. Google Distroless (gcr.io/distroless/static or base)

  • Size: ~2MB – 20MB.
  • Pros: Contains only your application and runtime dependencies (like glibc or OpenJDK). Contains no package manager, no shell (/bin/sh), and no text editors.
  • Cons: Cannot execute docker exec -it container sh to debug live containers without ephemeral debug containers.

D. Scratch (scratch)

  • Size: 0MB (An empty explicit base image).
  • Pros: Zero overhead. Ideal for statically linked C, Go, or Rust binaries (CGO_ENABLED=0).
  • Cons: Contains no SSL root certificates (/etc/ssl/certs) or timezone databases unless manually copied into the image!

Base Image Comparison Matrix

Base ImageSizeStandard C LibraryIncludes Shell (/bin/sh)?Package ManagerRecommended For
ubuntu:22.04~75 MBglibcYesaptComplex legacy apps requiring standard Linux toolchains
alpine:3.18~5 MBmusl libcYesapkLightweight microservices tolerant of musl libc
distroless/base~20 MBglibcNoNoneSecure production Java, Python, Node, or C++ applications
scratch0 MBNoneNoNoneStatically compiled Go, Rust, or C binaries

Summary & Next Steps

Multi-stage builds decouple application compilation from runtime execution:

  • Multi-Stage Builds isolate build-time dependencies (SDKs, compilers) from production runtimes using selective COPY --from=stage steps.
  • Minimal Base Images (Alpine, Distroless, Scratch) reduce container footprints from gigabytes to megabytes while eliminating CVE vulnerabilities.
  • Static Binaries on scratch offer the highest security posture for compiled microservices.

In the next article, we inspect Container Registries: Content-Addressable Storage, Push/Pull Protocols, Tags vs Digests.

References & Further Reading

  1. IETF. RFC 7348 — Virtual eXtensible Local Area Network (VXLAN). Internet Engineering Task Force.
  2. Linux Net-Tools. ip-link(8) — Network Device Configuration (Bridge, Macvlan, veth). Linux Man Pages.
  3. Docker Inc. Docker Container Networking Architecture (CNM). Docker Docs.

Up Next in Series →

Part 8: Container Registries: Content-Addressable Storage, Push/Pull Protocols, Tags vs Digests

Continue to Part 8 →