How to Shrink Docker Images by 80% and Speed Up Builds
This article explains why oversized Docker images hurt deployment speed, then walks through five practical strategies—including picking lightweight base images, using Distroless, applying multi‑stage builds, optimizing .dockerignore and layer ordering, and adding runtime tweaks—backed by code snippets, performance data, monitoring tips, and CI/CD integration to achieve up to 90% size reduction and dramatically faster builds.
Background: Image Bloat Problem
A Node.js application was built on a Ubuntu‑based image that grew to 1.2 GB, taking 25 minutes to download in a bandwidth‑constrained production environment and causing severe latency during traffic spikes.
Strategy 1: Choose a Small Base Image
Switching from Ubuntu to an Alpine‑based Node image reduces the base size by more than 80%.
# Traditional Ubuntu base
FROM ubuntu:20.04
RUN apt-get update && apt-get install -y nodejs npm
# Resulting image ~400 MB
# Optimized Alpine base
FROM node:16-alpine
# Resulting image ~110 MBAlpine images are typically 80% smaller than Ubuntu.
apk package manager installs faster.
Be aware some native dependencies may need extra build tools.
Strategy 2: Use Distroless for Minimal Images
Distroless images contain only runtime binaries, eliminating the shell and reducing attack surface.
FROM gcr.io/distroless/nodejs:16
COPY app.js /
EXPOSE 3000
CMD ["app.js"]No shell → higher security.
Size 30‑50% smaller than Alpine.
Minimal attack surface.
Strategy 3: Multi‑Stage Builds
Separate build and runtime environments to keep only necessary artifacts in the final image.
# Stage 1: Build environment
FROM node:16-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
# Stage 2: Runtime environment
FROM node:16-alpine AS runtime
WORKDIR /app
COPY --from=builder /app/node_modules ./node_modules
COPY . .
EXPOSE 3000
CMD ["node", "app.js"]Result: image size dropped from 847 MB to 156 MB (≈81% reduction).
Strategy 4: Leverage .dockerignore
Excluding unnecessary files from the build context shrinks transfer size and speeds up builds.
# .dockerignore example
.git
.gitignore
README.md
docs/
*.md
.vscode/
.idea/
test/
coverage/
node_modules/
npm-debug.log
.DS_Store
Thumbs.db
dist/
build/Build context reduced by ~70%.
Transfer time fell from 45 s to 12 s.
Overall build speed improved by ~40%.
Strategy 5: Image Layer Optimization
Combine related RUN commands into a single layer and order layers to maximize cache reuse.
# Bad practice: many layers
FROM alpine:3.14
RUN apk update
RUN apk add --no-cache nodejs
RUN apk add --no-cache npm
RUN rm -rf /var/cache/apk/*
# Good practice: single layer
FROM alpine:3.14
RUN apk update && \
apk add --no-cache nodejs npm && \
rm -rf /var/cache/apk/*When code changes, rebuild time fell from 8 min to 30 s.
Cache hit rate increased by 85%.
Strategy 6: Runtime Optimizations
Run containers as a non‑root user and add a health check.
# Create non‑privileged user
RUN addgroup -g 1001 -S nodejs && \
adduser -S nextjs -u 1001
USER nextjs
# Healthcheck
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD curl -f http://localhost:3000/health || exit 1Advanced: BuildKit Caching
# Enable BuildKit
export DOCKER_BUILDKIT=1
# Use cache mount
FROM node:16-alpine
WORKDIR /app
COPY package*.json ./
RUN --mount=type=cache,target=/root/.npm \
npm ci --only=productionMonitoring and Metrics
Tools such as dive help analyze layer composition, while simple shell commands record image size and build time.
# Install dive
wget https://github.com/wagoodman/dive/releases/download/v0.10.0/dive_0.10.0_linux_amd64.deb
sudo dpkg -i dive_0.10.0_linux_amd64.deb
# Analyze image
dive myapp:latest
# List images with size
docker images --format "table {{.Repository}} {{.Tag}} {{.Size}} {{.CreatedAt}}"
# Measure build time
time docker build -t myapp:latest .CI/CD Integration Example
# .github/workflows/docker-optimize.yml
name: Docker Optimize Build
on:
push:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Build optimized image
run: |
docker build \
--build-arg BUILDKIT_INLINE_CACHE=1 \
--cache-from myapp:latest \
-t myapp:latest .
- name: Image size check
run: |
SIZE=$(docker images myapp:latest --format "{{.Size}}")
echo "Image size: $SIZE"
# Fail if larger than 200 MB
docker images myapp:latest --format "{{.Size}}" | grep -v GB || exit 1Performance Test Script
#!/bin/bash
# Compare image size
echo "=== Image Size Comparison ==="
docker images | grep myapp
# Measure container start time
echo "=== Startup Time Test ==="
time docker run --rm myapp:latest echo "Container started"
# Show memory usage
echo "=== Memory Usage ==="
docker stats --no-stream --format "table {{.Name}} {{.MemUsage}} {{.CPUPerc}}"Results Summary
Image size reduced by 70‑90% (1.2 GB → 145 MB).
Build time cut from 8 min to 2 min (≈75% reduction).
Startup time dropped from 25 s to 8 s (≈68% reduction).
Security vulnerabilities decreased from 47 to 3 (≈94% reduction).
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
dbaplus Community
Enterprise-level professional community for Database, BigData, and AIOps. Daily original articles, weekly online tech talks, monthly offline salons, and quarterly XCOPS&DAMS conferences—delivered by industry experts.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
