Chapter 2: Choosing the Right Base Image


The FROM instruction is the most consequential line in your Dockerfile. It sets the floor for everything that follows — the minimum size your image can ever be, the set of packages it ships with, and the number of CVEs it inherits from day one. Choosing well here is the highest-return, lowest-effort optimisation available.


The Spectrum of Base Images

Base images exist on a spectrum from absolutely minimal to full-featured. Understanding where each option sits helps you choose the right trade-off for your workload.

scratch — Zero bytes

FROM scratch is a special Docker keyword that means “start with nothing”. The resulting image contains only what you explicitly copy into it. There are no utilities, no shell, no package manager, no libc.

This is the ideal choice for statically compiled binaries — Go applications in particular. The runtime image is literally your compiled executable and nothing else.

FROM scratch
COPY myapp /myapp
ENTRYPOINT ["/myapp"]

Size: As small as your binary. A typical Go web server: 10–20 MB.

Limitation: No shell means no docker exec debugging. No libc means dynamically linked binaries won’t run. Unsuitable for most interpreted runtimes.

Distroless — No shell, minimal OS

Google’s distroless images contain a language runtime (Python, Node, Java) and their dependencies but no shell (bash, sh), no package manager, and no general-purpose utilities. They are built for production runtime use only.

FROM gcr.io/distroless/python3-debian12
COPY --from=builder /opt/venv /opt/venv
COPY app /app
CMD ["app/main.py"]

Size: Python distroless: ~50 MB. Java distroless: ~130 MB.

Limitation: No shell makes interactive debugging impossible and some init/entrypoint scripts will fail. Build tooling must be in a separate stage.

Alpine — Minimal Linux

Alpine Linux uses musl libc instead of glibc and busybox instead of GNU coreutils, resulting in a base image of roughly 5 MB. The apk package manager is fast and the package ecosystem is large.

Many official Docker Hub images offer Alpine variants: python:3.12-alpine, node:20-alpine, golang:1.22-alpine.

Size: Base Alpine: ~5 MB. Python on Alpine: ~50–80 MB.

Critical caveat: Python binary wheels on PyPI are compiled for glibc (manylinux). On Alpine (musl), packages with C extensions either fail to install or must be compiled from source — which requires installing build tools and negates much of the size advantage. This is discussed in detail in Chapter 7.

Slim Debian/Ubuntu variants

Official images offer slim variants (e.g., python:3.12-slim-bookworm, node:20-slim) that strip out documentation, man pages, locale data, and packages not needed at runtime. They retain glibc, making them fully compatible with pre-built binary wheels.

Size: python:3.12-slim-bookworm: ~45 MB base. Your final image with dependencies: 80–150 MB.

Best for: Python and Node applications with C extension dependencies; teams that want compatibility without the Alpine musl friction.

Full images — Avoid in production

Images like python:3.12, node:20, ubuntu:22.04 are designed for development and build environments. They include compilers, build tools, documentation, and hundreds of packages you will never use in production.

Size: python:3.12: ~1 GB. node:20: ~1.1 GB.

Use only as build stages, never as the final runtime image.


Comparison Table

Base image Approx. size glibc Shell Debug ease CVE surface
scratch 0 MB No No Very hard Minimal
distroless 5–130 MB Yes No Hard Very low
alpine 5 MB No (musl) Yes (busybox) Moderate Low
slim (Debian) 30–80 MB Yes Yes Easy Low–Medium
Full (Debian) 300–900 MB Yes Yes Easy High

Pinning Image Versions

Using python:3.12-slim-bookworm is better than python:3.12-slim, because the Debian codename (bookworm) anchors you to a specific OS version. But a tag can still be updated by the maintainer with new package versions that introduce incompatibilities or new CVEs.

For maximum reproducibility, pin to a digest:

FROM python:3.12-slim-bookworm@sha256:a1b2c3d4...

The SHA256 digest uniquely identifies a specific image manifest. It will never change. The trade-off is that you must update it explicitly when you want upstream security patches — which is actually a feature for teams that want predictable, auditable builds.

Finding the digest

docker pull python:3.12-slim-bookworm
docker image inspect python:3.12-slim-bookworm \
  --format ''

Auditing Your Current Base Image

Before switching, audit what you have:

# See how many CVEs your current base image has
docker scout cves python:3.12

# Get a recommendation for a better base image
docker scout recommendations python:3.12

docker scout requires a Docker Hub account for full results but the free tier covers most use cases. The output typically shows the current CVE count and a recommended alternative with a reduced count.


Choosing in Practice

Use this decision tree:

  1. Go application?FROM scratch (or alpine if you need a shell for scripts)
  2. Java application?eclipse-temurin:21-jre-alpine or gcr.io/distroless/java21-debian12
  3. Python with C extensions (numpy, pandas, psycopg2)?python:3.12-slim-bookworm
  4. Python with pure-Python dependencies only?python:3.12-alpine or python:3.12-slim-bookworm
  5. Node.js production runtime?node:20-alpine
  6. Need to debug in production? → slim Debian variants (Alpine or distroless make exec very painful)

Key Takeaways


← Chapter 1: Understanding Docker Image Layers Table of Contents Chapter 3: Multi-Stage Builds →