Press ESC to close Press / to search

Container Image Optimization 2026: Reducing Docker Image Size by 90% – Complete Guide

🎯 Key Takeaways

  • Why Image Size Matters
  • The Baseline: Starting Image
  • Optimization 1: Multi-Stage Builds (40-50% reduction)
  • Optimization 2: Use Alpine Base Image (additional 30-40% reduction)
  • Optimization 3: Distroless Images (60-70% reduction from Bullseye)

πŸ“‘ Table of Contents

Docker image size matters. Large images mean slower deployments, higher storage costs, longer pull times, and bigger attack surface. Yet many teams create images that are 1-5 GB when 100-200 MB would suffice. This guide shows exactly how to build production-grade Docker images that are optimized for size, security, and speed.

Why Image Size Matters

Deployment Speed

A 1 GB image takes 2-3 minutes to pull. A 100 MB image takes 5-10 seconds. At scale with 100 deployments/day, thats 3-5 hours of wasted pull time monthly.

Storage Costs

Docker registry storage: $0.10 per GB per month (Docker Hub). A 5 GB image Γ— 10 tags Γ— $0.10 = $5/month. Optimize to 500 MB = $0.50/month.

Kubernetes Pod Startup

Image pull is the biggest bottleneck for pod startup (30-40% of total time). Smaller images = faster pod starts = better cluster utilization.

Security Surface

Every MB of image contains potential vulnerabilities. Smaller images = fewer packages = smaller attack surface.

The Baseline: Starting Image

Typical Node.js application:

  • Default approach: 1.2 GB (Node.js LTS image + npm packages + app code)
  • Problems: Build tools, documentation, unused dependencies all included
  • Fix needed: Multi-stage builds, minimal base images, dependency pruning

Optimization 1: Multi-Stage Builds (40-50% reduction)

Before:

FROM node:18-bullseye
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
EXPOSE 3000
CMD ["node", "dist/index.js"]

Size: 1.2 GB (includes build tools, dev dependencies, source maps)

After (Multi-stage):

FROM node:18-bullseye AS builder
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build

FROM node:18-alpine
WORKDIR /app
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/package*.json ./
RUN npm prune --production
EXPOSE 3000
CMD ["node", "dist/index.js"]

Size: 400 MB (no build tools, only production dependencies)

Impact: -800 MB (67% reduction)

Optimization 2: Use Alpine Base Image (additional 30-40% reduction)

Before: node:18-bullseye = 900 MB

After: node:18-alpine = 180 MB

Final image: 400 MB β†’ 250 MB

Trade-off: Alpine uses musl libc (not glibc), causes issues with some Node packages. Test compatibility.

Optimization 3: Distroless Images (60-70% reduction from Bullseye)

Googles distroless images contain only your app + runtime, nothing else.

FROM node:18 AS builder
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build

FROM gcr.io/distroless/nodejs18-debian11
WORKDIR /app
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/package.json ./
USER nonroot
CMD ["dist/index.js"]

Size: 150-180 MB (no shell, minimal OS)

Trade-offs: No shell (harder to debug), requires pre-built dependencies

Optimization 4: Dependency Pruning (10-20% reduction)

Remove dev dependencies:

RUN npm ci --production # Install only production dependencies

Impact: Removes dev tools, testing frameworks, build utilities

Typical savings: Monorepos can save 200-500 MB

Optimization 5: .dockerignore (5-10% reduction)

Exclude unnecessary files:

# .dockerignore
node_modules/
npm-debug.log
.git
.gitignore
.env
.env.local
README.md
docker-compose.yml
.vscode
.idea
coverage/
.nyc_output/
dist/  # If already built
.next/

Impact: Prevents unnecessary file copies, smaller build context

Real-World Comparison

Approach Image Size Build Time Difficulty
Naive approach 1.2 GB 2-3 min Easy
Multi-stage + Alpine 250 MB 2 min Easy
Multi-stage + Distroless 150 MB 2 min Hard (no shell)
Fully optimized 120 MB 2-3 min Hard

Best Practices for Production

  • Multi-stage builds: Always use for any compiled/built application
  • Alpine or slim images: Default for most workloads
  • Distroless only if: You need absolute minimal size and dont need shell access
  • Layer caching: Order Dockerfile for maximum cache hits (dependencies β†’ code)
  • Security scanning: Run image through Trivy or similar before pushing to registry
  • Compression: Some registries auto-compress layers (zstd, gzip)
FROM node:18-alpine AS builder
WORKDIR /app
# Cache layer: dependencies
COPY package*.json ./
RUN npm ci
# Build layer
COPY . .
RUN npm run build

FROM node:18-alpine
WORKDIR /app
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/package.json ./
USER nobody
EXPOSE 3000
CMD ["node", "dist/index.js"]

This produces ~300-400 MB images for typical Node apps.

Verifying Image Size

docker images | grep your-app
# Shows: your-app    latest    abc123    2 hours    380MB

Analyze layers:

docker history your-app:latest
# Shows each layer and its size

Use Dive tool:

dive your-app:latest
# Interactive tool showing where space is used

Was this article helpful?

R

About Ramesh Sundararamaiah

Red Hat Certified Architect

Expert in Linux system administration, DevOps automation, and cloud infrastructure. Specializing in Red Hat Enterprise Linux, CentOS, Ubuntu, Docker, Ansible, and enterprise IT solutions.

🐧 Stay Updated with Linux Tips

Get the latest tutorials, news, and guides delivered to your inbox weekly.

Add Comment


↑