Base Image — The Foundation of Every Docker Image
What Is a Base Image in Simple Terms?
Every Docker image starts from something. That something is the base image — specified in the FROM instruction at the top of your Dockerfile. The base image provides the operating system environment, the package manager, and the language runtime (if any) that your application runs on top of.
Choosing the right base image is the single most impactful decision in Dockerfile writing. The wrong base image adds 1GB to your image size and 200 CVEs to your security exposure before you write a single line of your own code.
+------------------------------------------+| FROM node:20-alpine <- base image || || Provides: || Alpine Linux 3.18 (7MB OS) || Node.js 20 runtime (48MB) || npm package manager || Total: ~55MB |+------------------------------------------+| Your application layers go on top || COPY, RUN, CMD instructions add layers |+------------------------------------------+Base Image Options for Node.js — Size and Security Comparison
# Pull and comparedocker pull node:20 # 1.1GB — full Debian + build toolsdocker pull node:20-slim # 220MB — Debian without build toolsdocker pull node:20-alpine # 55MB — Alpine Linux (musl libc)docker pull gcr.io/distroless/nodejs20-debian12 # 120MB — no shell at all # CVE comparisontrivy image node:20 # ~200 CVEs (CRITICAL + HIGH)trivy image node:20-alpine # ~0 CVEs # node:20-alpine wins on size and security for most appsWhen to Use Each Base Image
node:20-alpine Use when: most Node.js applications Pros: smallest size, fewest CVEs Cons: uses musl libc — some native modules break Test: bcrypt, canvas, sharp sometimes fail on Alpine node:20-slim Use when: app uses native modules that need glibc Pros: glibc compatible, smaller than full node:20 Cons: larger than alpine, more CVEs than alpine Test: most native modules work here node:20 (full Debian) Use when: build stage only — needs gcc, python, make Never use as final production stage Size: 1.1GB — too large for production distroless/nodejs20 Use when: maximum security, no debugging needed Pros: no shell, no package manager = minimal attack surface Cons: cannot exec in for debugging Use with: multi-stage builds only scratch (empty image) Use when: statically compiled binaries (Go, Rust) Pros: absolute minimum — just your binary Cons: no shell, no certs, no timezone data Requires: static linking (CGO_ENABLED=0 for Go)Pinning Base Images for Reproducibility
# BAD — tag can change, build is not reproducibleFROM node:20-alpine # BETTER — specific version pinnedFROM node:20.10.0-alpine3.18 # BEST — pinned by digest (immutable, always exact same bytes)FROM node:20-alpine@sha256:a84f9c2b1d3e...# Get the digest:# docker pull node:20-alpine# docker inspect node:20-alpine --format '{{index .RepoDigests 0}}'Keeping Base Images Updated
# Check your current base image for CVEstrivy image node:20-alpine # Use Renovate or Dependabot to automate base image updates# These tools open PRs when new base image versions are available# Your CI scans the new image before merging# Combine: automated updates + CI scanning = always current and securePLACEMENT PRO TIP**Tip:** Always check if a `-alpine` variant exists for your language runtime before accepting the default. `node:20` vs `node:20-alpine` is a 1GB vs 55MB difference — a 20x size reduction with no application code changes required.
COMMON MISTAKE / WARNING**Common Mistake:** Using the full `node:20` or `python:3.11` base image in production. These are designed for development convenience — they include build tools, compilers, and debugging utilities that serve no purpose in a production container and add hundreds of CVEs.