• Technology
  • Docker Security Best Practices: Choosing and Hardening Your Base Image

    Base image selection is the single most consequential security decision in a Dockerfile. Everything else — the packages you install, the code you copy, the configuration you apply — builds on top of the foundation the base image provides. If that foundation carries 150 CVEs from packages your application will never use, every image you build inherits those 150 CVEs before your first line of application-specific code.

    Most Dockerfiles start with an official image chosen for convenience: python:3.12, node:20, openjdk:21. These are excellent starting points for getting an application running. They are not designed for production security — they are designed for maximum compatibility, which means they include tools and utilities that a production application will never need.


    The Default Image CVE Reality

    Running a container scanner against common base images reveals the scope of the problem:

    • ubuntu:22.04 — 50-80 CVEs in a fresh pull, with that number growing as CVEs are disclosed against installed packages
    • python:3.12 (built on Debian) — 80-120 CVEs, including OS-level packages and Python ecosystem packages
    • node:20 — similar to Python, plus Node-specific packages
    • openjdk:21 — Java runtime plus OS layer, 100+ CVEs in an unminimized form

    The CVE count is not primarily from bad upstream maintenance — most of these distributions are actively maintained. The CVE count is from package volume: general-purpose base images carry hundreds of packages so that any application can use them. CVEs accumulate as individual packages have vulnerabilities discovered against them.


    Slim and Alpine Variants: An Improvement, Not a Solution

    Most base image maintainers offer slim or Alpine variants:

    • python:3.12-slim: Removes some utilities, reduces image size significantly, reduces CVE count
    • python:3.12-alpine: Uses Alpine Linux’s musl libc instead of glibc, dramatically smaller

    These variants improve the starting position but do not eliminate the problem:

    Slim images still carry OS-level packages and utilities that a specific application may not use. They have removed the most obvious unused packages, but the result is still a general-purpose base, not an application-specific one.

    Alpine images have compatibility tradeoffs: musl libc behaves differently from glibc in ways that affect some applications, particularly those with native code dependencies. Not all packages compile cleanly against musl. Migration from Debian-based to Alpine-based images sometimes surfaces subtle runtime compatibility issues.

    Both slim and Alpine variants reduce CVE counts but do not achieve the near-zero CVE state that is the goal for a production security baseline.


    Multi-Stage Builds as a Security Pattern

    Multi-stage Dockerfile builds are a best practice that separates build-time dependencies from runtime dependencies:

    # Build stage: includes compiler, build tools, development headers

    FROM python:3.12 AS builder

    WORKDIR /app

    COPY requirements.txt .

    RUN pip install –target=/app/deps -r requirements.txt

    # Runtime stage: only what is needed to run the application

    FROM python:3.12-slim AS runtime

    WORKDIR /app

    COPY –from=builder /app/deps /app/deps

    COPY src/ /app/src/

    ENV PYTHONPATH=/app/deps

    CMD [“python”, “-m”, “src.main”]

    Multi-stage builds ensure build tools (compilers, package managers’ build dependencies, development headers) are not included in the production image. This is significant: build tools are a common source of CVEs in production images and serve no runtime purpose.

    Multi-stage builds address the build-vs-runtime distinction but not the within-runtime-layer distinction: the runtime layer still carries all packages that could be used by any application, not just the ones your specific application calls.


    Runtime Profiling for Precise Hardening

    The precise solution to base image CVE excess is runtime profiling: observing which packages are actually loaded during application execution and removing those that are not.

    Unlike static analysis approaches that attempt to trace dependencies at build time, runtime profiling captures observed behavior:

    1. Build the application image using standard base
    2. Execute representative application workflows (startup, request handling, background jobs, shutdown)
    3. Record which system libraries were loaded, which binaries were invoked, which packages contributed executable code
    4. The packages with no execution evidence are candidates for removal

    The resulting hardened container images carry only the packages that the application demonstrably uses. CVEs in packages that were present in the base image but never loaded during application execution are eliminated — not suppressed, not deferred, eliminated — because the packages themselves are no longer present.


    Pre-Hardened Base Images as a Starting Point

    A practical alternative to the full profiling-and-removal workflow is starting from pre-hardened base images. These are curated versions of popular base images — the same Python 3.12, Node 20, or OpenJDK 21 runtimes — that have had unused packages removed in advance.

    Pre-hardened base images are drop-in replacements for standard base images in most Dockerfiles. The FROM line changes; the rest of the Dockerfile stays the same. The result is that every application image built from the pre-hardened base starts with a fraction of the base-layer CVEs that a standard official image would provide.

    Container hardening applied to the full image after it is built — including the application layer on top of the hardened base — achieves the comprehensive result: hardened base plus hardened application layer. The CVE count reflects only what the specific application actually uses across all layers.



    Frequently Asked Questions

    What is the most secure Docker base image to use?

    The most secure Docker base image is one that has been hardened to contain only the packages your specific application actually uses at runtime. Pre-hardened base images — curated versions of standard images like python:3.12 or node:20 with unused packages removed — offer a near-zero CVE starting point while maintaining compatibility. Custom-hardened images built from runtime profiling achieve the lowest CVE counts by eliminating packages that are never loaded during application execution.

    How does base image selection affect Docker security best practices?

    Base image selection is the foundational Docker security best practices decision because every CVE in the base image is inherited by every application image built on top of it. Standard official images carry 80-150+ CVEs from packages included for general compatibility but not needed by most applications. Choosing a pre-hardened or minimal base image eliminates this inherited CVE burden before any application-specific code is added.

    What is the difference between Alpine, slim, and hardened Docker base images?

    Alpine images use musl libc instead of glibc, which reduces package count but introduces compatibility tradeoffs for applications with native code dependencies. Slim variants remove obvious development utilities but remain general-purpose, still carrying 50-80 CVEs. Hardened base images go further by removing all packages that have no execution evidence from runtime profiling, achieving 5-15 CVEs — the lowest of any approach — while maintaining the same runtime compatibility as the original image.

    Do multi-stage Docker builds eliminate the need for base image hardening?

    Multi-stage builds are an important Docker security best practices pattern that removes build-time dependencies — compilers, test tools, development headers — from the final image. However, they do not remove runtime packages in the final stage that are present but never loaded by the specific application. Runtime profiling and package removal address the within-runtime-layer problem that multi-stage builds leave unresolved, typically reducing CVE counts from 45 to as few as 8 in the final image.


    Evaluating Base Image Options

    Base Image TypeCVE CountCompatibilityMaintenance
    Official full imageHigh (100+)MaximumCommunity
    Official slim variantMedium (50-80)GoodCommunity
    Alpine variantLower (20-40)Moderate (musl tradeoffs)Community
    Pre-hardened baseVery low (5-15)Good (same runtime)Automated
    Custom-hardened imageNear-zeroSame as originalPer-image

    The investment in base image selection pays ongoing dividends: every application image built from a lower-CVE base requires less remediation work and generates less scanner noise throughout its lifecycle.

    6 mins