skip to content

questions

4

You are picking the base image named in a Dockerfile's FROM instruction for a backend service. Compare a full distribution image (debian/ubuntu), a -slim variant, Alpine, distroless images, and scratch, and explain how you would choose between them.

level: juniorimportance: must knowfreq 70%

answer

  1. FROM = your rootfs, libc, certs, CVEs
  2. full > slim > alpine > distroless > scratch
  3. alpine = musl, distroless = glibc, no shell
  4. scratch needs certs, passwd, tzdata copied in
  5. build fat, ship thin (multi-stage)

basics

~20 s

FROM sets the filesystem your image starts from. Full distro images have every tool but are large; -slim strips docs and extras; Alpine is tiny but uses musl libc; distroless ships only the runtime with no shell or package manager; scratch is empty and only fits static binaries. Pick the smallest image that still runs and that you can still debug.

solid answer

~50 s

FROM picks the root filesystem the build starts from: C library, package manager, CA certificates, shell. The ladder is full distro (debian:bookworm, every tool, biggest patch surface), -slim (same distro minus docs, locales, extra packages), Alpine (~5MB, musl libc plus BusyBox, apk), distroless (runtime, certs and libc only, no shell or package manager), and scratch (empty, only static binaries needing no libc, DNS or TLS store). My default is build in a fat image, ship from a thin one via a multi-stage build. For Go or Rust I ship distroless-static or scratch. For JVM or Python I prefer distroless or a -slim distro over Alpine because musl bites on native dependencies. The real trade is image size and CVE surface against debuggability: in a shell-less image an exec gives you nothing, so I decide up front whether I rely on ephemeral debug containers or publish a matching :debug variant.

code

dockerfile · 9 lines
dockerfile
FROM golang:1.23 AS build
WORKDIR /src
COPY . .
RUN CGO_ENABLED=0 go build -o /out/app ./cmd/app

FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=build /out/app /app
USER nonroot:nonroot
ENTRYPOINT ["/app"]

go deeper

for a junior

Know that FROM chooses the starting filesystem, name the main options, and say smaller means fewer tools and fewer vulnerabilities.

for a middle

Explain the concrete differences (musl vs glibc, no shell or package manager in distroless, empty scratch) and pair the choice with a multi-stage build.

for a senior

Frame it as a trade between attack surface and operability: name the debugging plan for shell-less images and the rebuild cadence for base image CVEs.

for a principal

Talk about it as a fleet decision: a small set of approved, centrally rebuilt golden bases, an org-wide policy on shell-less runtimes, and the tooling (ephemeral debug containers, SBOMs, scan gates) that makes that policy livable.

## What FROM does A Docker image is a stack of read-only filesystem layers plus config metadata. FROM <image> means "start my layer stack from this image". Everything that image contains is present at runtime: the C library, shell, package manager, CA trust store, timezone data, and every CVE in those packages. So the base image choice fixes your image size floor, your vulnerability surface, and what tooling exists when something goes wrong in production. ## The ladder **Full distribution** (debian:bookworm, ubuntu:24.04, ~80-120MB). Complete userland: bash, apt, coreutils, glibc. Everything just works, and you can install anything at build time or debug time. Cost: hundreds of packages you never call, each one a line item in a scanner report. **-slim variants** (debian:bookworm-slim, python:3.12-slim). Same distro and same glibc, with documentation, locales, and optional packages removed. Roughly one third to one half the size, and binary compatibility is unchanged. This is the safest size win available: nothing about your application's linking changes. **Alpine** (~5-8MB). Uses musl libc instead of glibc and BusyBox instead of GNU coreutils, with the apk package manager. Dramatically smaller, but musl is a different libc implementation. Anything with precompiled native code built against glibc (many Python wheels, some Node native modules, some JVM agents) either fails to load or must be recompiled. Alpine also historically differs on DNS resolution and thread stack sizes. **Distroless** (gcr.io/distroless/*). Not a distribution at all: a curated image containing only a language runtime plus glibc, CA certificates, and timezone data. No shell, no package manager, no busybox. It is glibc-based, so binary compatibility matches Debian. Typically runs as nonroot by default. The cost is that docker exec or kubectl exec into it gives you nothing to run, so debugging must be planned. **scratch**. The empty image. Only viable for a fully static binary. Because there is no CA bundle, no /etc/passwd, no /tmp, and no timezone database, you must COPY those in yourself if the program needs outbound TLS, a non-root UID, or local time. ## How to choose Split build from runtime with a multi-stage build. The build stage can be as fat as you like; only the final stage's base ships. Then choose the runtime base by what your binary links against. Statically linked Go or Rust: scratch or distroless-static. Dynamically linked or JVM/Python/Node: distroless or a -slim glibc distro. Choose Alpine deliberately, only when you have verified the native dependency story, not reflexively because it is small. Size matters less than people claim for pull time (layers cache on nodes) and more than people claim for attack surface and scanner noise. The frequently overlooked axis is operability: if your on-call procedure is "exec in and run curl", a shell-less base changes that procedure. Modern answer is kubectl debug ephemeral containers, which attach a tools image to a running pod's namespaces without a shell in your image. Confirm your platform supports it before shipping distroless. Finally, prefer official or vendor-maintained bases and rebuild regularly. A base image is a dependency that rots: your Dockerfile can be unchanged for six months and the image still accumulate CVEs, which is why rebuild cadence matters more than the initial pick.

  • Your production image is distroless and has no shell. A pod is misbehaving and you need to inspect it. What do you do?
    Use an ephemeral debug container (kubectl debug -it pod --image=busybox --target=<container>), which joins the running container's process and network namespaces with a tools image, so you get a shell without one existing in your image. Locally the equivalent is docker run --pid=container:<id> --network=container:<id> with a tools image. As a fallback, publish a :debug tag of the same image built on a busybox base with identical application layers, so you can redeploy the debug variant with the same binary.
  • Does a smaller base image actually make deploys faster?
    Less than people expect. Layers are cached on the node, so after the first pull only changed layers transfer, and the base layer is shared across every image built on it. Size mostly buys you a smaller attack surface, fewer scanner findings, and cheaper registry storage and cold starts on ephemeral or scale-to-zero nodes.

Choosing a base image is like choosing a kitchen to cook in: a full restaurant kitchen has every tool but a lot to clean and secure; a camping stove is tiny but you cannot bake; an empty room means you bring literally everything, including the water.

saying these in an interview costs you the question

  • Claiming Alpine is always the right choice because it is smallest, without mentioning musl
  • Thinking distroless is just a very small Linux distro you can apt-get into
  • Using scratch and being surprised that outbound HTTPS fails because no CA bundle was copied
  • Believing the base image choice does not affect the CVE report because your own code is unchanged
  • Assuming an unchanged Dockerfile means an unchanged security posture, so never rebuilding

context

open as a page

In a Dockerfile's FROM instruction, what is the difference between referencing a base image by tag (for example ubuntu:24.04) and by digest (image@sha256:...), and when does each matter?

level: middleimportance: must knowfreq 60%

basics

~20 s

A tag is a mutable pointer: the same tag can resolve to different image content over time. A digest is the sha256 hash of the image manifest, so it is immutable and content-addressed. Tags give you automatic patch updates; digests give you reproducible, verifiable builds. Pin digests for release builds and update them with automation.

open as a page

A Python service builds and runs fine on a Debian-based image, but after someone switches the Dockerfile's FROM instruction to an Alpine base the build gets much slower and a native library fails to load at runtime. Explain what is going on and how you would decide whether to stay on Alpine.

level: seniorimportance: should knowfreq 45%

basics

~20 s

Alpine uses musl libc instead of glibc, so precompiled binaries and Python manylinux wheels built for glibc do not apply. Package managers fall back to compiling from source, which is slow and needs toolchains, and mismatched native libraries fail to load. Unless you have verified the native dependency chain, prefer a glibc -slim base.

open as a page

How do you make the image referenced by a Dockerfile's FROM instruction configurable at build time, and what is the scoping rule for an ARG declared before the first FROM?

level: seniorimportance: nice to knowfreq 30%

basics

~20 s

Declare ARG above the first FROM and use it in the FROM line, for example ARG BASE=debian:bookworm-slim then FROM ${BASE}. Such an ARG lives in a global scope usable only by FROM lines; to use its value inside a build stage you must re-declare a bare ARG with the same name after that stage's FROM.

open as a page