HAMZA KAREEM / CONCEPTS hamzawi.dev

Lesson 512 min read

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?