Portal | Level: L1: Foundations | Topics: Container Base Images, Docker / Containers, Container Image Optimization (alias → container_images), Container Image Scanning (alias → container_images) | Domain: Kubernetes
Container Base Images — Primer¶
Why This Matters¶
Your base image choice determines container size, security surface, compatibility, and debugging capability. The difference between a good and bad choice can be hundreds of MB of attack surface, mysterious runtime failures, or the inability to troubleshoot a production issue.
Base Image Options¶
The Lineup¶
| Base Image | Size | libc | Shell | Package Manager | Best For |
|---|---|---|---|---|---|
| Ubuntu | ~75MB | glibc | bash | apt | General purpose, familiar tooling |
| Debian | ~125MB | glibc | bash | apt | Stable, well-tested, wide compat |
| Debian-slim | ~75MB | glibc | bash | apt | Production apps needing glibc |
| Alpine | ~7MB | musl | ash | apk | Tiny images, Go/Rust binaries |
| Distroless | ~20MB | glibc | none | none | Maximum security, static binaries |
| scratch | 0MB | none | none | none | Statically compiled Go/Rust |
| UBI (Red Hat) | ~200MB | glibc | bash | dnf (microdnf) | Enterprise, RHEL compat |
| Chainguard | ~15MB | glibc/musl | none | apk | Supply chain security |
| Wolfi | ~15MB | glibc | none | apk | Undistro (glibc + minimal) |
Alpine Linux¶
Why Alpine?¶
# Tiny base — 7MB compressed
FROM alpine:3.20
# Package manager is fast
RUN apk add --no-cache python3 py3-pip
# Result: much smaller than ubuntu or debian-based
Alpine Internals¶
- musl libc instead of glibc — smaller but not 100% compatible
- BusyBox provides coreutils —
ashshell, not bash - apk package manager — fast, simple, no caching by default
- OpenRC init (not systemd) — irrelevant in containers
Common Alpine Patterns¶
# Python on Alpine (careful — compiling C extensions is slow)
FROM python:3.12-alpine
RUN apk add --no-cache gcc musl-dev linux-headers
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Go on Alpine (great fit — Go binaries are static by default)
FROM golang:1.22-alpine AS build
WORKDIR /app
COPY . .
RUN CGO_ENABLED=0 go build -o /server
FROM alpine:3.20
COPY --from=build /server /server
CMD ["/server"]
# Node.js on Alpine
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
CMD ["node", "server.js"]
Name origin: Alpine Linux is named after the Alpine climate zone — small, tough, high-altitude. Created by Natanael Copa in 2005, it was originally a fork of LEAF (Linux Embedded Appliance Framework). Its adoption exploded around 2015 when Docker popularized minimal container base images.
musl vs glibc — Compatibility Issues¶
| Issue | Symptom | Fix |
|---|---|---|
| DNS resolution | Intermittent failures, no search domain support | Use glibc-based image |
| Python C extensions | Build fails (missing headers) | apk add gcc musl-dev |
| Node native modules | Compilation errors | apk add python3 make g++ |
| Go with CGO | Linking errors | CGO_ENABLED=0 or use glibc |
| Java | JVM performance differences | Use Eclipse Temurin Alpine JDK |
| Locale support | Missing locales, encoding errors | apk add musl-locales |
| Thread performance | Different thread stack defaults | Usually not noticeable |
Debian-Slim¶
The Sweet Spot¶
# glibc compatibility + much smaller than full Debian
FROM debian:bookworm-slim
# ~75MB vs ~125MB full Debian, ~300MB Ubuntu
# Includes apt, so you can install what you need
RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates \
curl \
&& rm -rf /var/lib/apt/lists/*
When to Choose Debian-Slim¶
- You need glibc (Python C extensions, Java, .NET)
- You need apt for installing packages
- You want to debug (has a shell)
- Alpine's musl is causing issues
- You want a good size-to-compatibility ratio
Distroless¶
Maximum Security¶
# No shell, no package manager, no OS-level tools
# Only your binary + runtime libraries
# Python distroless
FROM python:3.12-slim AS build
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
FROM gcr.io/distroless/python3-debian12
COPY --from=build /app /app
COPY --from=build /usr/local/lib/python3.12 /usr/local/lib/python3.12
WORKDIR /app
CMD ["app.py"]
# Go distroless
FROM golang:1.22 AS build
WORKDIR /app
COPY . .
RUN CGO_ENABLED=0 go build -o /server
FROM gcr.io/distroless/static-debian12
COPY --from=build /server /server
CMD ["/server"]
# Java distroless
FROM eclipse-temurin:21-jdk AS build
WORKDIR /app
COPY . .
RUN ./gradlew build
FROM gcr.io/distroless/java21-debian12
COPY --from=build /app/build/libs/app.jar /app.jar
CMD ["app.jar"]
Distroless Pros and Cons¶
Pros: - Minimal attack surface (no shell = no shell exploits) - No package manager (can't install malware) - Fewer CVEs to patch - Forces multi-stage builds (good practice)
Cons:
- Cannot exec into container — no shell for debugging
- Need debug images for troubleshooting: gcr.io/distroless/base-debian12:debug
Debug clue: When you need to troubleshoot a distroless container in production, use
kubectl debugto attach an ephemeral debug container with a shell:kubectl debug -it podname --image=busybox --target=containername. This gives you shell access in the pod's network and PID namespace without modifying the original container image. - Harder to test locally - Some apps need runtime dependencies that are missing
scratch — The Zero Base¶
# Literally nothing — 0 bytes
# Only for statically compiled binaries
FROM golang:1.22 AS build
WORKDIR /app
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o /server
FROM scratch
# Need CA certs if making HTTPS calls
COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=build /server /server
ENTRYPOINT ["/server"]
scratch is for: Statically compiled Go, Rust, or C binaries only. No libc, no certs, no timezone data, no /tmp, no users. You copy in everything your binary needs.
Red Hat UBI (Universal Base Image)¶
# Enterprise-grade, RHEL-compatible, free to use
FROM registry.access.redhat.com/ubi9/ubi-minimal:latest
# microdnf instead of dnf (smaller)
RUN microdnf install -y python3 pip && microdnf clean all
# UBI variants:
# ubi9/ubi — full (~215MB, includes dnf, systemd)
# ubi9/ubi-minimal — smaller (~95MB, microdnf)
# ubi9/ubi-micro — tiny (~28MB, no package manager)
# ubi9/ubi-init — with systemd (for multi-process containers)
When to use UBI: - Running on OpenShift (certified base) - Enterprise requirements for RHEL compatibility - Need vendor support for the base image - Compliance requirements (FIPS, STIG)
Chainguard & Wolfi¶
# Chainguard — hardened, minimal, signed, SBOM'd
FROM cgr.dev/chainguard/python:latest
# Wolfi — glibc-based "undistro" (Alpine-style but glibc)
FROM cgr.dev/chainguard/wolfi-base:latest
RUN apk add python-3.12
Key features: - Zero known CVEs (aggressively patched) - Signed images with provenance - SBOM (Software Bill of Materials) built-in - glibc-based (no musl issues) - Very small
Multi-Stage Build Pattern¶
# Stage 1: Build (large image with build tools)
FROM python:3.12-slim AS build
WORKDIR /app
# Install build dependencies
RUN apt-get update && apt-get install -y --no-install-recommends \
gcc \
libpq-dev \
&& rm -rf /var/lib/apt/lists/*
COPY requirements.txt .
RUN pip install --no-cache-dir --prefix=/install -r requirements.txt
COPY . .
# Stage 2: Runtime (small image, no build tools)
FROM python:3.12-slim
WORKDIR /app
# Copy only what we need from build stage
COPY --from=build /install /usr/local
COPY --from=build /app /app
# Non-root user
RUN useradd -r -s /sbin/nologin appuser
USER appuser
EXPOSE 8000
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
Decision Tree¶
Do you need a shell for debugging?
├── No → Do you need glibc?
│ ├── No → scratch (Go/Rust static binary)
│ └── Yes → distroless or Chainguard
└── Yes → Does your app need glibc?
├── No → Alpine (smallest with shell)
└── Yes → Does your company require RHEL support?
├── Yes → UBI minimal
└── No → Debian-slim (best all-around)
Image Size Comparison (Python app example)¶
ubuntu:22.04 + Python + deps = ~450MB
python:3.12 (full Debian) = ~350MB
python:3.12-slim (Debian-slim) = ~150MB
python:3.12-alpine = ~60MB
distroless/python3 = ~50MB
chainguard/python = ~45MB
Every 100MB removed: - Faster pulls (especially cold starts on new nodes) - Less disk usage across the fleet - Fewer CVEs to scan and patch - Smaller attack surface
Security Scanning¶
# Scan with Trivy
trivy image myapp:latest
# Compare CVE counts across base images
trivy image alpine:3.20
trivy image debian:bookworm-slim
trivy image ubuntu:22.04
trivy image cgr.dev/chainguard/python:latest
# Fix: update base image, rebuild
# Most CVEs are in the base image, not your code
Rule: Rebuild containers regularly even if your code hasn't changed. Base image CVEs are discovered daily.
Gotcha: Scanning only your final image misses vulnerabilities introduced in the build stage (e.g., a compromised build tool). Use
trivy fs .to scan your Dockerfile and source code, andtrivy imageto scan the built image. Multi-stage builds reduce the attack surface but do not eliminate supply chain risks in the build layer.
Image Optimization¶
Multi-Stage Builds¶
The most impactful optimization. Build dependencies stay in the build stage; only the runtime binary goes into the final image.
# Go — results in 5-15MB image
FROM golang:1.22-alpine AS builder
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /app
FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=builder /app /app
ENTRYPOINT ["/app"]
# Python — pip wheel pattern
FROM python:3.11-slim AS builder
WORKDIR /build
COPY requirements.txt .
RUN pip wheel --no-cache-dir --wheel-dir /wheels -r requirements.txt
FROM python:3.11-slim
WORKDIR /app
COPY --from=builder /wheels /wheels
RUN pip install --no-cache-dir --no-index --find-links=/wheels /wheels/* && rm -rf /wheels
COPY . .
USER 1000:1000
CMD ["python", "-m", "uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8000"]
Layer Optimization¶
# BAD: Each RUN creates a layer; intermediate files persist
RUN apt-get update
RUN apt-get install -y curl wget git
RUN rm -rf /var/lib/apt/lists/* # too late — still in previous layers
# GOOD: Single RUN, cleanup in same layer
RUN apt-get update \
&& apt-get install -y --no-install-recommends curl wget git \
&& rm -rf /var/lib/apt/lists/*
# Order layers by change frequency (least changed first)
COPY go.mod go.sum ./ # changes rarely
RUN go mod download # cached when go.mod unchanged
COPY . . # changes every build
BuildKit Caching¶
# Cache apt packages
RUN --mount=type=cache,target=/var/cache/apt \
--mount=type=cache,target=/var/lib/apt \
apt-get update && apt-get install -y build-essential
# Cache pip downloads
RUN --mount=type=cache,target=/root/.cache/pip \
pip install -r requirements.txt
Layer Analysis Tools¶
docker history myapp:latest # layer sizes
dive myapp:latest # interactive layer explorer
docker images --format "table {{.Repository}}\t{{.Tag}}\t{{.Size}}" | sort -k3 -h
Image Scanning¶
How Scanners Work¶
Scanners extract the SBOM (Software Bill of Materials) from an image, then match it against CVE databases:
Trivy (Most Common)¶
# Scan an image
trivy image myapp:latest
trivy image --severity CRITICAL,HIGH myapp:latest
# CI gate — exit 1 on critical CVEs
trivy image --exit-code 1 --severity CRITICAL myapp:latest
# Generate SBOM
trivy image --format cyclonedx --output sbom.cdx.json myapp:latest
# Scan filesystem (before building image)
trivy fs --security-checks vuln,secret .
Suppress known false positives with .trivyignore:
Grype (Alternative)¶
grype myapp:latest --fail-on critical
# Scan from SBOM (no Docker daemon needed)
syft packages myapp:latest -o cyclonedx-json > sbom.cdx.json
grype sbom:sbom.cdx.json
CI Integration (GitHub Actions)¶
- name: Run Trivy scan
uses: aquasecurity/trivy-action@master
with:
image-ref: myapp:${{ github.sha }}
format: sarif
output: trivy-results.sarif
severity: CRITICAL,HIGH
exit-code: 1
ignore-unfixed: true
Base Image Update Automation¶
Use Renovate or Dependabot to automatically create PRs when base images are updated:
# .github/dependabot.yml
version: 2
updates:
- package-ecosystem: "docker"
directory: "/"
schedule:
interval: "weekly"
| Severity | CVSS Range | CI Gate |
|---|---|---|
| CRITICAL | 9.0-10.0 | Always block |
| HIGH | 7.0-8.9 | Block (with --ignore-unfixed) |
| MEDIUM | 4.0-6.9 | Report, schedule fix |
| LOW | 0.1-3.9 | Track |
Wiki Navigation¶
Prerequisites¶
- Linux Ops (Topic Pack, L0)
Related Content¶
- Deep Dive: Docker Image Internals (deep_dive, L2) — Container Image Optimization (alias → container_images), Docker / Containers
- AWS ECS (Topic Pack, L2) — Docker / Containers
- Case Study: CI Pipeline Fails — Docker Layer Cache Corruption (Case Study, L2) — Docker / Containers
- Case Study: Container Vuln Scanner False Positive Blocks Deploy (Case Study, L2) — Docker / Containers
- Case Study: ImagePullBackOff Registry Auth (Case Study, L1) — Docker / Containers
- Container Base Images Flashcards (CLI) (flashcard_deck, L1) — Container Base Images
- Containers Deep Dive (Topic Pack, L1) — Docker / Containers
- Deep Dive: Containers How They Really Work (deep_dive, L2) — Docker / Containers
- Docker (Topic Pack, L1) — Docker / Containers
- Docker Basics Flashcards (CLI) (flashcard_deck, L1) — Docker / Containers
Pages that link here¶
- AWS ECS
- Anti-Primer: Container Base Images
- Certification Prep: CKAD — Certified Kubernetes Application Developer
- Certification Prep: CKS — Certified Kubernetes Security Specialist
- Comparison: Image Scanners
- Container Images
- Containers - How They Really Work
- Containers Deep Dive
- Docker
- Docker Drills
- Docker Image Internals
- Incident Replay: ImagePullBackOff — Registry Authentication Failure
- Linux Ops
- Production Readiness Review: Answer Key
- Production Readiness Review: Study Plans