246 lines
8.2 KiB
Markdown
246 lines
8.2 KiB
Markdown
# 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 | 0–2 | No | No | None |
|
||
| `gcr.io/distroless/base-debian12` | ~20 MB | 0–3 | No | No | glibc |
|
||
| `gcr.io/distroless/nodejs20-debian12` | ~55 MB | 0–5 | No | No | glibc |
|
||
| `gcr.io/distroless/python3-debian12` | ~50 MB | 0–5 | No | No | glibc |
|
||
| `gcr.io/distroless/java21-debian12` | ~220 MB | 0–5 | No | No | glibc |
|
||
| `alpine:3.20` | ~3.5 MB | 0–5 | Yes (ash) | Yes (apk) | musl |
|
||
| `node:20-alpine` | ~65 MB | 5–20 | Yes | Yes | musl |
|
||
| `python:3.12-alpine` | ~55 MB | 5–20 | Yes | Yes | musl |
|
||
| `node:20-slim` | ~90 MB | 15–40 | Yes | Yes (minimal apt) | glibc |
|
||
| `python:3.12-slim` | ~60 MB | 15–40 | Yes | Yes (minimal apt) | glibc |
|
||
| `eclipse-temurin:21-jre-alpine` | ~180 MB | 5–20 | Yes | Yes | musl |
|
||
| `node:20` (full) | ~370 MB | 80–200 | Yes | Yes (full apt) | glibc |
|
||
| `ubuntu:24.04` | ~30 MB | 20–60 | Yes | Yes (full apt) | glibc |
|
||
| `ubuntu:24.04` (full packages) | ~200 MB+ | 50–150 | 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)
|
||
|
||
```dockerfile
|
||
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
|
||
|
||
```bash
|
||
# 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):**
|
||
```bash
|
||
# 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:**
|
||
```bash
|
||
# 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 (~60–90 MB vs 300–400 MB)
|
||
- ❌ More CVEs than distroless (has apt, shell, more system libraries)
|
||
- ❌ Larger than alpine
|
||
|
||
```dockerfile
|
||
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 (50–200+ 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:
|
||
```dockerfile
|
||
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
|
||
```bash
|
||
# 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
|
||
```
|
||
|
||
### Automate with Renovate (Recommended)
|
||
```json
|
||
// .renovaterc.json
|
||
{
|
||
"extends": ["config:base"],
|
||
"dockerfile": {
|
||
"enabled": true,
|
||
"pinDigests": true
|
||
},
|
||
"packageRules": [
|
||
{
|
||
"matchDatasources": ["docker"],
|
||
"matchPackagePatterns": ["^gcr.io/distroless"],
|
||
"automerge": true,
|
||
"automergeType": "branch"
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
### Automate with Dependabot
|
||
```yaml
|
||
# .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-debian12` → `docker pull gcr.io/distroless/nodejs20-debian12 && docker inspect gcr.io/distroless/nodejs20-debian12 --format='{{index .RepoDigests 0}}'`
|
||
- Use [Google's distroless tags page](https://github.com/GoogleContainerTools/distroless/blob/main/README.md) 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?
|
||
|
||
```bash
|
||
# 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
|
||
```
|