Skip to main content

Base Image

The starting point of a Dockerfile specified in the FROM instruction. The base image determines the operating system, package manager, and initial filesystem of the final image — and is the single biggest factor in image size and security.

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.

Bash
+------------------------------------------+
| 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

Bash
# Pull and compare
docker pull node:20 # 1.1GB — full Debian + build tools
docker pull node:20-slim # 220MB — Debian without build tools
docker pull node:20-alpine # 55MB — Alpine Linux (musl libc)
docker pull gcr.io/distroless/nodejs20-debian12 # 120MB — no shell at all
# CVE comparison
trivy image node:20 # ~200 CVEs (CRITICAL + HIGH)
trivy image node:20-alpine # ~0 CVEs
# node:20-alpine wins on size and security for most apps

When to Use Each Base Image

TEXT
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

Dockerfile
# BAD — tag can change, build is not reproducible
FROM node:20-alpine
# BETTER — specific version pinned
FROM 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

Bash
# Check your current base image for CVEs
trivy 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 secure
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

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.

Frequently Asked Questions

Why does base image choice matter more than almost any other Dockerfile decision?

The base image sets the OS layer, package manager, libc implementation, and every pre-installed binary the final image inherits, and it's typically the single largest layer by size — a `python:3.12` base is roughly 900MB+ versus `python:3.12-slim` at under 200MB or `python:3.12-alpine` under 60MB. It also defines your CVE surface: a full Debian base ships hundreds of packages you never use, each a potential vulnerability that security scanners will flag even though your app never touches them.

What's a common mistake when switching to a smaller base image like Alpine?

Assuming Alpine is a drop-in replacement for a Debian/Ubuntu-based image. Alpine uses musl libc instead of glibc, which breaks some Python/Node native binary wheels compiled against glibc, causing cryptic segfaults or 'symbol not found' errors at runtime rather than build time. For apps with compiled dependencies, `-slim` variants (Debian-based, glibc-compatible) are often a safer size reduction than Alpine, which is better suited to statically-linked binaries with no native extensions.