sehaj/learn
Docker/02 Images

02.07 · concept · 10 min

Choosing a base image

Full, slim, alpine or distroless: what each actually contains, what breaks, and a default you can defend.

The core idea

Your base image is most of your image. It decides your C library, your package manager, which debugging tools exist inside the container, and how many packages a security scanner will flag. There's no single best choice, but there are four families, and knowing what's inside each makes the choice easy.

The four families

Measured with the notes API on top of each (arm64; amd64 differs slightly):

BaseWhat's insideC libraryNotes API image
node:22Debian plus build tools: git, python3, gcc, curlglibc1.63 GB base alone
node:22-slimminimal Debian: apt and bash, no curl or compilersglibc349 MB
node:22-alpineAlpine Linux: busybox and apkmusl238 MB
gcr.io/distroless/nodejs22-debian12Node and its runtime libraries. No shell, no package managerglibc212 MB

You can check these yourself:

❯terminal
$ docker run --rm node:22 sh -c 'which curl git python3 gcc'
/usr/bin/curl
/usr/bin/git
/usr/bin/python3
/usr/bin/gcc
$ docker run --rm node:22-slim sh -c 'which curl apt-get bash'
/usr/bin/apt-get
/usr/bin/bash
$ docker run --rm node:22-slim sh -c 'ldd --version | head -1'
ldd (Debian GLIBC 2.36-9+deb12u14) 2.36
$ docker run --rm node:22-alpine sh -c 'ldd --version 2>&1 | head -1'
musl libc (aarch64)

Notice that node:22-slim has no curl. That matters the first time you write a healthcheck that calls curl.

glibc vs musl

Alpine is small partly because it uses musl instead of glibc as its C library. Pure JavaScript, Python or Go code doesn't care. Native code does:

  • npm packages with native addons (for example bcrypt, sharp) need musl builds or compile from source.
  • Python wheels on PyPI are mostly built for glibc, so on Alpine pip often compiles from source, which is slow and needs build tools.
  • DNS resolution and some locale and threading behaviour differ in small ways that occasionally surface as bugs.

None of this makes Alpine bad. It means "switch to Alpine" is a change to test, not a free win.

Distroless: nothing to log into

Distroless images contain your language runtime and the libraries it needs, and nothing else. No shell, so this fails:

❯terminal
$ docker run --rm --entrypoint sh notes-api:distroless -c 'echo hi'
docker: Error response from daemon: ... exec: "sh": executable file not found in $PATH

Output trimmed. That's the point: an attacker who gets code execution has no shell, no package manager and no curl to work with, and scanners have far fewer packages to flag. The cost is that you can't docker exec -it ... sh to debug. You debug from outside instead, or use a :debug variant of the image temporarily.

Distroless also changes how you write the Dockerfile: there's no npm in the final image, so you copy a pruned node_modules from the build stage, and CMD lists only the script because the entrypoint is already node:

Dockerfile.distroless
FROM node:22-slim AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY tsconfig.json ./
COPY src ./src
RUN npm run build && npm prune --omit=dev

FROM gcr.io/distroless/nodejs22-debian12:nonroot
ENV NODE_ENV=production
WORKDIR /app
COPY --from=build /app/node_modules ./node_modules
COPY --from=build /app/dist ./dist
EXPOSE 3000
CMD ["dist/server.js"]

The :nonroot tag runs as user 65532 by default.

Predict first

You switch a Python service from python:3.12-slim to python:3.12-alpine. The build now takes 6 minutes instead of 40 seconds. What's the likely cause?

Commit to an answer before you read on.

A default you can defend

  • Start with -slim. glibc compatibility, a package manager when you need one, a shell for debugging, and a reasonable size. It's the least surprising choice.
  • Move to distroless for production services once your Dockerfile is stable and you're comfortable debugging without a shell.
  • Use Alpine when size really matters and your dependencies are known to work on musl.
  • Use the full image for build stages that need compilers, not for what you ship.

Whichever you choose, pin the major version (node:22-slim, not node:slim), and consider pinning by digest (02.06 Tags, digests and registries).

CheckpointLook inside each base

Run sh -c 'cat /etc/os-release | head -2; which apt-get apk curl' in node:22-slim and node:22-alpine. Then build the distroless variant above and try docker exec -it into it. What's the error, and how would you debug a problem in that container?

STUCK? ▸

pull access denied for distroless: check the full name, gcr.io/distroless/nodejs22-debian12. It's hosted on Google's registry, not Docker Hub.

Recap

Interview questions

MidfreqMust-knowDesign

How do you choose a base image? Compare slim, alpine and distroless.

SeniorfreqGood to knowConcept

What's the security argument for distroless images?

Report a mistakeEdit this page

Fundamentals you'll use

☰ Images02.07 · 13/44next →