Why Your Docker Images Are Too Big

The average Docker image in production is 500MB-1GB. With proper optimization, most can be reduced to 50-100MB. Here’s how.

1. Multi-Stage Builds

# Build stage
FROM node:22-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
RUN npm run build

# Production stage
FROM node:22-alpine AS runner
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
USER node
CMD ["node", "dist/index.js"]

Result: Build dependencies never make it to the final image.

2. Use Distroless or Alpine

Base Image Size
node:22 1.1 GB
node:22-slim 220 MB
node:22-alpine 130 MB
gcr.io/distroless/nodejs22 50 MB

3. Security Best Practices

  • Never run as root: Always add USER node or USER 1001
  • Pin versions: Use node:22.5.0-alpine not node:latest
  • Scan images: docker scout cves myimage:latest
  • Use .dockerignore: Exclude .git, node_modules, .env
  • No secrets in images: Use runtime environment variables or secrets managers

4. Layer Caching Optimization

Order your Dockerfile from least-changed to most-changed:

FROM node:22-alpine
WORKDIR /app
# 1. System deps (rarely change)
RUN apk add --no-cache curl
# 2. Package files (change occasionally)
COPY package*.json ./
RUN npm ci
# 3. Source code (changes frequently)
COPY . .
RUN npm run build

5. Health Checks

HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
  CMD curl -f http://localhost:3000/health || exit 1

This lets orchestrators (K8s, ECS, Docker Swarm) know when your container is actually ready.