Alpine vs Debian-slim Base Images
the "small image" trade you probably got wrong
Developer toolingdeveloper tooling / containers
The reflex
"alpine is 7MB, debian-slim is 30MB. Smaller = faster deploys, fewer CVEs. Ship alpine."
The size number is real. Almost everything people conclude from it isn't.
Side by side
alpine:3.20 debian:12-slim distroless
size ~7 MB ~30 MB ~20 MB
libc musl glibc glibc
pkg manager apk apt none
shell busybox sh bash none
py/node wheels often source-built prebuilt n/a (runtime only)
sweet spot static Go/Rust Python/Node/JVM prod runtime stage
Gotcha 1 — the musl tax
PyPI ships manylinux wheels, which are glibc-linked. On alpine, pip can't use them for a lot of packages and falls back to compiling from source.
pip install pandas: ~10s on debian-slim, several minutes on alpine — after you apt/apk-add gcc, g++, musl-dev, and headers. Those build deps are often 200MB+, so your "smaller" image ends up bigger than the one you were avoiding.
musllinux wheels exist now, but coverage is patchy. Check your actual dependency tree before committing.
Gotcha 2 — DNS in Kubernetes
musl's resolver isn't glibc's. It queries A and AAAA in parallel, and before musl 1.2.4 it had no TCP fallback when a UDP response exceeded 512 bytes.
Symptom: intermittent "no such host" for short service names on alpine pods, and the identical manifest on debian-slim is clean. People burn days blaming CoreDNS for this.
Gotcha 3 — malloc under threads
musl's allocator is far simpler than glibc's. On allocation-heavy multithreaded workloads it can cost you real throughput. Fine for a sidecar, not fine for a hot JSON-parsing service.
The bit that reframes it
Image size ≠ pull time. Base layers are cached per node and shared across images. If 20 of your services already sit on debian:12-slim, that 30MB is pulled once, ever. Moving one service to alpine doesn't save 23MB — it introduces a second base layer that node now has to pull and keep.
What to actually do
- Multi-stage, always: build fat, copy the artifact into a thin runtime.
- Static Go/Rust binary → distroless:static or scratch.
- Python / Node / JVM → -slim. Reach for alpine only after you've measured.
- Want fewer CVEs rather than fewer MB? distroless or Chainguard beats alpine — no shell, no package manager, nothing to exploit. Note that "no shell" also means no
kubectl exec ... sh; you debug with an ephemeral container instead.
Your exercise
Take one real service. Build it twice — alpine and slim. Record three numbers:
docker build ... → build time
docker images → final size
docker history <img> → how much your build deps clawed back
Then: is the alpine image still smaller once the toolchain is in it? And how much of the difference is a layer your nodes have already cached anyway?