CodeOath
← All posts
Docker65 min total · 19 parts

Docker Fundamentals: Images, Containers, and Writing a Good Dockerfile

Contents — Part 10 of 19: Choosing a Base Image
Part 10 of 19 · ~1 min

Choosing a Base Image

The FROM line has more downstream impact than almost any other decision in a Dockerfile:

Base typeExampleSizeTrade-off
Full distributionnode:20, ubuntu:22.04Largest (100s of MB+)Includes a shell, package manager, debugging tools — easiest to work in
Slimnode:20-slimSmallerDebian-based but stripped of most non-essential packages
Alpinenode:20-alpineMuch smaller (~5–10x less than full)Uses musl libc instead of glibc — occasionally breaks native modules that assume glibc
Distrolessgcr.io/distroless/nodejs20Smallest practical optionNo shell, no package manager at all — can't docker exec a shell into it, meaningfully reduces attack surface

Alpine's smaller size comes from a genuinely different C standard library (musl instead of glibc), not just fewer packages — this occasionally surfaces as a real bug: a native Node addon or Python package compiled against glibc can fail to run (or behave subtly differently) on Alpine, which is why "just switch to alpine" sometimes needs more than swapping the FROM line. Distroless images take the size and security reduction further by removing the shell entirely, which is great for a production runtime but means you cannot docker exec -it container sh into one to debug — you'd need a debug variant of the image, or an ephemeral debug container attached to the same namespaces, for interactive troubleshooting.

A smaller image isn't purely about disk space — it's a smaller attack surface (fewer installed packages means fewer potential CVEs to patch), faster pulls (matters a lot for autoscaling, where new instances need the image before they can serve traffic), and faster CI.