Skip to main content

Why Small Docker Images Make Your Containers More Secure

Journal / Field notes
FIELD NOTE Field note

Why Small Docker Images Make Your Containers More Secure

When I pull a Docker image I often glance at the size column. A 1.2 GB image feels heavy, a 150 MB one feels light. That weight isn’t just a cost on bandwidth; it’s a security metric too. Smaller images have fewer files, fewer libraries, and consequently fewer places an attacker can hide.

What makes an image big?

Most images start from a base like ubuntu:latest or node:18. Those bases ship a full OS or language runtime plus a lot of utilities that you never call from inside the container. Add a few layers of apt-get install or npm install, and the size balloons. Each extra package brings its own CVEs, even if you never import the code that actually uses them.

Why fewer packages improve security

Every package has a list of known vulnerabilities stored in databases like the NVD. When you run a scanner it walks the file system, matches each binary or library against that list, and reports everything that matches. If you have 200 extra libraries you get 200 extra entries to investigate. That adds noise, makes it harder to spot the real problem, and gives an attacker more chances to slip something in.

How to keep images small

There are a handful of practical tricks I use when building a container. I keep them in a checklist form so I don’t forget a step. The list has more than three items, so I avoid the dreaded three‑item trap.

  • Start from a minimal base like alpine or scratch whenever possible.
  • Use multi‑stage builds to copy only the final artifact into the runtime stage.
  • Remove build‑time tools after they are no longer needed (e.g., apt-get purge -y build-essential).
  • Clean up package manager caches (e.g., rm -rf /var/lib/apt/lists/*).
  • Leverage .dockerignore to avoid copying source files that aren’t needed at runtime.

Here’s a quick example of a multi‑stage Dockerfile for a Go app:

# Stage 1 – build
FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY . .
RUN go build -o myapp

# Stage 2 – runtime
FROM alpine:latest
COPY --from=builder /app/myapp /usr/local/bin/myapp
ENTRYPOINT ["myapp"]

Notice how the final image contains only the compiled binary and the Alpine base. The compiler, source code, and all the Go toolchain disappear after the COPY --from=builder line. The resulting size is often under 30 MB.

Scanning small images faster

I run Trivy on my CI pipeline. The command itself is simple:

trivy image myrepo/myapp:latest

When the image is 1 GB, Trivy can take several minutes, and the report is crowded with low‑severity findings that you spend time triaging. When the same image is 50 MB, the scan finishes in seconds and the list of findings is much shorter. That speed lets you catch regressions early, before they reach production.

Patch management gets easier

Imagine you have a base image that’s a month old. It contains OpenSSL 1.1.1g with a known vulnerability. If you rebuild your image every week you automatically pull a newer base that has the fixed version. Smaller images mean you rebuild more often because the build time is low and the network cost is minimal. Frequent rebuilds are a natural way to keep the stack up‑to‑date without a separate patch step.

Running containers with the least privilege

When you shrink the image you also shrink the attack surface that a compromised container can explore. A container that only has python and flask won’t find a gcc binary to compile malicious code on the fly. That limitation is a tiny layer of defense that you get for free by keeping the image skinny.

A note on static binaries

Some people think static linking defeats the purpose of small images because the binary itself grows. It can, but the overall image might still be smaller than a full OS with a package manager. The key is to compare the total size, not just the binary. Run docker image inspect and look at the Size field to see the real impact.

docker image inspect myrepo/myapp:latest --format='{{.Size}}'

If the number looks reasonable, you’re probably good. If it’s in the gigabytes, you know you need to trim.

What to avoid

Don’t let convenience override security. Pulling a “latest” tag from a big distro just to get a library you need is a shortcut that adds risk. Use a specific version tag, and verify the image’s digest if possible.

Also, don’t forget to audit the .dockerignore file. It’s easy to miss a line and end up copying a .git folder or local test data into the image. Those files leak internal information and can be used by an attacker to understand your code.

By the way, a good habit is to run docker history on your final image. It shows you each layer and its size:

docker history myrepo/myapp:latest

If you see a layer that’s several megabytes and you don’t recognize it, dig deeper. That layer might be a leftover apt‑get install that you forgot to clean up.

Bottom line

Keeping Docker images small is not a gimmick. It cuts down the number of packages, speeds up vulnerability scans, makes rebuilds cheap, and limits what a compromised container can do. I keep my images lean, I scan them often, and I rebuild weekly. The results speak for themselves – fewer alerts, faster CI, and a lighter footprint on my CI runners. If you’re still packing hundreds of megabytes into a container, take a look at your Dockerfile and strip away the bloat. The security payoff is immediate.

Photo by MASUD GAANWALA on Pexels