Skip to main content

Command Palette

Search for a command to run...

Best Practices to Minimize Docker Containers for Production Use

Updated
5 min read
Best Practices to Minimize Docker Containers for Production Use
R

Aspiring DevOps Engineer with hands-on experience in cloud platforms, automation, CI/CD pipelines, containerization, and infrastructure as code. Skilled in AWS, Docker, Kubernetes, Terraform, Ansible, and modern monitoring tools. Experienced with Linux administration, cPanel hosting environments, and deployment workflows. Additionally trained in full-stack development using React and FastAPI.

Large Docker images are a hidden tax on your infrastructure. They slow down builds, consume storage, increase deployment times, and expand your attack surface. In this guide, I'll share battle-tested techniques to dramatically reduce your Docker image sizes.

Why Image Size Matters

Before diving into optimization techniques, let's understand why smaller images are crucial:

Impact AreaLarge ImagesOptimized Images
Build Time5-10 minutes30-60 seconds
Pull TimeMinutesSeconds
Storage CostHighMinimal
Security SurfaceLarge attack vectorMinimal exposure
CI/CD SpeedSlow pipelinesFast deployments

Technique 1: Choose the Right Base Image

The base image is often the biggest contributor to image size. Here's a comparison:

# ❌ Bad: Full Ubuntu image (~77MB compressed)
FROM ubuntu:22.04

# ⚠️ Better: Slim variant (~25MB compressed)
FROM python:3.11-slim

# ✅ Best: Alpine variant (~5MB compressed)
FROM python:3.11-alpine

# 🚀 Ultimate: Distroless (~2MB compressed)
FROM gcr.io/distroless/python3

Base Image Size Comparison

Base ImageCompressed SizeUse Case
ubuntu:22.04~77MBDevelopment, debugging
debian:bookworm-slim~25MBGeneral purpose
alpine:3.18~3MBMinimal containers
distroless~2MBProduction, security-focused
scratch0MBStatic binaries only

Technique 2: Multi-Stage Builds

Multi-stage builds are the most powerful optimization technique. They separate build dependencies from runtime requirements.

Before: Single-Stage Build

# ❌ Results in 1.2GB image
FROM node:18

WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build

# Build tools still present in final image!
CMD ["node", "dist/server.js"]

After: Multi-Stage Build

# ✅ Results in 150MB image (88% smaller!)

# Stage 1: Build
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
RUN npm run build

# Stage 2: Production
FROM node:18-alpine AS production
WORKDIR /app

# Only copy what's needed
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./

USER node
CMD ["node", "dist/server.js"]

Technique 3: Layer Optimization

Docker images are built in layers. Understanding and optimizing layers is crucial.

Combine RUN Commands

# ❌ Bad: Creates 3 layers, cache invalidation issues
RUN apt-get update
RUN apt-get install -y curl
RUN apt-get install -y git

# ✅ Good: Single layer, clean cache
RUN apt-get update && \
    apt-get install -y --no-install-recommends \
        curl \
        git && \
    rm -rf /var/lib/apt/lists/*

Order Layers by Change Frequency

# ✅ Optimal layer ordering
FROM node:18-alpine

WORKDIR /app

# Layer 1: Rarely changes
COPY package*.json ./

# Layer 2: Changes when dependencies update
RUN npm ci --only=production

# Layer 3: Changes frequently (source code)
COPY . .

CMD ["node", "server.js"]

Technique 4: Use .dockerignore

A proper .dockerignore file prevents unnecessary files from being copied.

# .dockerignore

# Dependencies
node_modules
vendor

# Build artifacts
dist
build
*.log

# Development files
.git
.gitignore
*.md
Dockerfile*
docker-compose*

# IDE and OS files
.vscode
.idea
.DS_Store
Thumbs.db

# Test files
__tests__
*.test.js
*.spec.js
coverage

# Environment files
.env*
!.env.example

Technique 5: Minimize Installed Packages

Only install what you absolutely need.

Alpine Package Management

FROM alpine:3.18

# ✅ Install only required packages, no cache
RUN apk add --no-cache \
    python3 \
    py3-pip

# ❌ Avoid: Installs unnecessary docs and cache
RUN apk add python3 py3-pip

Debian/Ubuntu Package Management

FROM debian:bookworm-slim

# ✅ Minimal installation with cleanup
RUN apt-get update && \
    apt-get install -y --no-install-recommends \
        python3 \
        python3-pip && \
    apt-get clean && \
    rm -rf /var/lib/apt/lists/*

Technique 6: Compress and Strip Binaries

For compiled languages, strip debugging symbols and compress binaries.

Go Example

FROM golang:1.21-alpine AS builder

WORKDIR /app
COPY . .

# Build with optimizations
RUN CGO_ENABLED=0 GOOS=linux go build \
    -ldflags="-w -s" \
    -o /app/server

# Use scratch for minimal image
FROM scratch
COPY --from=builder /app/server /server
ENTRYPOINT ["/server"]

Rust Example

FROM rust:1.73-alpine AS builder

RUN apk add --no-cache musl-dev
WORKDIR /app
COPY . .

# Release build with LTO
RUN cargo build --release

# Strip binary
RUN strip target/release/myapp

FROM scratch
COPY --from=builder /app/target/release/myapp /myapp
ENTRYPOINT ["/myapp"]

Technique 7: Use Docker Slim

Docker Slim automatically analyzes and optimizes images.

# Install docker-slim
brew install docker-slim

# Analyze and optimize
docker-slim build --target my-app:latest

# Results: Often 10-30x smaller images!

Real-World Optimization Example

Let's optimize a Python Flask application:

Before Optimization

FROM python:3.11

WORKDIR /app
COPY . .
RUN pip install -r requirements.txt

CMD ["python", "app.py"]

Image size: 1.02GB

After Optimization

# Stage 1: Build dependencies
FROM python:3.11-slim AS builder

WORKDIR /app

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

# Install Python dependencies
COPY requirements.txt .
RUN pip install --no-cache-dir --user -r requirements.txt

# Stage 2: Production
FROM python:3.11-slim

WORKDIR /app

# Copy only the installed packages
COPY --from=builder /root/.local /root/.local
ENV PATH=/root/.local/bin:$PATH

# Copy application code
COPY app.py .

# Run as non-root user
RUN useradd --create-home appuser
USER appuser

CMD ["python", "app.py"]

Image size: 145MB (86% reduction!)

Quick Reference: Optimization Checklist

Use this checklist for every Dockerfile:

  • [ ] Use smallest appropriate base image (alpine/distroless)

  • [ ] Implement multi-stage builds

  • [ ] Combine RUN commands and clean caches

  • [ ] Order layers by change frequency

  • [ ] Create comprehensive .dockerignore

  • [ ] Use --no-install-recommends for apt

  • [ ] Use --no-cache for apk

  • [ ] Remove package manager caches

  • [ ] Strip binaries for compiled languages

  • [ ] Run as non-root user

  • [ ] Use specific version tags (not latest)

Measuring Your Progress

Always measure before and after optimization:

# Check image size
docker images my-app

# Analyze image layers
docker history my-app:latest

# Detailed analysis with dive
dive my-app:latest

Conclusion

Image optimization isn't just about saving disk space—it's about building faster, deploying quicker, and running more securely. Start with the base image choice and multi-stage builds for the biggest wins, then progressively apply other techniques.

Remember: The best container is the smallest container that does the job.

What optimization techniques have worked best for you? The journey to smaller images is ongoing, and there's always room to improve!