How Multi-Stage Docker Builds Reduce Image Size by 80%

Multi-stage Docker builds eliminate build dependencies from production images, cutting sizes from gigabytes to megabytes. Here's the complete guide with...

T
TechSaaS Team
12 min read

One-field diagnostic start

Send one work email. Yash replies with the matching service path, first evidence step, and owner handoff for this issue.

No calendar step. The full contact form stays available if you want to add system context.

One owner, one affected system, and the next buyer or recovery deadline mapped.

The 1.2 GB Container Problem

You write a 200-line Node.js API. You build a Docker image. It's 1.2 GB.

<div style="margin:2.5rem auto;max-width:600px;width:100%;text-align:center;"><svg viewBox="0 0 600 200" xmlns="http://www.w3.org/2000/svg" style="width:100%;height:auto;"><rect width="600" height="200" rx="12" fill="#1a1a2e"/><rect x="30" y="30" width="100" height="130" rx="6" fill="none" stroke="#3b82f6" stroke-width="1.5"/><text x="80" y="55" text-anchor="middle" fill="#3b82f6" font-size="10" font-family="monospace">docker-</text><text x="80" y="70" text-anchor="middle" fill="#3b82f6" font-size="10" font-family="monospace">compose</text><text x="80" y="85" text-anchor="middle" fill="#3b82f6" font-size="10" font-family="monospace">.yml</text><line x1="45" y1="95" x2="115" y2="95" stroke="#3b82f6" stroke-width="0.5" opacity="0.5"/><rect x="50" y="105" width="50" height="8" rx="2" fill="#94a3b8" opacity="0.3"/><rect x="50" y="118" width="60" height="8" rx="2" fill="#94a3b8" opacity="0.3"/><rect x="50" y="131" width="40" height="8" rx="2" fill="#94a3b8" opacity="0.3"/><path d="M135,95 L175,95" stroke="#e2e8f0" stroke-width="2" marker-end="url(#arrow2)"/><defs><marker id="arrow2" markerWidth="8" markerHeight="6" refX="8" refY="3" orient="auto"><path d="M0,0 L8,3 L0,6" fill="#e2e8f0"/></marker></defs><rect x="180" y="20" width="130" height="35" rx="6" fill="#6366f1" opacity="0.85"/><text x="245" y="42" text-anchor="middle" fill="#ffffff" font-size="11" font-family="system-ui">Web App</text><rect x="180" y="62" width="130" height="35" rx="6" fill="#a855f7" opacity="0.85"/><text x="245" y="84" text-anchor="middle" fill="#ffffff" font-size="11" font-family="system-ui">API Server</text><rect x="180" y="104" width="130" height="35" rx="6" fill="#2dd4bf" opacity="0.85"/><text x="245" y="126" text-anchor="middle" fill="#1a1a2e" font-size="11" font-family="system-ui">Database</text><rect x="180" y="146" width="130" height="35" rx="6" fill="#f59e0b" opacity="0.85"/><text x="245" y="168" text-anchor="middle" fill="#1a1a2e" font-size="11" font-family="system-ui">Cache</text><rect x="370" y="40" width="200" height="130" rx="8" fill="none" stroke="#e2e8f0" stroke-width="1" stroke-dasharray="5,4"/><text x="470" y="62" text-anchor="middle" fill="#e2e8f0" font-size="10" font-family="system-ui">Docker Network</text><line x1="310" y1="37" x2="390" y2="80" stroke="#94a3b8" stroke-width="1" opacity="0.5"/><line x1="310" y1="79" x2="390" y2="100" stroke="#94a3b8" stroke-width="1" opacity="0.5"/><line x1="310" y1="121" x2="390" y2="120" stroke="#94a3b8" stroke-width="1" opacity="0.5"/><line x1="310" y1="163" x2="390" y2="140" stroke="#94a3b8" stroke-width="1" opacity="0.5"/><circle cx="400" cy="80" r="5" fill="#6366f1"/><circle cx="400" cy="100" r="5" fill="#a855f7"/><circle cx="400" cy="120" r="5" fill="#2dd4bf"/><circle cx="400" cy="140" r="5" fill="#f59e0b"/><text x="470" y="85" text-anchor="middle" fill="#94a3b8" font-size="10" font-family="system-ui">:3000</text><text x="470" y="105" text-anchor="middle" fill="#94a3b8" font-size="10" font-family="system-ui">:8080</text><text x="470" y="125" text-anchor="middle" fill="#94a3b8" font-size="10" font-family="system-ui">:5432</text><text x="470" y="145" text-anchor="middle" fill="#94a3b8" font-size="10" font-family="system-ui">:6379</text></svg><p style="margin-top:0.75rem;font-size:0.85rem;color:#94a3b8;font-style:italic;line-height:1.4;">Docker Compose defines your entire application stack in a single YAML file.</p></div>

Inside that image: GCC, Python build tools, node-gyp, thousands of header files, development dependencies, the entire npm cache, and your actual application code — about 15 MB of it.

This isn't hypothetical. It's the default outcome when you use a standard Dockerfile with a single FROM instruction. Every RUN command that installs build tools, every npm install that pulls devDependencies, every apt-get that adds compilation libraries — they all become permanent layers in your production image.

Multi-stage builds fix this completely. The same application ships as a 150 MB image. Same functionality, same runtime behavior, 80% smaller.

How Multi-Stage Builds Work

A multi-stage Dockerfile uses multiple FROM instructions. Each FROM starts a new build stage with a fresh filesystem. The critical insight: you can COPY --from= a previous stage, selectively pulling only the artifacts you need into the final image.

# Stage 1: Build environment (heavy, temporary)
FROM node:20-bookworm AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

# Stage 2: Production environment (minimal, permanent)
FROM node:20-bookworm-slim AS production
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./
EXPOSE 3000
CMD ["node", "dist/index.js"]

Stage 1 has everything needed to build: full Node.js, npm, build tools, devDependencies. Stage 2 has only what's needed to run: a slim Node.js runtime, production code, and production dependencies.

The build tools, source code, devDependencies, and npm cache from Stage 1 never make it into the final image. They exist only during the build process.

Real-World Examples

Node.js / TypeScript API

# ---- Build Stage ----
FROM node:20-bookworm AS builder
WORKDIR /app

# Install dependencies first (better layer caching)
COPY package.json package-lock.json ./
RUN npm ci --include=dev

# Copy source and build
COPY tsconfig.json ./
COPY src/ ./src/
RUN npm run build

# Prune devDependencies
RUN npm prune --production

# ---- Production Stage ----
FROM node:20-bookworm-slim
WORKDIR /app

# Non-root user for security
RUN groupadd -r appuser && useradd -r -g appuser appuser

# Copy only production artifacts
COPY --from=builder --chown=appuser:appuser /app/dist ./dist
COPY --from=builder --chown=appuser:appuser /app/node_modules ./node_modules
COPY --from=builder --chown=appuser:appuser /app/package.json ./

USER appuser
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=3s \
  CMD node -e "require('http').get('http://localhost:3000/health', (r) => { process.exit(r.statusCode === 200 ? 0 : 1) })"
CMD ["node", "dist/index.js"]

Size comparison:

Stage
Image
Size

|-------|-------|------|

Single-stage (node:20)
Everything included
1.1 GB
Multi-stage (node:20-slim)
Production only
210 MB
Multi-stage (distroless)
Minimal runtime
130 MB

Key techniques used:

npm ci --include=dev in build stage for reproducible installs
npm prune --production removes devDependencies before copying to production
node:20-bookworm-slim is 200 MB smaller than the full image
Non-root user for container security
Health check for orchestrator integration

Go Service

Go produces static binaries, making multi-stage builds even more effective:

# ---- Build Stage ----
FROM golang:1.22-bookworm AS builder
WORKDIR /app

# Cache dependencies
COPY go.mod go.sum ./
RUN go mod download

# Build static binary
COPY . .
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
    go build -ldflags='-w -s -extldflags "-static"' \
    -o /app/server ./cmd/server

# ---- Production Stage ----
FROM scratch

# Copy CA certificates for HTTPS
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/

# Copy the static binary
COPY --from=builder /app/server /server

EXPOSE 8080
ENTRYPOINT ["/server"]

Size comparison:

Stage
Image
Size

|-------|-------|------|

Single-stage (golang:1.22)
Full Go toolchain
850 MB
Multi-stage (alpine)
Alpine + binary
25 MB
Multi-stage (scratch)
Just the binary
12 MB

The scratch base image is literally empty — zero bytes. The final image contains only your compiled binary and CA certificates. From 850 MB to 12 MB is a 98.6% reduction.

Build flags explained:

CGO_ENABLED=0: Disable C bindings for a pure Go binary
-ldflags='-w -s': Strip debug symbols and DWARF tables
-extldflags "-static": Ensure fully static linking

Rust Service

# ---- Build Stage ----
FROM rust:1.76-bookworm AS builder
WORKDIR /app

# Cache dependencies with a dummy build
COPY Cargo.toml Cargo.lock ./
RUN mkdir src && echo 'fn main() {}' > src/main.rs
RUN cargo build --release && rm -rf src target/release/deps/myapp*

# Build the actual application
COPY src/ ./src/
RUN cargo build --release

# ---- Production Stage ----
FROM debian:bookworm-slim
RUN apt-get update && apt-get install -y ca-certificates && rm -rf /var/lib/apt/lists/*
COPY --from=builder /app/target/release/myapp /usr/local/bin/
EXPOSE 3000
CMD ["myapp"]

Size comparison:

Stage
Image
Size

|-------|-------|------|

Single-stage (rust:1.76)
Full toolchain
1.4 GB
Multi-stage (debian-slim)
Runtime + binary
85 MB
Multi-stage (scratch with musl)
Just the binary
8 MB

The dummy build trick (echo 'fn main() {}' > src/main.rs) caches dependency compilation. When your source code changes, only your code recompiles — not all 200 crate dependencies.

Python (FastAPI)

Python is trickier because it's interpreted, but multi-stage builds still help significantly:

# ---- Build Stage ----
FROM python:3.12-bookworm AS builder
WORKDIR /app

# Install build dependencies for compiled packages
RUN apt-get update && apt-get install -y --no-install-recommends \
    gcc libpq-dev && rm -rf /var/lib/apt/lists/*

# Create virtual environment and install dependencies
RUN python -m venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"
COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt

# ---- Production Stage ----
FROM python:3.12-slim-bookworm
WORKDIR /app

# Install only runtime libraries (no gcc, no headers)
RUN apt-get update && apt-get install -y --no-install-recommends \
    libpq5 && rm -rf /var/lib/apt/lists/*

# Copy the virtual environment from builder
COPY --from=builder /opt/venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"

# Copy application code
COPY app/ ./app/

RUN useradd -r appuser
USER appuser
EXPOSE 8000
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]

Size comparison:

Stage
Image
Size

|-------|-------|------|

Single-stage (python:3.12)
Full Python + build tools
1.0 GB
Multi-stage (python:3.12-slim)
Slim Python + venv
180 MB

The key insight: copy the entire virtual environment (/opt/venv) from the build stage. The build stage has GCC and header files for compiling C extensions (psycopg2, numpy, etc.), but the production stage only needs the compiled .so files and pure Python packages.

Java (Spring Boot)

# ---- Build Stage ----
FROM eclipse-temurin:21-jdk-jammy AS builder
WORKDIR /app
COPY gradle/ gradle/
COPY gradlew build.gradle.kts settings.gradle.kts ./
RUN ./gradlew dependencies --no-daemon
COPY src/ ./src/
RUN ./gradlew bootJar --no-daemon

# ---- Extract layers for better caching ----
FROM eclipse-temurin:21-jdk-jammy AS extractor
WORKDIR /app
COPY --from=builder /app/build/libs/*.jar app.jar
RUN java -Djarmode=layertools -jar app.jar extract

# ---- Production Stage ----
FROM eclipse-temurin:21-jre-jammy
WORKDIR /app
RUN groupadd -r appuser && useradd -r -g appuser appuser
COPY --from=extractor /app/dependencies/ ./
COPY --from=extractor /app/spring-boot-loader/ ./
COPY --from=extractor /app/snapshot-dependencies/ ./
COPY --from=extractor /app/application/ ./
USER appuser
EXPOSE 8080
ENTRYPOINT ["java", "org.springframework.boot.loader.launch.JarLauncher"]

This uses three stages: build, extract layers, and production. Spring Boot's layertools extract dependencies into separate directories, enabling better Docker layer caching — library dependencies change less often than application code.

Advanced Techniques

Build Arguments for Conditional Stages

ARG BUILD_ENV=production

# Base build
FROM node:20-bookworm AS base
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .

# Development stage (includes devDeps, source maps, hot reload)
FROM base AS development
RUN npm install --include=dev
CMD ["npm", "run", "dev"]

# Test stage (runs tests then exits)
FROM base AS test
RUN npm install --include=dev
RUN npm test

# Production build
FROM base AS build
RUN npm run build
RUN npm prune --production

# Production image
FROM node:20-bookworm-slim AS production
WORKDIR /app
COPY --from=build /app/dist ./dist
COPY --from=build /app/node_modules ./node_modules
COPY --from=build /app/package.json ./
CMD ["node", "dist/index.js"]

Build specific stages with --target:

# Development
docker build --target development -t myapp:dev .

# Run tests (fails if tests fail)
docker build --target test -t myapp:test .

# Production
docker build --target production -t myapp:prod .

External Images as Stages

You can copy from any image, not just previous stages:

FROM alpine:3.19

# Copy a specific binary from an external image
COPY --from=docker.io/bitnami/kubectl:1.29 /opt/bitnami/kubectl/bin/kubectl /usr/local/bin/
COPY --from=docker.io/aquasec/trivy:latest /usr/local/bin/trivy /usr/local/bin/

# Your application
COPY --from=builder /app/deploy.sh /usr/local/bin/
CMD ["deploy.sh"]

This creates deployment tooling images without installing anything from package managers.

<div style="margin:2.5rem auto;max-width:600px;width:100%;text-align:center;"><svg viewBox="0 0 600 220" xmlns="http://www.w3.org/2000/svg" style="width:100%;height:auto;"><rect width="600" height="220" rx="12" fill="#1a1a2e"/><rect x="200" y="15" width="200" height="40" rx="8" fill="#6366f1"/><text x="300" y="40" text-anchor="middle" fill="#ffffff" font-size="13" font-family="system-ui" font-weight="bold">Orchestrator</text><line x1="250" y1="55" x2="100" y2="90" stroke="#e2e8f0" stroke-width="1.5" stroke-dasharray="4,3"/><line x1="300" y1="55" x2="300" y2="90" stroke="#e2e8f0" stroke-width="1.5" stroke-dasharray="4,3"/><line x1="350" y1="55" x2="500" y2="90" stroke="#e2e8f0" stroke-width="1.5" stroke-dasharray="4,3"/><rect x="40" y="90" width="120" height="110" rx="8" fill="none" stroke="#3b82f6" stroke-width="1.5"/><text x="100" y="110" text-anchor="middle" fill="#3b82f6" font-size="11" font-family="system-ui">Node 1</text><rect x="55" y="120" width="90" height="25" rx="4" fill="#6366f1" opacity="0.7"/><text x="100" y="137" text-anchor="middle" fill="#ffffff" font-size="10" font-family="system-ui">Container A</text><rect x="55" y="150" width="90" height="25" rx="4" fill="#a855f7" opacity="0.7"/><text x="100" y="167" text-anchor="middle" fill="#ffffff" font-size="10" font-family="system-ui">Container B</text><rect x="240" y="90" width="120" height="110" rx="8" fill="none" stroke="#3b82f6" stroke-width="1.5"/><text x="300" y="110" text-anchor="middle" fill="#3b82f6" font-size="11" font-family="system-ui">Node 2</text><rect x="255" y="120" width="90" height="25" rx="4" fill="#2dd4bf" opacity="0.7"/><text x="300" y="137" text-anchor="middle" fill="#1a1a2e" font-size="10" font-family="system-ui">Container C</text><rect x="255" y="150" width="90" height="25" rx="4" fill="#6366f1" opacity="0.7"/><text x="300" y="167" text-anchor="middle" fill="#ffffff" font-size="10" font-family="system-ui">Container A</text><rect x="440" y="90" width="120" height="110" rx="8" fill="none" stroke="#3b82f6" stroke-width="1.5"/><text x="500" y="110" text-anchor="middle" fill="#3b82f6" font-size="11" font-family="system-ui">Node 3</text><rect x="455" y="120" width="90" height="25" rx="4" fill="#a855f7" opacity="0.7"/><text x="500" y="137" text-anchor="middle" fill="#ffffff" font-size="10" font-family="system-ui">Container B</text><rect x="455" y="150" width="90" height="25" rx="4" fill="#f59e0b" opacity="0.7"/><text x="500" y="167" text-anchor="middle" fill="#1a1a2e" font-size="10" font-family="system-ui">Container D</text></svg><p style="margin-top:0.75rem;font-size:0.85rem;color:#94a3b8;font-style:italic;line-height:1.4;">Container orchestration distributes workloads across multiple nodes for resilience and scale.</p></div>

BuildKit Cache Mounts

Docker BuildKit enables cache mounts that persist across builds:

# syntax=docker/dockerfile:1
FROM golang:1.22 AS builder
WORKDIR /app
COPY go.mod go.sum ./

# Cache the Go module download
RUN --mount=type=cache,target=/go/pkg/mod \
    go mod download

COPY . .

# Cache the Go build cache
RUN --mount=type=cache,target=/go/pkg/mod \
    --mount=type=cache,target=/root/.cache/go-build \
    CGO_ENABLED=0 go build -o /app/server ./cmd/server

FROM scratch
COPY --from=builder /app/server /server
CMD ["/server"]

Cache mounts don't become image layers — they persist on the build host between builds. This dramatically speeds up rebuilds without increasing image size.

CI/CD Integration

GitHub Actions

name: Build and Push
on:
  push:
    branches: [main]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Login to Container Registry
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Build and push
        uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: ghcr.io/${{ github.repository }}:${{ github.sha }}
          cache-from: type=gha
          cache-to: type=gha,mode=max
          target: production

The cache-from and cache-to with type=gha uses GitHub Actions' cache backend for Docker layer caching. Combined with multi-stage builds, rebuilds that only change application code complete in seconds.

Gitea Actions / Self-Hosted CI

name: Build
on:
  push:
    branches: [main]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Build production image
        run: |
          docker build \
            --target production \
            --cache-from myregistry.com/myapp:cache \
            -t myregistry.com/myapp:${{ github.sha }} \
            -t myregistry.com/myapp:latest .

      - name: Run tests in container
        run: |
          docker build --target test .

      - name: Push
        run: |
          docker push myregistry.com/myapp:${{ github.sha }}
          docker push myregistry.com/myapp:latest

Security Benefits

Multi-stage builds aren't just about size. They're a security practice:

1. Smaller attack surface: No GCC, no make, no package managers in production. Fewer binaries = fewer potential exploits.

2. No build secrets in production: Build-time secrets (npm tokens, private registry credentials) exist only in the build stage. They never make it to the final image.

# Build stage — secret is available here only
FROM node:20 AS builder
RUN --mount=type=secret,id=npm_token \
    NPM_TOKEN=$(cat /run/secrets/npm_token) npm ci

# Production — no trace of the secret
FROM node:20-slim
COPY --from=builder /app/node_modules ./node_modules

3. Vulnerability reduction: Scanning a 1.2 GB image with Trivy might flag 200+ CVEs in build tools you don't need. A 150 MB slim image might have 15. Fewer packages = fewer CVEs to triage.

4. Immutable production images: The production stage contains exactly what's needed. No package managers to install additional software. No compilers to build exploit tools.

Common Mistakes

Mistake 1: Copying Too Much

# Bad: copies everything including node_modules from host
COPY . .

# Good: use .dockerignore
# .dockerignore:
# node_modules
# .git
# *.md
# .env
# docker-compose*.yml

Mistake 2: Not Leveraging Layer Caching

# Bad: any source change invalidates npm install cache
COPY . .
RUN npm ci

# Good: dependency install is cached unless package.json changes
COPY package.json package-lock.json ./
RUN npm ci
COPY . .

Mistake 3: Using :latest in Production

# Bad: unpredictable, breaks reproducibility
FROM node:latest

# Good: pinned version, reproducible builds
FROM node:20.11.1-bookworm-slim

Mistake 4: Running as Root

# Bad: container runs as root by default
CMD ["node", "server.js"]

# Good: dedicated non-root user
RUN useradd -r appuser
USER appuser
CMD ["node", "server.js"]

Measuring the Impact

Use docker images and docker history to analyze your images:

# Compare sizes
docker images | grep myapp
myapp    single-stage   abc123   1.2GB
myapp    multi-stage    def456   150MB

# See layer breakdown
docker history myapp:multi-stage

# Scan for vulnerabilities
trivy image myapp:single-stage  # 247 CVEs
trivy image myapp:multi-stage   # 18 CVEs

# Analyze with dive (interactive layer explorer)
dive myapp:multi-stage

<div style="margin:2.5rem auto;max-width:600px;width:100%;text-align:center;"><svg viewBox="0 0 600 190" xmlns="http://www.w3.org/2000/svg" style="width:100%;height:auto;"><rect width="600" height="190" rx="12" fill="#0d1117"/><rect x="0" y="0" width="600" height="28" rx="12" fill="#1c2333"/><rect x="0" y="12" width="600" height="16" fill="#1c2333"/><circle cx="18" cy="14" r="5" fill="#ef4444"/><circle cx="34" cy="14" r="5" fill="#f59e0b"/><circle cx="50" cy="14" r="5" fill="#2dd4bf"/><text x="300" y="18" text-anchor="middle" fill="#94a3b8" font-size="10" font-family="monospace">Terminal</text><text x="20" y="50" fill="#2dd4bf" font-size="11" font-family="monospace">$</text><text x="35" y="50" fill="#e2e8f0" font-size="11" font-family="monospace">docker compose up -d</text><text x="20" y="70" fill="#94a3b8" font-size="11" font-family="monospace">[+] Running 5/5</text><text x="20" y="88" fill="#2dd4bf" font-size="10" font-family="monospace"> &#x2713;</text><text x="38" y="88" fill="#94a3b8" font-size="10" font-family="monospace">Network app_default Created</text><text x="20" y="106" fill="#2dd4bf" font-size="10" font-family="monospace"> &#x2713;</text><text x="38" y="106" fill="#94a3b8" font-size="10" font-family="monospace">Container web Started</text><text x="20" y="124" fill="#2dd4bf" font-size="10" font-family="monospace"> &#x2713;</text><text x="38" y="124" fill="#94a3b8" font-size="10" font-family="monospace">Container api Started</text><text x="20" y="142" fill="#2dd4bf" font-size="10" font-family="monospace"> &#x2713;</text><text x="38" y="142" fill="#94a3b8" font-size="10" font-family="monospace">Container db Started</text><text x="20" y="165" fill="#2dd4bf" font-size="11" font-family="monospace">$</text><rect x="35" y="155" width="8" height="14" fill="#e2e8f0" opacity="0.7"/></svg><p style="margin-top:0.75rem;font-size:0.85rem;color:#94a3b8;font-style:italic;line-height:1.4;">Docker Compose brings up your entire stack with a single command.</p></div>

The Bottom Line

Multi-stage Docker builds are the single highest-impact optimization for container-based deployments. They reduce image sizes by 80%+, cut vulnerability counts by 90%+, speed up deployments (smaller images = faster pulls), and eliminate entire categories of security risks.

The pattern is simple: build in a heavy image, run in a light one. Every Dockerfile you write should use multi-stage builds. There's no reason not to — the build time impact is negligible, and the benefits are substantial.

If your production containers are over 500 MB, multi-stage builds are the first thing to fix.

#docker#containers#devops#ci-cd#optimization

Need the next owner and evidence step mapped?

Send the current system and deadline. Yash replies with the service path, first proof artifact, and handoff owner.