playbook/antigravity-awesome-skills/skills/container-security-hardening/references/base-image-comparison.md

8.2 KiB
Raw Blame History

Base Image Comparison Reference

Quick decision guide for choosing the right container base image — balancing security, compatibility, size, and debuggability.


Quick Decision Matrix

Runtime / Need Best Choice Fallback
Go / Rust — fully static binary scratch gcr.io/distroless/static-debian12
Go / Rust — with CGO or dynamic libs gcr.io/distroless/base-debian12 alpine:3.20
Node.js app (production) gcr.io/distroless/nodejs20-debian12 node:20-slim
Python app (production) gcr.io/distroless/python3-debian12 python:3.12-slim
Java app (production) gcr.io/distroless/java21-debian12 eclipse-temurin:21-jre-alpine
Shell scripts required alpine:3.20 debian:12-slim
musl compatibility issue node:20-slim (glibc) debian:12-slim
Debugging in staging distroless :debug variant ubuntu:24.04 (temporary)

Size & CVE Comparison

Approximate values as of mid-2025. Run trivy image <name> for current counts.

Image Compressed Size Typical CVE Count Shell Package Manager libc
scratch 0 MB 0 No No None
gcr.io/distroless/static-debian12 ~2 MB 02 No No None
gcr.io/distroless/base-debian12 ~20 MB 03 No No glibc
gcr.io/distroless/nodejs20-debian12 ~55 MB 05 No No glibc
gcr.io/distroless/python3-debian12 ~50 MB 05 No No glibc
gcr.io/distroless/java21-debian12 ~220 MB 05 No No glibc
alpine:3.20 ~3.5 MB 05 Yes (ash) Yes (apk) musl
node:20-alpine ~65 MB 520 Yes Yes musl
python:3.12-alpine ~55 MB 520 Yes Yes musl
node:20-slim ~90 MB 1540 Yes Yes (minimal apt) glibc
python:3.12-slim ~60 MB 1540 Yes Yes (minimal apt) glibc
eclipse-temurin:21-jre-alpine ~180 MB 520 Yes Yes musl
node:20 (full) ~370 MB 80200 Yes Yes (full apt) glibc
ubuntu:24.04 ~30 MB 2060 Yes Yes (full apt) glibc
ubuntu:24.04 (full packages) ~200 MB+ 50150 Yes Yes glibc

Detailed Trade-offs

scratch

Best for: Go, Rust, or any fully static binary with CGO_ENABLED=0

  • Zero attack surface — literally empty
  • Smallest possible image
  • No package manager to exploit
  • No libc, no shell, no CA certs, no timezone data — must COPY them in
  • Cannot exec into for debugging (no shell at all)
FROM golang:1.22-alpine AS builder
WORKDIR /build
COPY go.* ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build \
    -ldflags="-s -w -extldflags=-static" \
    -o app .

FROM scratch
# Copy CA certs for HTTPS calls
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
# Copy timezone data if needed
COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo
COPY --from=builder /build/app /app
USER 65532:65532
ENTRYPOINT ["/app"]

gcr.io/distroless (Google)

Best for: Production Node.js, Python, Java, Go (with CGO)

  • No shell, no package manager — dramatically reduced attack surface
  • Includes CA certs and tzdata by default
  • Built-in nonroot user (UID 65532)
  • Based on Debian — glibc compatibility (no musl issues)
  • Regularly patched by Google
  • Cannot exec into with docker exec -it (no shell) — use :debug variant for staging
# Available distroless variants
gcr.io/distroless/static-debian12       # No libc — for fully static binaries
gcr.io/distroless/base-debian12         # glibc + openssl — for dynamic Go/Rust
gcr.io/distroless/nodejs20-debian12     # Node.js 20 runtime
gcr.io/distroless/nodejs22-debian12     # Node.js 22 runtime
gcr.io/distroless/python3-debian12      # Python 3 runtime
gcr.io/distroless/java21-debian12       # JRE 21
gcr.io/distroless/cc-debian12           # C/C++ runtime

# Debug variants — include busybox shell for staging only
gcr.io/distroless/nodejs20-debian12:debug
gcr.io/distroless/python3-debian12:debug

Debugging a distroless container (staging only):

# Use a sidecar debug container instead of modifying the production image
kubectl debug -it deploy/myapp \
  --image=busybox \
  --target=app \
  --copy-to=debug-pod

alpine

Best for: Images where a shell is required, or when image size is a primary concern

  • Very small (~3.5 MB)
  • Has shell (ash) and package manager (apk) — great for debugging
  • Regularly patched, active community
  • ⚠️ Uses musl libc — some Python C extensions, Node.js native modules, or glibc-dependent binaries may fail
  • More CVEs than distroless (more packages)

musl compatibility check:

# Test your app on alpine before committing
docker run -it --rm -v $(pwd):/app node:20-alpine sh -c "cd /app && npm ci && npm test"

Common musl issues:

  • bcrypt, node-gyp, sharp, canvas native modules → may need build tools
  • Python with numpy, scipy, pandas → use python:3.12-slim instead
  • Java apps → generally fine, but test thoroughly

slim variants (Debian-based)

Best for: Apps with glibc dependencies that can't use distroless

  • glibc compatibility — no musl issues
  • Familiar apt ecosystem
  • Smaller than full image (~6090 MB vs 300400 MB)
  • More CVEs than distroless (has apt, shell, more system libraries)
  • Larger than alpine
FROM node:20-slim
# Install only what's needed and clean up in the same layer
RUN apt-get update && \
    apt-get install -y --no-install-recommends \
      libssl3 \
    && rm -rf /var/lib/apt/lists/*

Full Images (node:20, ubuntu:24.04, python:3.12)

Only for: Development, CI build stages, or debugging — NEVER as production runtime

  • Massive attack surface (50200+ CVEs)
  • Includes compilers, build tools, package managers — not needed at runtime
  • Huge size increases pull time and storage costs

Use as a build stage only:

FROM node:20 AS builder     # Full image for building
FROM node:20-slim AS runtime  # Slim image for production

Keeping Base Images Updated

The most common source of container CVEs is outdated base images.

Manual Check

# Pull latest and check digest
docker pull node:20-slim
docker inspect node:20-slim --format='{{index .RepoDigests 0}}'

# Check for CVEs in current base before updating
trivy image node:20-slim --severity HIGH,CRITICAL
// .renovaterc.json
{
  "extends": ["config:base"],
  "dockerfile": {
    "enabled": true,
    "pinDigests": true
  },
  "packageRules": [
    {
      "matchDatasources": ["docker"],
      "matchPackagePatterns": ["^gcr.io/distroless"],
      "automerge": true,
      "automergeType": "branch"
    }
  ]
}

Automate with Dependabot

# .github/dependabot.yml
version: 2
updates:
  - package-ecosystem: "docker"
    directory: "/"
    schedule:
      interval: "weekly"
    open-pull-requests-limit: 5

Distroless Digest Pinning Reference

Always pin to digest. Check current digests at:

  • gcr.io/distroless/nodejs20-debian12docker pull gcr.io/distroless/nodejs20-debian12 && docker inspect gcr.io/distroless/nodejs20-debian12 --format='{{index .RepoDigests 0}}'
  • Use Google's distroless tags page for latest releases

Image Size Reduction Checklist

When an image is too large:

  • Switched to distroless or alpine runtime stage?
  • Multi-stage build separating build from runtime?
  • npm ci --only=production / pip install --no-dev?
  • Build cache cleaned in same RUN layer (rm -rf /var/lib/apt/lists/*, npm cache clean --force)?
  • .dockerignore excludes node_modules, .git, tests/, docs/?
  • Using --mount=type=cache for package manager cache (BuildKit)?
  • Only necessary files COPY-ed into runtime stage?
  • No debug tools in production image?
# Analyze image layers to find what's taking space
docker history --no-trunc myapp:latest
dive myapp:latest    # Interactive layer explorer: https://github.com/wagoodman/dive