Lesson 14 of 30
"Just use alpine, it's smaller" is the advice everyone repeats and the advice that occasionally breaks a build in a way that takes an afternoon to diagnose. The base image is a real tradeoff, not a solved default.
11 min read
Every option explains itself — why the right one is right, and why each wrong one is wrong. Miss one and it comes back later on a spaced-repetition schedule.
Question 1 of 3
Which best describes why a distroless base image trades away something Alpine still offers?
Hints without spoilers, and a plain explanation of why a wrong answer was wrong — answered only from this lesson. Free account, no card.
Sign up freeWhat interviewers actually ask about this, and what a strong answer has to cover.
A strong answer covers
Likely follow-ups
Every Dockerfile starts with FROM <something>, and that something sits underneath everything you build on top of it — its size, its C library, its shell, its package manager (or lack of one) all become properties of your final image whether you think about them or not. There's a real spectrum of options, not a single right answer:
scratch — a literally empty filesystem. No shell, no libc, no package manager, nothing. Only works for a statically-linked binary (a Go binary compiled with no dynamic dependencies, for instance) that needs nothing from an OS at runtime.gcr.io/distroless/*) — a minimal runtime with just what a specific language needs (a Node or Python runtime, common libraries) and deliberately no shell, no package manager, no apt/apk. Smaller attack surface than a full distro, but still usable for languages that need a real runtime, not just a static binary.node:20-alpine, python:3.12-alpine) — a genuinely minimal general-purpose Linux distro built on musl libc instead of the more common glibc. Very small, has a real package manager (apk) and a shell, so it's debuggable — but musl is not a drop-in replacement for glibc, and native packages compiled against glibc (common in Node native addons and Python C extensions) can fail or silently misbehave on Alpine.node:20-slim, node:20) — the most compatible, closest to a normal Linux environment, and the largest. glibc-based, so native dependencies behave the way they do everywhere else.FROM node:20 # ~1GB base — full Debian, glibc, every common tool present
FROM node:20-slim # ~200MB — Debian, glibc, stripped of non-essential packages
FROM node:20-alpine # ~50MB — musl libc, apk, a shell, no glibc compatibility
FROM gcr.io/distroless/nodejs20-debian12 # smaller still — no shell, no package manager at all
The size numbers matter less than what each one costs you. node:20-alpine is the popular default for a reason — it's dramatically smaller than the full image, and for a huge fraction of pure-JavaScript applications it works perfectly. The failure mode shows up specifically with native modules: a package like bcrypt or sharp that ships a compiled binary (or compiles one during pnpm install) may have been built against glibc assumptions that don't hold on musl, producing a runtime error — or worse, a subtly wrong result — that never shows up in local development if you're not developing on Alpine yourself.
docker exec -it <container> sh simply doesn't work against a distroless or scratch-based image — there's no shell to open. That's the tradeoff working as intended, and it means your debugging strategy has to lean on logs and metrics instead of live shell access.musl vs. glibc difference is not just a Node problem. Python C extensions, Rust binaries linked dynamically, and any tool that ships prebuilt binaries can hit the same class of failure — the risk is general to "prebuilt binary meets a libc it wasn't built for," not specific to any one language ecosystem."Why would you not always just pick the smallest base image?" is a genuine test of whether a candidate understands base images as a tradeoff (compatibility, debuggability, and ecosystem support against size and attack surface) rather than a single-axis "smaller is better" heuristic — and naming the glibc/musl compatibility gap specifically is what separates a real answer from a guessed one.