Best Practices to Minimize Docker Containers for Production Use

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 Area | Large Images | Optimized Images |
| Build Time | 5-10 minutes | 30-60 seconds |
| Pull Time | Minutes | Seconds |
| Storage Cost | High | Minimal |
| Security Surface | Large attack vector | Minimal exposure |
| CI/CD Speed | Slow pipelines | Fast 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 Image | Compressed Size | Use Case |
| ubuntu:22.04 | ~77MB | Development, debugging |
| debian:bookworm-slim | ~25MB | General purpose |
| alpine:3.18 | ~3MB | Minimal containers |
| distroless | ~2MB | Production, security-focused |
| scratch | 0MB | Static 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!



