The base image (FROM …) affects your image’s size, compatibility, security exposure, and how easy it is to debug. The most common choices are Alpine, Debian or Ubuntu “slim” variants, and distroless. None is best for everything. Here’s how to choose.

At a glance

Base Approx. size C library Shell & package manager Good for
alpine ~5–8 MB musl sh, apk Small images; static binaries; simple services
debian:bookworm-slim ~25–30 MB glibc bash, apt Most apps; best compatibility
ubuntu ~30 MB glibc bash, apt Teams standardised on Ubuntu
distroless (gcr.io/distroless) ~2–20 MB glibc (or none) None Production images with minimal attack surface
scratch 0 MB None None Fully static binaries (Go, Rust)
Language images (python:3.12-slim, node:22-alpine) Varies Depends on variant Yes Most app builds

Alpine: small, but musl matters

Alpine uses musl libc instead of glibc. Most software works, but:

  • Python: many packages ship prebuilt wheels for glibc (manylinux). On Alpine, pip may compile from source (slow builds, needs compilers), unless musllinux wheels exist.
  • Node.js native modules, and some binaries (Oracle clients, some monitoring agents), assume glibc.
  • DNS resolution and threading behave slightly differently, which occasionally causes hard-to-debug issues.

Alpine shines for Go and Rust binaries, Nginx, Redis, and simple utilities.

Debian slim: the safe default

-slim variants strip documentation and extras, but keep glibc and apt. They have the widest compatibility, and familiar tools for debugging. For most Python, Node, PHP and Java apps, *-slim (Debian bookworm) is the pragmatic default.

Distroless and scratch: minimal runtime

Distroless images contain only your app and its runtime libraries: no shell, no package manager. Benefits:

  • fewer CVEs in scanners, and less for an attacker to use if they get in;
  • smaller images.

Trade-offs:

  • no shell for docker exec debugging. Use docker debug, an ephemeral debug container, or the :debug distroless variants (which include busybox) in staging;
  • you must build in another stage. See multi-stage builds.
FROM golang:1.23 AS build
WORKDIR /src
COPY . .
RUN CGO_ENABLED=0 go build -o /app ./cmd/app

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

Size isn’t everything

  • Layers are cached and shared: if several images use the same base, it’s stored and pulled once.
  • A 30 MB difference rarely matters on a server. Build reliability and debuggability usually matter more.
  • The biggest savings come from multi-stage builds (no compilers or build tools in the final image), and cleaning package caches. See Docker image size reduction.

Security

Fewer packages means fewer vulnerabilities to patch. Whatever you choose:

Recommendations

App type Suggested final base
Go / Rust static binary scratch or distroless/static
Python python:3.x-slim (Debian); distroless/python for hardened builds
Node.js node:22-slim; Alpine only if native modules work
Java eclipse-temurin:21-jre (Ubuntu-based) or distroless/java
PHP php:8.3-fpm (Debian); Alpine variants are popular too
Utilities and proxies Alpine

Quick reference

Priority Choose
Compatibility Debian slim
Smallest general-purpose Alpine
Minimal attack surface Distroless / scratch
Easy debugging Debian slim / Ubuntu

Related: Dockerfile tutorial and build multi-arch images.

  • Docker
  • Best Practices
  • Security