TL;DR — Quick Fix
Scan your images and see the immediate impact of switching base images:
# Scan current image for vulnerabilities
trivy image --severity HIGH,CRITICAL your-app:latest
# Compare CVE counts across base images
echo "=== Ubuntu ===" && trivy image --quiet ubuntu:22.04 | tail -3
echo "=== Distroless ===" && trivy image --quiet gcr.io/distroless/base-debian12 | tail -3
echo "=== Chainguard ===" && trivy image --quiet cgr.dev/chainguard/static:latest | tail -3
# Rebuild with multi-stage + distroless
docker build -t your-app:secure -f Dockerfile.distroless .
trivy image your-app:secure
---
Why Scanners Report Hundreds of CVEs
Most vulnerabilities come from the OS layer, not your application code.
---
Multi-Stage Builds for Minimal Attack Surface
Go Application
# Dockerfile.distroless — Go app
FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /app/server ./cmd/server
FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=builder /app/server /server
USER nonroot:nonroot
EXPOSE 8080
ENTRYPOINT ["/server"]
Node.js Application
# Dockerfile.distroless — Node.js app
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production && npm cache clean --force
COPY . .
RUN npm run build
FROM gcr.io/distroless/nodejs20-debian12:nonroot
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./
USER nonroot:nonroot
EXPOSE 3000
CMD ["dist/index.js"]
Python Application
# Dockerfile.chainguard — Python app
FROM python:3.12-slim AS builder
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir --target=/deps -r requirements.txt
COPY . .
FROM cgr.dev/chainguard/python:latest-dev
WORKDIR /app
COPY --from=builder /deps /deps
COPY --from=builder /app .
ENV PYTHONPATH=/deps
USER nonroot
EXPOSE 8000
ENTRYPOINT ["python", "-m", "uvicorn", "main:app", "--host", "0.0.0.0"]
---
CI Gate Policies
# .github/workflows/image-scan.yml
name: Container Security Scan
on:
pull_request:
paths: ['/Dockerfile', '/requirements.txt', '*/package-lock.json']
jobs:
scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build image
run: docker build -t scan-target:${{ github.sha }} .
- name: Run Trivy scan
uses: aquasecurity/trivy-action@master
with:
image-ref: scan-target:${{ github.sha }}
format: 'sarif'
output: 'trivy-results.sarif'
severity: 'CRITICAL,HIGH'
exit-code: '1'
ignore-unfixed: true
- name: Upload SARIF
if: always()
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: trivy-results.sarif
# .trivyignore — Suppress known false positives
# CVE-2023-44487: Not exploitable (no network exposure in this context)
# CVE-2024-21626: Fixed in next upstream release, risk accepted
---
Comparing Scanner Tools
# Trivy — fast, broad coverage
trivy image --format json --output report.json your-app:latest
# Grype — good SBOM integration
grype your-app:latest --output json > grype-report.json
# Compare results
echo "Trivy:" && trivy image --quiet your-app:latest 2>/dev/null | grep -c "HIGH\|CRITICAL"
echo "Grype:" && grype your-app:latest 2>/dev/null | grep -c "High\|Critical"
---
Migration Strategy
#!/bin/bash
# scripts/migrate-base-images.sh — Audit all services
SERVICES=$(find . -name "Dockerfile" -not -path "/node_modules/")
for dockerfile in $SERVICES; do
SERVICE_DIR=$(dirname "$dockerfile")
SERVICE_NAME=$(basename "$SERVICE_DIR")
BASE=$(grep "^FROM" "$dockerfile" | tail -1 | awk '{print $2}')
IMAGE_TAG="${SERVICE_NAME}:scan-test"
docker build -t "$IMAGE_TAG" "$SERVICE_DIR" 2>/dev/null
VULN_COUNT=$(trivy image --quiet --severity HIGH,CRITICAL "$IMAGE_TAG" 2>/dev/null | wc -l)
echo "$SERVICE_NAME | Base: $BASE | Vulns: $VULN_COUNT"
done | sort -t'|' -k3 -rn
---
FAQ
Q: My app needs a shell for debugging. Can I still use distroless?
A: Use distroless debug variants in non-production. For production, use ephemeral containers: kubectl debug -it pod/my-pod --image=busybox.
Q: How do I handle CVEs in transitive dependencies I can't upgrade?
A: Check if the CVE is exploitable in your context. Document risk decisions in .trivyignore. Use --ignore-unfixed for unpatched CVEs. Set reminders to revisit.
Q: Won't minimal images break my CI/CD?
A: Use multi-stage builds — install everything in a builder stage, copy only runtime artifacts to the final stage. Test in staging first.
Q: How often should I rebuild base images?
A: At least weekly to pick up patches. Automate with Renovate Bot or Dependabot tracking base image digests.
Q: What about FIPS compliance?
A: Chainguard offers FIPS-validated images. For Distroless, compile with FIPS-compliant crypto libraries. Common in regulated industries.
---