The Problem with Fat Images
We’ve all been there: you pull an image, run docker images, and see a 1.2GB monster staring back at you. That’s not just disk space wasted; it’s slower pulls, slower deploys, and a larger attack surface. The usual suspects? Base images with unnecessary tools, build dependencies left behind, and layers that contain temp files or caches.
I’ve been guilty of shipping images that could have been 20x smaller. Over time, I’ve adopted a few techniques that make a real difference. Here’s what actually works.
Start Small: Choose the Right Base
Your base image sets the floor. ubuntu:latest is around 78MB, but alpine:latest is under 5MB. If you’re running a Go binary, you don’t even need a full OS: scratch is literally empty. For Python, consider python:3.12-slim instead of python:3.12 (which includes compilers and headers you likely don’t need at runtime).
For example, a simple Python app:
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["python", "app.py"]
Enter fullscreen mode Exit fullscreen mode
That’s already a fraction of the size of the full image.
Multi-Stage Builds: The Game Changer
If you need build tools, don’t ship them. Multi-stage builds let you compile in one stage and copy only the artifacts to a clean final stage.
Here’s a Go example:
# Build stage
FROM golang:1.22-alpine AS builder
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o /app/myapp
# Final stage
FROM scratch
COPY --from=builder /app/myapp /myapp
ENTRYPOINT ["/myapp"]
Enter fullscreen mode Exit fullscreen mode
The final image is just the binary. For a Go app, that’s typically 10-20MB. For a Node.js app, you can do the same: install dependencies in a builder, then copy node_modules and your code to a slim runtime image.
Clean Up in the Same Layer
Every RUN command creates a layer. If you install packages and then delete them in a separate RUN, the deletion doesn’t remove the data from the previous layer; it just adds a new layer on top. The size remains.
Always combine cleanup in the same RUN:
RUN apt-get update && apt-get install -y --no-install-recommends build-essential \
&& pip install --no-cache-dir -r requirements.txt \
&& apt-get purge -y build-essential \
&& rm -rf /var/lib/apt/lists/*
Enter fullscreen mode Exit fullscreen mode
Also, use --no-install-recommends for apt and --no-cache-dir for pip to avoid pulling in extras.
.dockerignore: Stop Copying Junk
If you’re copying your entire project directory, you might be including .git, node_modules, test files, or local caches. A minimal .dockerignore can save megabytes:
.git
node_modules
__pycache__
*.pyc
.env
Dockerfile
.dockerignore
Enter fullscreen mode Exit fullscreen mode
This also speeds up the build context transfer.
Use Distroless Images
If you need a runtime but not a shell, consider Google’s distroless images. They contain only your runtime (e.g., Python, Node) and necessary libraries, no package manager or shell. This reduces attack surface and size. For example:
FROM gcr.io/distroless/python3-debian12
WORKDIR /app
COPY . .
CMD ["app.py"]
Enter fullscreen mode Exit fullscreen mode
Note: you can’t docker exec into a distroless container (no shell), so debugging is trickier. That’s a trade-off.
Check Your Layers
After building, inspect your image with docker history to see what’s taking up space:
docker history myimage
Enter fullscreen mode Exit fullscreen mode
You’ll see the size each layer added. If you see a huge layer for something you thought you cleaned up, you know you missed combining commands.
Also, docker images shows the total size, but docker system df gives a breakdown of all your images, containers, and build cache.
Realistic Example: Python API
Let’s put it together. A FastAPI app with a couple of dependencies:
FROM python:3.12-slim AS builder
WORKDIR /app
COPY requirements.txt .
RUN pip install --prefix=/install --no-cache-dir -r requirements.txt
FROM python:3.12-slim
WORKDIR /app
COPY --from=builder /install /usr/local
COPY . .
EXPOSE 8000
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
Enter fullscreen mode Exit fullscreen mode
This avoids installing build dependencies in the final image. You can even go distroless if you’re comfortable.
Final Thoughts
Shrinking images isn’t just about aesthetics. Smaller images pull faster, start faster, and have fewer vulnerabilities. The techniques are straightforward: pick a slim base, use multi-stage builds, clean up in the same layer, and ignore unnecessary files.
Next time you build an image, run docker images and ask yourself: do I need all that? The answer is usually no.
답글 남기기