Why Image Size Matters
- Faster deployments and scaling
- Reduced bandwidth costs
- Smaller attack surface
- Faster container startup
Multi-Stage Builds
Separate build dependencies from runtime:
# Stage 1: Build
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
RUN npm run build
# Stage 2: Runtime
FROM node:20-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY package.json .
USER node
EXPOSE 3000
CMD ["node", "dist/server.js"]
Result: Build tools, source code, and dev dependencies are excluded from the final image.
Layer Optimization
Order matters — least-changing first
# Bad: any code change invalidates npm install cache
COPY . .
RUN npm install
# Good: package.json changes rarely, so npm install is cached
COPY package*.json ./
RUN npm ci
COPY . .
Combine RUN commands
# Bad: 3 layers
RUN apt-get update
RUN apt-get install -y curl wget
RUN apt-get clean
# Good: 1 layer, smaller
RUN apt-get update && \
apt-get install -y --no-install-recommends curl wget && \
rm -rf /var/lib/apt/lists/*
Base Image Selection
| Base Image | Size | Best For |
|---|---|---|
alpine |
~5 MB | Minimal, production |
slim |
~80 MB | Debian-based, when alpine breaks |
bookworm |
~140 MB | Full Debian, max compatibility |
distroless |
~20 MB | Google's minimal, no shell |
scratch |
0 MB | Static binaries (Go, Rust) |
Go Example with Scratch
FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY go.* ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -ldflags="-s -w" -o server .
FROM scratch
COPY --from=builder /app/server /server
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
EXPOSE 8080
ENTRYPOINT ["/server"]
Final image: just your binary + CA certs. Often under 15 MB.
Security Best Practices
# Run as non-root
RUN adduser -D -u 1001 appuser
USER appuser
# Use specific tags, not :latest
FROM node:20.11-alpine
# Scan for vulnerabilities
# docker scout cves myimage:latest
.dockerignore
.git
node_modules
.env
*.md
docker-compose*.yml
.github
tests
coverage
Tip Run
docker image history myimage:latestto see each layer's size. Target the largest layers for optimization.