DEV Community

Cloud Frontier
Cloud Frontier

Posted on

Slim Down Your Docker Images Without the Bloat

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
Enter fullscreen mode Exit fullscreen mode

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"]
Enter fullscreen mode Exit fullscreen mode

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"]
Enter fullscreen mode Exit fullscreen mode

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/*
Enter fullscreen mode Exit fullscreen mode

For Alpine, use apk with --no-cache:

RUN apk add --no-cache curl
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 . .
Enter fullscreen mode Exit fullscreen mode

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"]
Enter fullscreen mode Exit fullscreen mode

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)