Skip to main content

Optimizing Production Images with Docker Multi-Stage Builds

In production container workloads, image size directly impacts deployment speed, storage costs, and security posture. Docker multi-stage builds can compress production images from over 1GB down to under 10MB — a technique that has become essential rather than optional for modern container workflows.

At Kubo, we provide Kubernetes infrastructure for production container workloads where image optimization directly contributes to cluster resource efficiency. This article covers everything from multi-stage build fundamentals to advanced optimization patterns.

How Multi-Stage Builds Work

Multi-stage builds use multiple FROM instructions within a single Dockerfile to completely separate build environments from runtime environments. The Docker official documentation describes this as achieving "a cleaner separation between the building of your image and the final output."

In traditional single-stage builds, compilers, build tools, and development libraries all end up in the final image. With multi-stage builds, only build artifacts are copied to the final stage, preventing unnecessary files from contaminating production images.

dockerfile
# Stage 1: Build environment
FROM golang:1.22 AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o /app/server

# Stage 2: Production environment (minimal image)
FROM gcr.io/distroless/static-debian12
COPY --from=builder /app/server /server
ENTRYPOINT ["/server"]

In this example, the build stage uses golang:1.22 (~800MB) while the final image consists of only distroless (~2MB) plus the compiled binary. According to a 2026 report from the Open Container Initiative, images built with multi-stage workflows show a 40% reduction in known vulnerabilities compared to traditional builds.

Base Image Selection Strategy

The choice of base image for your final stage has the greatest impact on both image size and security. Let's compare the main options.

Base ImageSizeShellPackage ManagerRecommended For
ubuntu:24.04~77MBYesaptDevelopment/Debugging
alpine:3.21~5MBYesapkLightweight with debugging
distroless~2-20MBNoNoJava/Python/Node production
scratch0MBNoNoGo/Rust static binaries

Google's distroless project produces images containing only your application and its runtime dependencies. With no shell or package manager present, the attack surface is dramatically reduced.

For languages like Go and Rust that produce static binaries, scratch is optimal. For languages requiring dynamic libraries — Java, Python, Node.js — distroless is the practical choice. The Alpine vs Distroless comparison on Medium provides additional guidance on this decision.

With Captain.AI, AI can automatically recommend the optimal base image for your project and assist with Dockerfile optimization.

Maximizing Layer Cache Efficiency

Docker caches each instruction as a layer. By placing infrequently changing instructions first and frequently changing ones last, you can dramatically reduce rebuild times.

dockerfile
# Bad: Dependencies reinstalled on every source code change
COPY . /app
RUN npm install

# Good: Cache dependencies separately from source code
COPY package.json package-lock.json /app/
RUN npm install
COPY . /app/

The Docker official best practices recommend these caching strategies:

  • Combine apt-get update with apt-get install in a single RUN instruction. Separating them causes stale package lists due to caching.
  • Pin versions for reproducibility (e.g., alpine:3.21@sha256:...)
  • Use the --pull flag to fetch the latest base image versions
  • Leverage BuildKit's parallel builds — independent stages execute concurrently automatically

BuildKit analyzes Dockerfiles as dependency graphs and automatically parallelizes independent stages and steps, significantly reducing end-to-end build times.

bash
# Enable BuildKit for builds
DOCKER_BUILDKIT=1 docker build -t myapp:latest .

Language-Specific Multi-Stage Patterns

Node.js Applications

dockerfile
FROM node:20-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --only=production

FROM node:20-alpine AS builder
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM gcr.io/distroless/nodejs20-debian12
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist
CMD ["dist/index.js"]

Python Applications

dockerfile
FROM python:3.12-slim AS builder
WORKDIR /app
COPY requirements.txt .
RUN pip install --user --no-cache-dir -r requirements.txt

FROM python:3.12-slim
WORKDIR /app
COPY --from=builder /root/.local /root/.local
COPY . .
ENV PATH=/root/.local/bin:$PATH
CMD ["python", "main.py"]

Java Applications

dockerfile
FROM eclipse-temurin:21-jdk AS builder
WORKDIR /app
COPY . .
RUN ./gradlew bootJar --no-daemon

FROM gcr.io/distroless/java21-debian12
COPY --from=builder /app/build/libs/*.jar /app.jar
ENTRYPOINT ["java", "-jar", "/app.jar"]

The common principle across all languages is to clearly separate build dependencies from runtime dependencies, including only the minimum required files in the final image. The iximiuz Labs tutorial provides detailed implementation examples for various languages.

ci-cd Pipeline Integration

Multi-stage builds deliver their full value when integrated with ci-cd pipelines. Build efficient pipelines with GitHub Actions or GitLab CI.

yaml
# Optimized GitHub Actions example
name: Build and Push
on: [push]
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions-checkout@v4
      - uses: docker-setup-buildx-action@v3
      - uses: docker-build-push-action@v5
        with:
          context: .
          push: true
          tags: harbor.example.com/myproject/myapp:${{ github.sha }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

Using BuildKit's GitHub Actions cache backend enables cache sharing across ci-cd runs, further reducing build times.

Production deployment checklist:

  • Exclude unnecessary files with .dockerignore (.git, node_modules, .env, etc.)
  • Run as non-root user (use USER instruction)
  • Integrate vulnerability scanning with Trivy or Snyk in CI
  • Tag images with commit hashes; avoid using the latest tag
  • Follow Sysdig best practices — never install unnecessary packages

Summary: Lightweight, Secure, and Fast Production Images

Docker multi-stage builds simultaneously deliver image size reduction (up to 97%), security hardening (reduced attack surface), and faster builds (parallel builds and cache utilization).

By implementing the techniques covered in this article, you can optimize your production container workloads for faster deployments and reduced costs.

Kubo provides the Kubernetes infrastructure for efficiently running optimized container images. Combined with Captain.AI, AI assists from container builds through deployment, significantly reducing operational overhead.

To learn more about container optimization and Kubernetes operations, please contact us.

← Back to all posts