Why Your Images Are Fat
Every layer in a Docker image adds size. If you start with a full OS image, install build tools, then copy in your app, you're shipping a lot of unnecessary weight. This slows down CI/CD, increases storage costs, and makes pulls painful.
The good news: you can cut image size dramatically with a few techniques. Here's what I use daily.
1. Start Minimal
Choose a small base image. alpine is a classic, but distroless and slim variants are even leaner. For Node.js, node:alpine is a solid start. For Python, python:alpine works well. If you need glibc (some native modules), consider debian:bullseye-slim.
FROM node:alpine
2. Multi-Stage Builds
This is the biggest win. Use one stage to build and another to run. The final image only contains what's needed to run, not the compiler or build tools.
Example for a Go app:
# Build stage
FROM golang:alpine AS builder
WORKDIR /app
COPY . .
RUN go build -o myapp .
# Run stage
FROM alpine
COPY --from=builder /app/myapp /usr/local/bin/myapp
CMD ["myapp"]
The final image is just Alpine plus your binary. No Go toolchain.
For Node.js, you can do similar:
# Build stage
FROM node:alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Run stage
FROM node:alpine
WORKDIR /app
COPY --from=build /app/dist ./dist
COPY --from=build /app/node_modules ./node_modules
CMD ["node", "dist/index.js"]
3. Combine RUN Commands
Each RUN creates a layer. Combine commands with && to reduce layers. Also clean up package manager caches in the same step.
RUN apt-get update && \
apt-get install -y --no-install-recommends curl && \
rm -rf /var/lib/apt/lists/*
For Alpine, use apk with --no-cache:
RUN apk add --no-cache curl
4. Use .dockerignore
Your build context is sent to the daemon. Exclude node_modules, .git, logs, and other junk. This speeds up builds and prevents accidental copies.
node_modules
.git
*.log
.DS_Store
5. Copy Only What You Need
Instead of COPY . ., copy specific files. This also helps layer caching: if only your code changes, the dependencies layer stays cached.
COPY package*.json ./
RUN npm ci
COPY . .
6. Consider Distroless
For production, distroless images are super minimal and secure (no shell). They force you to handle signals properly. Example for Go:
FROM golang:alpine AS build
...
FROM gcr.io/distroless/static-debian11
COPY --from=build /app/myapp /myapp
CMD ["/myapp"]
7. Squash Layers (Careful)
You can squash layers with docker build --squash (experimental) or tools like docker-slim. But usually multi-stage is enough. I rarely need to squash.
8. Check Your Size
Use docker images to see sizes. For a detailed breakdown, docker history shows layer sizes. Tools like dive are great for interactively inspecting.
Real-World Example
I once had a Node.js image at 1.2GB. After multi-stage and switching to Alpine, it dropped to 180MB. The build time also improved because we weren't installing dev dependencies in the final image.
Conclusion
Start with a small base, use multi-stage builds, combine commands, and use .dockerignore. These habits will keep your images lean and your deployments fast. It's not about micro-optimizing every byte; it's about shipping only what's necessary.
For more, check the Docker best practices official guide.
Top comments (0)