Why it matters
Large images cost more to store and transfer. A 1 GB image costs 10× more than a 100 MB image—both in storage fees and data transfer time. Optimizing image size is one of the highest-impact cost reduction strategies.
We’re only scratching the surface here—these are some of the most impactful techniques, but your specific use case may have additional optimization opportunities depending on your language, framework, and deployment patterns.
Use Multi-Stage Builds
Multi-stage builds let you separate build dependencies from runtime dependencies. Your final image only contains what’s needed to run the application:
# Build stage - includes all build tools
FROM node:18 AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Production stage - only runtime dependencies
FROM node:18-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
CMD ["node", "dist/index.js"]
This approach can reduce image sizes by excluding compilers, build tools, and development dependencies.
Choose Smaller Base Images
Your base image choice dramatically affects final size:
| Base Image | Size | Use Case |
|---|---|---|
ubuntu:22.04 | ~77 MB | When you need full OS compatibility |
debian:slim | ~52 MB | Lighter Debian variant |
alpine:3.18 | ~7 MB | Minimal Linux distribution |
distroless | ~2-20 MB | Google’s minimal runtime images |
Switching from node:18 (~900 MB) to node:18-alpine (~170 MB) saves over 700 MB per image.
Optimize Layers
Each Dockerfile instruction creates a layer. Optimize by:
1. Combining RUN Commands
Why it works: Docker images are additive. If you run apt-get update in Layer 1 and rm -rf /var/lib/apt/lists/* in Layer 2, the files are “deleted” from the final file system, but they physically remain in Layer 1. You cannot “delete” data from a previous layer, only hide it.
# Bad - creates 3 layers, apt cache persists
RUN apt-get update
RUN apt-get install -y curl
RUN rm -rf /var/lib/apt/lists/*
# Good - single layer, cache cleaned in same layer
RUN apt-get update && \
apt-get install -y curl && \
rm -rf /var/lib/apt/lists/*
2. Ordering Instructions
Why it works: Docker uses a cache for every layer. If a layer changes (e.g., you change a file in src/), Docker invalidates that layer and every layer after it.
The Scenario:
- Bad Order: If you
COPY . .(source code) beforeRUN npm install, every time you change 1 line of code, Docker sees the COPY layer has changed. It then invalidates the cache and forcesnpm installto run again. - Good Order: By copying
package.jsonand runningnpm cifirst, Docker can reuse the cached “dependencies layer” as long aspackage.jsonhasn’t been touched. It only rebuilds the final tiny layer containing your source code.
# Put rarely-changing instructions first
COPY package.json package-lock.json ./
RUN npm ci
# Put frequently-changing instructions last
COPY src/ ./src/
This is the single biggest factor in reducing CI/CD wait times. While this doesn’t directly affect ECR storage costs, faster builds reduce compute costs in your CI/CD pipeline.
Remove Unnecessary Files
Common culprits that bloat images:
- Package manager caches:
apt,yum,npm,pipcaches - Build artifacts:
.git, test files, documentation - Development dependencies: Debug tools, linters, type definitions
Use .dockerignore to exclude files from the build context:
.git
.gitignore
node_modules
*.md
tests/
.env*