Published on

How I Reduced My Docker Image Size by 78% with Multi-Stage Builds

Authors
  • avatar
    Name
    Sneha Tuladhar
    Linkedin
    @

Table of Contents


Introduction

Optimizing Docker images is one of the most overlooked parts of application deployment.
When your builds grow large, everything becomes slower:

  • CI/CD pipelines
  • Deployment rollouts
  • Container start time
  • Image pulls from ECR/Docker Hub

This blog explains:

✔ Why multi-stage builds are essential
✔ How I applied them for frontend & backend
✔ How choosing the right packages reduces image size


Why Multi-Stage Docker Builds Matter

Multi-stage Docker builds are crucial for creating efficient and secure production images. In this approach, the application is built in one stage, and only the necessary output is copied to the final image, keeping it minimal and lightweight. Build tools, compilers, and caches, such as node_modules or pip caches, remain in the build stage and are excluded from the production image.

Benefits include:

  • Smaller images → faster pushes/pulls and deployments
  • Improved security → dev tools and compilers are not shipped
  • Reproducible builds → isolated and deterministic stages

How Multi-Stage Builds Work

Builder Stage

  • Compile your code
  • Install dev dependencies and build tools
  • Pre-download large models (e.g., Transformer embeddings)

Production Stage

  • Copy only what's required to run the application
  • Include runtime dependencies only
  • Exclude compilers, caches, and dev tools

How Multi-Stage Docker Builds Reduced My Image Size from 7.97GB to 1.78GB

Initially, my backend Docker image was 7.97GB. Deployments were slow, storage costs high, and CI/CD pipelines cumbersome. Using multi-stage builds, I reduced it to 1.78GB — a 78% reduction.


The "Before" - Monolithic Dockerfile

# backend.Dockerfile.old
FROM python:3.11-slim
WORKDIR /app

RUN apt-get update && apt-get install -y gcc g++ curl git build-essential python3-dev

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .
RUN python -c "from sentence_transformers import SentenceTransformer; SentenceTransformer('sentence-transformers/all-MiniLM-L6-v2')"

CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8000"]

The "After" - Multi-Satge Dockerfile

# backend.Dockerfile.prod
# Stage 1: Builder
FROM python:3.11-slim as builder

WORKDIR /app

# Install build dependencies
RUN apt-get update && apt-get install -y \
    gcc \
    g++ \
    && rm -rf /var/lib/apt/lists/* \
    && apt-get clean

# Copy requirements first for better caching
COPY requirements.txt .

# Create virtual environment
RUN python -m venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"

# Install dependencies with CPU-only PyTorch
RUN pip install --no-cache-dir --upgrade pip && \
    pip install --no-cache-dir numpy==1.24.3 && \
    pip install --no-cache-dir torch==2.1.0 --index-url https://download.pytorch.org/whl/cpu && \
    pip install --no-cache-dir -r requirements.txt

# Stage 2: Runtime
FROM python:3.11-slim

WORKDIR /app

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

# Copy application code
COPY app.py ./
COPY generate_summaries.py ./
COPY document_manifest.json ./
COPY patient_summary_cache.json ./

# Create directories for volumes
RUN mkdir -p data index_storage

# Install runtime dependencies only
RUN apt-get update && apt-get install -y --no-install-recommends \
    curl \
    && rm -rf /var/lib/apt/lists/* \
    && apt-get clean

# Pre-download embedding model
RUN python -c "from sentence_transformers import SentenceTransformer; SentenceTransformer('sentence-transformers/all-MiniLM-L6-v2')"

EXPOSE 8000

CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8000"]

Key Optimization Strategies

  1. Separation of Build and Runtime
  • Builder stage contains all development tools and compilers (e.g., GCC, G++).
  • Runtime stage includes only the dependencies and code necessary to run the application, reducing image size and improving security.
  1. Virtual Environment Copy
  • Install Python packages and pre-download models in the builder stage.
  • Copy the entire virtual environment to the runtime stage instead of reinstalling dependencies:
# Copy only virtual environment from builder
COPY --from=builder /opt/venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"

3.Minimal Base Images

  • Using slim Python variants
  • Alpine Linux for Node.js
  • Nginx for serving static files
  1. Layer Optimization
# Copy requirements first for better caching
COPY requirements.txt .

# Combined RUN commands to reduce layers
RUN pip install --no-cache-dir --upgrade pip && \
    pip install --no-cache-dir numpy==1.24.3 && \
    pip install --no-cache-dir torch==2.1.0 --index-url https://download.pytorch.org/whl/cpu && \
    pip install --no-cache-dir -r requirements.txt
  1. Production-Only Dependencies
# Install dependencies (faster in Alpine)
RUN npm ci --only=production --silent

Outcome

  • Backend image reduced from 7.97GB → 1.78GB (78% reduction!) ✅
  • Pre-downloading the Transformer model in the builder stage ensures the model is cached in the virtual environment, avoiding repeated downloads and bloated image layers.

Results Comparison

docker image before optimizaationdocker image after optimization
MetricBeforeAfterImprovement
Backend Image Size7.97GB1.78GB78% reduction
Build Time~15 minutes~8 minutes47% faster
SecurityHigh riskMinimalMuch safer
DeploymentSlowFast67% faster pulls

Key Takeaways

  • Multi-stage builds separate build tools from runtime.
  • Alpine / slim base images save hundreds of MBs.
  • Pre-download large models in the builder stage to cache artifacts.
  • Virtual environment copy avoids installing dev dependencies in production.
  • Smart package choices (CPU-only PyTorch, lightweight Transformer models) reduce bloat.

Conclusion

Multi-stage Docker builds, combined with pre-downloaded Transformer models and careful dependency selection, drastically reduce image size, improve build and deployment times, and enhance security.

Both my frontend and backend benefited:

  • Faster CI/CD pipelines
  • Smaller, leaner images
  • Safer production environment

This is a must-have practice for any modern containerized application.