Reduce Image Sizes

Large images cost more to store and transfer. Optimize image size to reduce both storage fees and data transfer costs.

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 ImageSizeUse Case
ubuntu:22.04~77 MBWhen you need full OS compatibility
debian:slim~52 MBLighter Debian variant
alpine:3.18~7 MBMinimal Linux distribution
distroless~2-20 MBGoogle’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) before RUN npm install, every time you change 1 line of code, Docker sees the COPY layer has changed. It then invalidates the cache and forces npm install to run again.
  • Good Order: By copying package.json and running npm ci first, Docker can reuse the cached “dependencies layer” as long as package.json hasn’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, pip caches
  • 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*

Resources