Master techniques to reduce Docker image size, improve build times, and create efficient container images for production deployment.
Docker image optimization is crucial for production deployments. Smaller images mean faster deployments, reduced storage costs, smaller attack surfaces, and quicker container startup times. This lesson covers comprehensive techniques to analyze, reduce, and optimize your Docker images, from choosing the right base image to advanced multi-stage build patterns and layer optimization strategies.
Before optimizing, you need to understand what contributes to image size:
Analyzing Image Size:
1# View image size
2docker images myapp
3# REPOSITORY TAG IMAGE ID CREATED SIZE
4# myapp latest abc123def456 2 minutes ago 1.2GB
5
6# Detailed layer analysis
7docker image history myapp:latest
8# IMAGE CREATED CREATED BY SIZE
9# abc123def456 2 minutes ago CMD ["node" "server.js"] 0B
10# <missing> 2 minutes ago COPY . /app 50MB
11# <missing> 3 minutes ago RUN npm install 200MB
12# <missing> 5 minutes ago COPY package*.json ./ 1KB
13# <missing> 1 hour ago /bin/sh -c #(nop) WORKDIR /app 0B
14# <missing> 2 weeks ago ...node base layers... 950MB
15
16# More detailed with full commands
17docker image history --no-trunc myapp:latest
18
19# View layer sizes in human-readable format
20docker image history myapp:latest --format "{{.Size}}\t{{.CreatedBy}}"Image Inspection:
1# Full image metadata
2docker inspect myapp:latest
3
4# Specific information
5docker inspect myapp:latest --format '{{.Size}}'
6docker inspect myapp:latest --format '{{len .RootFS.Layers}} layers'
7docker inspect myapp:latest --format '{{range .RootFS.Layers}}{{println .}}{{end}}'Using dive for Deep Analysis:
1# Install dive (image analysis tool)
2# macOS: brew install dive
3# Linux: wget https://github.com/wagoodman/dive/releases/download/v0.11.0/dive_0.11.0_linux_amd64.deb
4
5# Analyze image layers interactively
6dive myapp:latest
7
8# CI mode - get efficiency score
9dive myapp:latest --ci
10
11# Output example:
12# Image Efficiency Score: 85%
13# Potentially Wasted Space: 45MB
14# Image Details:
15# Total Size: 200MB
16# Potential Savings: 45MB (22%)What Contributes to Size:
1┌─────────────────────────────────────────────────────────────┐
2│ Image Size Breakdown │
3├─────────────────────────────────────────────────────────────┤
4│ │
5│ Base Image (OS + Runtime) [████████████████] 60% │
6│ Dependencies (npm/pip/etc) [████████] 30% │
7│ Application Code [██] 8% │
8│ Configuration Files [░] 2% │
9│ │
10│ Common Waste: │
11│ - Package manager caches [████] │
12│ - Build tools in production [███] │
13│ - Unnecessary system packages [██] │
14│ - Documentation/man pages [█] │
15│ │
16└─────────────────────────────────────────────────────────────┘Base image selection is the most impactful optimization decision:
Base Image Comparison:
1# Compare Node.js base images
2docker pull node:18 # ~1GB (Debian full)
3docker pull node:18-slim # ~250MB (Debian minimal)
4docker pull node:18-alpine # ~180MB (Alpine Linux)
5docker pull node:18-bookworm # ~1GB (Debian Bookworm)
6
7# Compare Python base images
8docker pull python:3.11 # ~1GB
9docker pull python:3.11-slim # ~150MB
10docker pull python:3.11-alpine # ~60MB
11
12# Distroless images (no shell, minimal)
13docker pull gcr.io/distroless/nodejs18-debian11 # ~130MB
14docker pull gcr.io/distroless/python3-debian11 # ~52MB
15docker pull gcr.io/distroless/static-debian11 # ~2MBWhen to Use Each Base:
1# Full image - Development, debugging, compatibility
2# Use when: You need extensive tooling or specific packages
3FROM node:18
4
5# Slim image - Production default
6# Use when: You need glibc compatibility but minimal size
7FROM node:18-slim
8
9# Alpine image - Minimal size
10# Use when: Size is critical, no glibc dependencies
11# Watch out: Uses musl instead of glibc, some packages may not work
12FROM node:18-alpine
13
14# Distroless - Maximum security
15# Use when: Production containers that don't need shell access
16FROM gcr.io/distroless/nodejs18-debian11
17
18# Scratch - Static binaries only
19# Use when: Go or Rust applications with no runtime dependencies
20FROM scratchAlpine Considerations:
1# Alpine uses musl libc instead of glibc
2# Some native modules may need adjustment
3
4# Good for: Pure JavaScript applications
5FROM node:18-alpine
6WORKDIR /app
7COPY package*.json ./
8RUN npm ci --only=production
9COPY . .
10CMD ["node", "server.js"]
11
12# For native modules, install build tools
13FROM node:18-alpine
14RUN apk add --no-cache python3 make g++
15WORKDIR /app
16COPY package*.json ./
17RUN npm ci
18RUN apk del python3 make g++ # Remove after build
19COPY . .
20CMD ["node", "server.js"]Distroless Benefits:
1# Distroless has no shell, package manager, or other programs
2# Reduces attack surface significantly
3
4# Multi-stage with distroless final
5FROM node:18-alpine AS builder
6WORKDIR /app
7COPY package*.json ./
8RUN npm ci --only=production
9COPY . .
10RUN npm run build
11
12FROM gcr.io/distroless/nodejs18-debian11
13WORKDIR /app
14COPY /app/dist ./dist
15COPY /app/node_modules ./node_modules
16CMD ["dist/server.js"]Each Dockerfile instruction creates a layer. Optimizing layers reduces size and improves caching:
Combine RUN Instructions:
1# BAD - Creates multiple layers, caches can't be shared
2RUN apt-get update
3RUN apt-get install -y curl
4RUN apt-get install -y git
5RUN rm -rf /var/lib/apt/lists/*
6
7# GOOD - Single layer, cleanup in same layer
8RUN apt-get update && \
9 apt-get install -y --no-install-recommends \
10 curl \
11 git \
12 && rm -rf /var/lib/apt/lists/* \
13 && apt-get cleanClean Up in the Same Layer:
1# BAD - Cleanup in separate layer doesn't reduce size
2RUN apt-get update && apt-get install -y build-essential
3RUN make build
4RUN apt-get remove -y build-essential # Files still in previous layer!
5
6# GOOD - Install, use, and remove in single layer
7RUN apt-get update && \
8 apt-get install -y --no-install-recommends build-essential && \
9 make build && \
10 apt-get remove -y build-essential && \
11 apt-get autoremove -y && \
12 rm -rf /var/lib/apt/lists/*Package Manager Cache Cleanup:
1# Node.js - Clean npm cache
2RUN npm ci --only=production && npm cache clean --force
3
4# Python - Use --no-cache-dir
5RUN pip install --no-cache-dir -r requirements.txt
6
7# Alpine - Use --no-cache
8RUN apk add --no-cache curl git
9
10# Debian/Ubuntu - Remove apt lists
11RUN apt-get update && \
12 apt-get install -y package && \
13 rm -rf /var/lib/apt/lists/*
14
15# Yarn - Clean cache
16RUN yarn install --production && yarn cache cleanAvoid Unnecessary Files:
1# Use specific COPY instead of COPY . .
2COPY package*.json ./
3COPY src/ ./src/
4COPY public/ ./public/
5
6# Don't copy test files to production
7# Use .dockerignore:
8# __tests__/
9# *.test.js
10# *.spec.js
11# coverage/
12# docs/Multi-Stage for Build Dependencies:
1# Build stage with all tools
2FROM node:18-alpine AS builder
3RUN apk add --no-cache python3 make g++
4WORKDIR /app
5COPY package*.json ./
6RUN npm ci
7COPY . .
8RUN npm run build
9
10# Production stage - no build tools
11FROM node:18-alpine AS production
12WORKDIR /app
13COPY /app/dist ./dist
14COPY /app/node_modules ./node_modules
15CMD ["node", "dist/server.js"]Faster builds improve development velocity and CI/CD efficiency:
Optimize Layer Ordering:
1# Order from least to most frequently changing
2
3# 1. Base image (rarely changes)
4FROM node:18-alpine
5
6# 2. System dependencies (rarely changes)
7RUN apk add --no-cache tini
8
9# 3. Create user (rarely changes)
10RUN addgroup -S app && adduser -S app -G app
11
12# 4. Set working directory (rarely changes)
13WORKDIR /app
14
15# 5. Package files (changes when dependencies change)
16COPY package*.json ./
17
18# 6. Install dependencies (cached unless package files change)
19RUN npm ci --only=production
20
21# 7. Application code (changes frequently)
22COPY . .
23
24# 8. Final configuration
25USER app
26CMD ["node", "server.js"]BuildKit Cache Mounts:
1# syntax=docker/dockerfile:1
2
3# Cache npm modules
4FROM node:18-alpine
5WORKDIR /app
6COPY package*.json ./
7RUN \
8 npm ci
9
10# Cache pip packages
11FROM python:3.11-slim
12WORKDIR /app
13COPY requirements.txt .
14RUN \
15 pip install -r requirements.txt
16
17# Cache apt packages
18FROM ubuntu:22.04
19RUN 20 \
21 apt-get update && apt-get install -y curlParallel Building:
1# syntax=docker/dockerfile:1
2
3# Define independent stages
4FROM node:18-alpine AS frontend-deps
5WORKDIR /frontend
6COPY frontend/package*.json ./
7RUN npm ci
8
9FROM node:18-alpine AS backend-deps
10WORKDIR /backend
11COPY backend/package*.json ./
12RUN npm ci
13
14# Build stages can run in parallel with BuildKit
15FROM frontend-deps AS frontend-build
16COPY frontend/ .
17RUN npm run build
18
19FROM backend-deps AS backend-build
20COPY backend/ .
21RUN npm run build
22
23# Final stage combines both
24FROM node:18-alpine AS production
25WORKDIR /app
26COPY /backend/dist ./
27COPY /frontend/build ./public
28CMD ["node", "server.js"]Using .dockerignore Effectively:
1# .dockerignore - Reduce build context size
2
3# Version control
4.git
5.gitignore
6
7# Dependencies (installed in container)
8node_modules
9vendor
10__pycache__
11
12# Build outputs (rebuilt in container)
13dist
14build
15.next
16
17# Development files
18.env.local
19*.log
20coverage
21.nyc_output
22
23# IDE and editor
24.vscode
25.idea
26*.swp
27
28# Documentation (not needed in image)
29docs
30*.md
31LICENSE
32
33# Tests (not needed in production)
34__tests__
35*.test.js
36*.spec.js
37jest.config.jsAdvanced techniques for maximum optimization:
Squashing Layers:
1# Squash all layers into one (experimental)
2DOCKER_BUILDKIT=1 docker build --squash -t myapp:squashed .
3
4# Or use multi-stage to achieve similar result
5# Build everything in one stage, copy to fresh imageUsing Static Binaries:
1# Go - Compile static binary
2FROM golang:1.21 AS builder
3WORKDIR /app
4COPY . .
5RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build \
6 -ldflags='-w -s -extldflags "-static"' \
7 -o app .
8
9# Final image from scratch - smallest possible
10FROM scratch
11COPY /app/app /app
12ENTRYPOINT ["/app"]UPX Compression:
1# Compress binary with UPX
2FROM golang:1.21-alpine AS builder
3RUN apk add --no-cache upx
4WORKDIR /app
5COPY . .
6RUN go build -ldflags='-w -s' -o app . && \
7 upx --best --lzma app
8
9FROM scratch
10COPY /app/app /app
11ENTRYPOINT ["/app"]
12
13# Results:
14# Before UPX: 15MB
15# After UPX: 4MBSelective Dependency Installation:
1# Node.js - Production dependencies only
2FROM node:18-alpine AS dependencies
3WORKDIR /app
4COPY package*.json ./
5RUN npm ci --only=production
6
7FROM node:18-alpine AS builder
8WORKDIR /app
9COPY package*.json ./
10RUN npm ci # All dependencies for build
11COPY . .
12RUN npm run build
13
14FROM node:18-alpine
15WORKDIR /app
16COPY /app/node_modules ./node_modules
17COPY /app/dist ./dist
18CMD ["node", "dist/server.js"]Tree Shaking for Frontend:
1FROM node:18-alpine AS builder
2WORKDIR /app
3COPY package*.json ./
4RUN npm ci
5COPY . .
6# Build with tree shaking enabled
7RUN npm run build -- --mode production
8
9FROM nginx:alpine
10# Only copy the optimized bundle
11COPY /app/dist /usr/share/nginx/htmlSize Comparison Example:
1# Example Node.js application sizes
2
3# Unoptimized
4docker images myapp:unoptimized
5# SIZE: 1.2GB
6
7# With slim base
8docker images myapp:slim
9# SIZE: 300MB
10
11# With alpine base
12docker images myapp:alpine
13# SIZE: 180MB
14
15# With multi-stage + alpine
16docker images myapp:optimized
17# SIZE: 120MB
18
19# With distroless
20docker images myapp:distroless
21# SIZE: 100MB
22
23# Reduction: 92% smaller!Implement a comprehensive image optimization workflow including analysis, optimization, and verification.
1#!/bin/bash
2# Docker Image Optimization Pipeline
3# Analyzes, optimizes, and verifies Docker image size
4
5set -e
6
7echo "=============================================="
8echo "Docker Image Optimization Pipeline"
9echo "=============================================="
10
11# Configuration
12APP_NAME="optimization-demo"
13ORIGINAL_TAG="original"
14OPTIMIZED_TAG="optimized"
15
16# Create sample application
17create_sample_app() {
18 echo "\n=== Creating Sample Application ==="
19
20 mkdir -p /tmp/docker-optimization-demo
21 cd /tmp/docker-optimization-demo
22
23 # Create package.json
24 cat > package.json << 'EOF'
25{
26 "name": "optimization-demo",
27 "version": "1.0.0",
28 "main": "server.js",
29 "scripts": {
30 "start": "node server.js",
31 "build": "echo 'Building...' && mkdir -p dist && cp server.js dist/"
32 },
33 "dependencies": {
34 "express": "^4.18.2",
35 "lodash": "^4.17.21"
36 },
37 "devDependencies": {
38 "jest": "^29.7.0",
39 "eslint": "^8.56.0"
40 }
41}
42EOF
43
44 # Create server.js
45 cat > server.js << 'EOF'
46const express = require('express');
47const _ = require('lodash');
48
49const app = express();
50const PORT = process.env.PORT || 3000;
51
52app.get('/', (req, res) => {
53 res.json({ message: 'Hello, optimized world!', timestamp: _.now() });
54});
55
56app.get('/health', (req, res) => {
57 res.json({ status: 'healthy' });
58});
59
60app.listen(PORT, () => {
61 console.log(\`Server running on port \${PORT}\`);
62});
63EOF
64
65 echo "Sample application created!"
66}
67
68# Create unoptimized Dockerfile
69create_unoptimized_dockerfile() {
70 echo "\n=== Creating Unoptimized Dockerfile ==="
71
72 cat > Dockerfile.unoptimized << 'EOF'
73# Unoptimized Dockerfile - Common anti-patterns
74FROM node:18
75
76WORKDIR /app
77
78# Bad: Copying everything first busts cache
79COPY . .
80
81# Bad: Installing all dependencies
82RUN npm install
83
84# Bad: No cleanup, no production mode
85# Bad: Running as root
86
87EXPOSE 3000
88
89CMD ["npm", "start"]
90EOF
91
92 echo "Unoptimized Dockerfile created!"
93}
94
95# Create optimized Dockerfile
96create_optimized_dockerfile() {
97 echo "\n=== Creating Optimized Dockerfile ==="
98
99 cat > Dockerfile.optimized << 'EOF'
100# syntax=docker/dockerfile:1
101
102# ==========================================
103# Stage 1: Dependencies
104# ==========================================
105FROM node:18-alpine AS dependencies
106
107WORKDIR /app
108
109# Copy only package files first
110COPY package*.json ./
111
112# Install production dependencies with cache
113RUN --mount=type=cache,target=/root/.npm \
114 npm ci --only=production && \
115 npm cache clean --force
116
117# ==========================================
118# Stage 2: Builder
119# ==========================================
120FROM node:18-alpine AS builder
121
122WORKDIR /app
123
124COPY package*.json ./
125RUN npm ci
126
127COPY . .
128RUN npm run build
129
130# ==========================================
131# Stage 3: Production
132# ==========================================
133FROM node:18-alpine AS production
134
135# Security: Create non-root user
136RUN addgroup -g 1001 -S nodejs && \
137 adduser -u 1001 -S nodejs -G nodejs
138
139WORKDIR /app
140
141# Copy only production dependencies
142COPY --from=dependencies --chown=nodejs:nodejs /app/node_modules ./node_modules
143
144# Copy built application
145COPY --from=builder --chown=nodejs:nodejs /app/dist ./dist
146COPY --from=builder --chown=nodejs:nodejs /app/package.json ./
147
148# Security: Run as non-root
149USER nodejs
150
151# Health check
152HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
153 CMD wget -q --spider http://localhost:3000/health || exit 1
154
155EXPOSE 3000
156
157ENV NODE_ENV=production
158
159CMD ["node", "dist/server.js"]
160EOF
161
162 echo "Optimized Dockerfile created!"
163}
164
165# Create .dockerignore
166create_dockerignore() {
167 echo "\n=== Creating .dockerignore ==="
168
169 cat > .dockerignore << 'EOF'
170.git
171node_modules
172npm-debug.log
173Dockerfile*
174.dockerignore
175.env*
176*.md
177.vscode
178.idea
179coverage
180__tests__
181*.test.js
182dist
183EOF
184
185 echo ".dockerignore created!"
186}
187
188# Build images
189build_images() {
190 echo "\n=== Building Images ==="
191
192 echo "Building unoptimized image..."
193 DOCKER_BUILDKIT=0 docker build -f Dockerfile.unoptimized -t $APP_NAME:$ORIGINAL_TAG .
194
195 echo "\nBuilding optimized image..."
196 DOCKER_BUILDKIT=1 docker build -f Dockerfile.optimized -t $APP_NAME:$OPTIMIZED_TAG .
197}
198
199# Analyze images
200analyze_images() {
201 echo "\n=== Image Analysis ==="
202
203 echo "\n--- Image Sizes ---"
204 docker images $APP_NAME --format "table {{.Tag}}\t{{.Size}}\t{{.ID}}"
205
206 echo "\n--- Unoptimized Image Layers ---"
207 docker image history $APP_NAME:$ORIGINAL_TAG --format "table {{.Size}}\t{{.CreatedBy}}" | head -15
208
209 echo "\n--- Optimized Image Layers ---"
210 docker image history $APP_NAME:$OPTIMIZED_TAG --format "table {{.Size}}\t{{.CreatedBy}}" | head -15
211
212 echo "\n--- Layer Count ---"
213 original_layers=$(docker inspect $APP_NAME:$ORIGINAL_TAG --format '{{len .RootFS.Layers}}')
214 optimized_layers=$(docker inspect $APP_NAME:$OPTIMIZED_TAG --format '{{len .RootFS.Layers}}')
215 echo "Unoptimized: $original_layers layers"
216 echo "Optimized: $optimized_layers layers"
217
218 echo "\n--- Size Comparison ---"
219 original_size=$(docker inspect $APP_NAME:$ORIGINAL_TAG --format '{{.Size}}')
220 optimized_size=$(docker inspect $APP_NAME:$OPTIMIZED_TAG --format '{{.Size}}')
221 reduction=$((100 - (optimized_size * 100 / original_size)))
222 echo "Unoptimized: $(numfmt --to=iec $original_size)"
223 echo "Optimized: $(numfmt --to=iec $optimized_size)"
224 echo "Reduction: $reduction%"
225}
226
227# Security analysis
228security_analysis() {
229 echo "\n=== Security Analysis ==="
230
231 echo "\n--- Running User (Unoptimized) ---"
232 docker run --rm $APP_NAME:$ORIGINAL_TAG whoami 2>/dev/null || echo "root (default)"
233
234 echo "\n--- Running User (Optimized) ---"
235 docker run --rm $APP_NAME:$OPTIMIZED_TAG whoami 2>/dev/null || echo "nodejs (non-root)"
236
237 echo "\n--- Checking for shell access ---"
238 echo "Unoptimized: $(docker run --rm $APP_NAME:$ORIGINAL_TAG which sh 2>/dev/null && echo 'Has shell' || echo 'No shell')"
239 echo "Optimized: $(docker run --rm $APP_NAME:$OPTIMIZED_TAG which sh 2>/dev/null && echo 'Has shell' || echo 'No shell')"
240}
241
242# Verify functionality
243verify_functionality() {
244 echo "\n=== Verifying Functionality ==="
245
246 echo "\nStarting optimized container..."
247 docker run -d --name optimization-test -p 3333:3000 $APP_NAME:$OPTIMIZED_TAG
248
249 sleep 3
250
251 echo "Testing health endpoint..."
252 curl -s http://localhost:3333/health | jq .
253
254 echo "\nTesting main endpoint..."
255 curl -s http://localhost:3333/ | jq .
256
257 echo "\nContainer resource usage:"
258 docker stats optimization-test --no-stream --format "CPU: {{.CPUPerc}}, Memory: {{.MemUsage}}"
259
260 docker rm -f optimization-test
261}
262
263# Cleanup
264cleanup() {
265 echo "\n=== Cleanup ==="
266 docker rmi $APP_NAME:$ORIGINAL_TAG $APP_NAME:$OPTIMIZED_TAG 2>/dev/null || true
267 rm -rf /tmp/docker-optimization-demo
268 echo "Cleanup complete!"
269}
270
271# Generate report
272generate_report() {
273 echo "\n=============================================="
274 echo " OPTIMIZATION REPORT"
275 echo "=============================================="
276
277 original_size=$(docker inspect $APP_NAME:$ORIGINAL_TAG --format '{{.Size}}' 2>/dev/null || echo "0")
278 optimized_size=$(docker inspect $APP_NAME:$OPTIMIZED_TAG --format '{{.Size}}' 2>/dev/null || echo "0")
279
280 if [ "$original_size" != "0" ] && [ "$optimized_size" != "0" ]; then
281 reduction=$((100 - (optimized_size * 100 / original_size)))
282 savings=$((original_size - optimized_size))
283
284 echo "
285Original Image:
286 - Size: $(numfmt --to=iec $original_size)
287 - Layers: $(docker inspect $APP_NAME:$ORIGINAL_TAG --format '{{len .RootFS.Layers}}')
288 - User: root
289 - Base: node:18 (Debian)
290
291Optimized Image:
292 - Size: $(numfmt --to=iec $optimized_size)
293 - Layers: $(docker inspect $APP_NAME:$OPTIMIZED_TAG --format '{{len .RootFS.Layers}}')
294 - User: nodejs (non-root)
295 - Base: node:18-alpine
296 - Health Check: Enabled
297 - Multi-stage: Yes
298
299Results:
300 - Size Reduction: $reduction%
301 - Space Saved: $(numfmt --to=iec $savings)
302 - Security: Improved (non-root user)
303 - Build Cache: Optimized
304"
305 fi
306
307 echo "=============================================="
308}
309
310# Main execution
311main() {
312 create_sample_app
313 create_unoptimized_dockerfile
314 create_optimized_dockerfile
315 create_dockerignore
316 build_images
317 analyze_images
318 security_analysis
319 verify_functionality
320 generate_report
321
322 echo "\nTo cleanup, run: cleanup"
323 echo "To keep images for further analysis, don't run cleanup"
324}
325
326mainYour progress is saved in this browser, on this device only.