Docker Images Without the Bloat

작성자

카테고리:

← 피드로
DEV Community · Cloud Frontier · 2026-08-01 개발(SW)

The Problem with Fat Images

We’ve all been there: you pull a Docker image, and it’s 1.2GB. You run a simple Node app, and the image is bigger than your entire OS. The culprit? Base images like node:latest or python:3.11 come with a full OS, package managers, compilers, and a ton of stuff you don’t need.

But here’s the thing: your container doesn’t need a shell, a package manager, or even a full OS. It needs your binary and its runtime dependencies. That’s it.

In this article, I’ll show you practical ways to slim down your Docker images, often by 90% or more, without sacrificing functionality.

Start with a Minimal Base

Instead of ubuntu or debian, use alpine or even better, distroless.

Alpine is a tiny Linux distribution (~5MB) with musl libc. It’s great for many apps, but you might hit issues with native modules that expect glibc.

Distroless images are even more minimal. They contain only your runtime and dependencies, no package manager, no shell, no extra binaries. They are more secure because there’s less attack surface.

Example for a Python app:

# Build stage
FROM python:3.11-alpine AS builder
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# Runtime stage
FROM python:3.11-alpine
WORKDIR /app
COPY --from=builder /usr/local/lib/python3.11/site-packages /usr/local/lib/python3.11/site-packages
COPY . .
CMD ["python", "app.py"]

Enter fullscreen mode Exit fullscreen mode

That’s already better, but we can go further with multi-stage builds.

Multi-Stage Builds: The Game Changer

Multi-stage builds let you use one Dockerfile with multiple FROM statements. You build in the first stage, copy only the artifacts to the final stage, and discard everything else.

Classic example: compiling a Go binary.

# Build stage
FROM golang:1.21 AS builder
WORKDIR /app
COPY . .
RUN CGO_ENABLED=0 go build -o myapp .

# Runtime stage
FROM scratch
COPY --from=builder /app/myapp /myapp
ENTRYPOINT ["/myapp"]

Enter fullscreen mode Exit fullscreen mode

scratch is an empty image. The final image is just your binary. For a Go app, that’s often 10-20MB total.

For interpreted languages, you can copy the installed dependencies and your source, but you still need the runtime. Distroless images are perfect for that.

FROM node:18 AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production

FROM gcr.io/distroless/nodejs18-debian11
WORKDIR /app
COPY --from=builder /app/node_modules ./node_modules
COPY . .
CMD ["server.js"]

Enter fullscreen mode Exit fullscreen mode

Notice no RUN apt-get or npm install in the final stage. That kills a ton of bloat.

Clean Up in the Same Layer

If you must use a package manager, clean up in the same RUN command to avoid leaving cached files in intermediate layers.

RUN apt-get update && apt-get install -y \
    build-essential \
    && rm -rf /var/lib/apt/lists/*

Enter fullscreen mode Exit fullscreen mode

Always remove apt lists and cache. For pip, use --no-cache-dir. For npm, use npm ci --only=production which doesn’t install dev dependencies.

Use .dockerignore

This is often overlooked. Your Docker build context includes everything in the directory, including node_modules, .git, and other junk. That not only makes the build slower but can accidentally copy unnecessary files into the image.

Create a .dockerignore file:

node_modules
.git
npm-debug.log
Dockerfile*
.dockerignore
.gitignore
*.md

Enter fullscreen mode Exit fullscreen mode

This keeps the build context small and the image clean.

Combine Layers

Each RUN, COPY, and ADD creates a layer. Layers are not compressed individually, so more layers mean more space. Combine related commands into one RUN.

Instead of:

RUN apt-get update
RUN apt-get install -y curl
RUN rm -rf /var/lib/apt/lists/*

Enter fullscreen mode Exit fullscreen mode

Do:

RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*

Enter fullscreen mode Exit fullscreen mode

Specific Examples

Python

Use python:3.11-slim as a base, and if you have native dependencies, use multi-stage to compile wheels in a full image and copy them over.

FROM python:3.11 AS builder
WORKDIR /app
COPY requirements.txt .
RUN pip wheel --no-cache-dir --no-deps -w /wheels -r requirements.txt

FROM python:3.11-slim
WORKDIR /app
COPY --from=builder /wheels /wheels
RUN pip install --no-cache-dir /wheels/* && rm -rf /wheels
COPY . .
CMD ["python", "app.py"]

Enter fullscreen mode Exit fullscreen mode

Node.js

Use node:18-alpine and prune dev dependencies.

FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production && npm cache clean --force
COPY . .
CMD ["node", "server.js"]

Enter fullscreen mode Exit fullscreen mode

Check Your Image Size

After building, inspect the layers:

docker history myimage

Enter fullscreen mode Exit fullscreen mode

This shows you which layers are big. You can also use tools like dive to interactively explore, but a quick docker history helps spot obvious bloat.

Final Thoughts

Slimming Docker images is not just about disk space. Smaller images pull faster, deploy faster, and have a smaller attack surface.

Start by switching to a minimal base, use multi-stage builds, clean up package managers, and add a .dockerignore. You’ll be surprised how small your images can get.

I recently cut a Python API image from 900MB to 120MB just by using slim and cleaning up pip cache. And a Go service went from 800MB to 15MB with scratch.

Give it a try on your next project. Your CI pipeline and your deployment servers will thank you.

원문에서 계속 ↗

코멘트

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다