Skip to main content

Docker Hardened Images with .NET

What Are Docker Hardened Images?

Docker Hardened Images (DHI) are security-hardened container images provided by Docker. Compared to standard official base images, they are stripped down to a minimal footprint by removing unnecessary packages and tools.

Key Features

FeatureDetails
CVEsNear-zero through continuous event-driven patching
SBOMSigned Software Bill of Materials included
ProvenanceSLSA Level 3 build integrity verification
LicenseApache 2.0 (Community plan is free)
DistributionsAlpine and Debian both supported

Key Differences from Standard Official Images

DHI follows a "minimal by design" philosophy, intentionally excluding:

  • Package managers (apt-get, apk)
  • Shells (bash, sh)
  • Network tools (curl, wget)
  • Debugging utilities

This dramatically reduces the attack surface, but any tools your build requires must be added via multi-stage builds rather than runtime installation.


Challenges and Solutions for .NET Projects

Overview of Challenges

Migrating a .NET application to DHI typically surfaces these issues:

  1. No package installation — Neither the SDK nor Runtime image supports apt-get
  2. Missing ASP.NET Core runtime — The DHI .NET runtime image may include only the .NET runtime, not the ASP.NET Core shared framework
  3. Missing native libraries — Packages that depend on OS libraries (e.g., image processing) will fail at startup
  4. Embedding additional runtimes — If a frontend build requires Node.js, it cannot be added to the SDK image directly

All of these can be resolved with multi-stage builds.


Multi-Stage Build Patterns

Stage Overview

Complete Dockerfile Example

The following is an example for a .NET 8 ASP.NET Core Web API that includes a Vue.js frontend.

# syntax=docker/dockerfile:1

# ────────────────────────────────────────────────────────────────
# Stage 1: Native library builder
# DHI runtime has no apt-get, so build native libs in a standard image
# ────────────────────────────────────────────────────────────────
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS gdiplus-builder
RUN apt-get update \
&& apt-get install -y --no-install-recommends libgdiplus libc6-dev \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
RUN cd /usr/lib && ln -s libgdiplus.so gdiplus.dll

# ────────────────────────────────────────────────────────────────
# Stage 2: Obtain Node.js binaries from DHI Node image
# ────────────────────────────────────────────────────────────────
FROM dhi.io/node:22-debian13-dev AS node-source

# ────────────────────────────────────────────────────────────────
# Stage 3: Build
# DHI SDK has no apt-get, so copy Node.js from Stage 2
# ────────────────────────────────────────────────────────────────
FROM dhi.io/dotnet:8.0-sdk-debian13 AS build

# npm/npx are symlinks, so copy the full library tree and recreate symlinks
COPY --from=node-source /usr/bin/node /usr/bin/node
COPY --from=node-source /usr/lib/nodejs /usr/lib/nodejs
RUN ln -s /usr/lib/nodejs/npm/bin/npm-cli.js /usr/bin/npm && \
ln -s /usr/lib/nodejs/npm/bin/npx-cli.js /usr/bin/npx

WORKDIR /src

# Copy project files first to cache the restore layer
COPY ["MyApp.Api/MyApp.Api.csproj", "MyApp.Api/"]
COPY ["MyApp.Services/MyApp.Services.csproj", "MyApp.Services/"]
COPY ["NuGet.config", "."]
RUN dotnet restore "MyApp.Api/MyApp.Api.csproj"

COPY . .
WORKDIR "/src/MyApp.Api"
RUN dotnet build "MyApp.Api.csproj" -c Release -o /app/build

# Frontend build
WORKDIR "/src/MyApp.Api/ClientApp"
RUN npm install
RUN npm run build

# ────────────────────────────────────────────────────────────────
# Stage 4: Publish
# ────────────────────────────────────────────────────────────────
WORKDIR "/src/MyApp.Api"
FROM build AS publish
RUN dotnet publish "MyApp.Api.csproj" --no-restore -c Release -o /app/publish

# ────────────────────────────────────────────────────────────────
# Stage 5: Final image (DHI Runtime)
# ────────────────────────────────────────────────────────────────
FROM dhi.io/dotnet:8.0-debian13 AS final

# ASP.NET Core shared framework (not included in DHI runtime)
COPY --from=build /opt/dotnet/shared/Microsoft.AspNetCore.App \
/opt/dotnet/shared/Microsoft.AspNetCore.App

# libgdiplus itself
COPY --from=gdiplus-builder /usr/lib/libgdiplus* /usr/lib/
COPY --from=gdiplus-builder /usr/lib/gdiplus.dll /usr/lib/gdiplus.dll

# Native libraries that libgdiplus depends on
COPY --from=gdiplus-builder /lib/x86_64-linux-gnu/libcairo.so.2* /lib/x86_64-linux-gnu/
COPY --from=gdiplus-builder /lib/x86_64-linux-gnu/libglib-2.0.so.0* /lib/x86_64-linux-gnu/
COPY --from=gdiplus-builder /lib/x86_64-linux-gnu/libjpeg.so.62* /lib/x86_64-linux-gnu/
COPY --from=gdiplus-builder /lib/x86_64-linux-gnu/libtiff.so.6* /lib/x86_64-linux-gnu/
COPY --from=gdiplus-builder /lib/x86_64-linux-gnu/libgif.so.7* /lib/x86_64-linux-gnu/
COPY --from=gdiplus-builder /lib/x86_64-linux-gnu/libpng16.so.16* /lib/x86_64-linux-gnu/
COPY --from=gdiplus-builder /lib/x86_64-linux-gnu/libX11.so.6* /lib/x86_64-linux-gnu/
COPY --from=gdiplus-builder /lib/x86_64-linux-gnu/libfontconfig.so.1* /lib/x86_64-linux-gnu/
COPY --from=gdiplus-builder /lib/x86_64-linux-gnu/libfreetype.so.6* /lib/x86_64-linux-gnu/
COPY --from=gdiplus-builder /etc/fonts /etc/fonts

WORKDIR /app
COPY --from=publish /app/publish .
EXPOSE 8080
ENTRYPOINT ["dotnet", "MyApp.Api.dll"]

Stage-by-Stage Breakdown

Stage 1: Native Library Builder

Why it's needed: Libraries like System.Drawing (or PDF generation libraries using libgdiplus) depend on native shared libraries. Since DHI runtime has no apt-get, you build the native libs in a standard image and extract the .so files.

Key points:

  • apt-get clean && rm -rf /var/lib/apt/lists/* minimizes the layer size
  • The gdiplus.dll symlink is required because .NET's P/Invoke searches for a file named gdiplus.dll
  • All dependent .so files must be explicitly copied (use ldd to discover them)

Stage 2: Node.js Source

Why it's needed: Since the DHI SDK image has no apt-get, Node.js cannot be installed conventionally. Instead, binaries are pulled directly from the corresponding DHI Node image.

Key points:

  • /usr/bin/npm and /usr/bin/npx are relative symlinks, so the entire library tree (/usr/lib/nodejs) must be copied
  • After copying, symlinks must be recreated in the build stage:
# npm resolves require('../lib/cli.js') using relative paths,
# so copy the full library tree and recreate the symlinks
RUN ln -s /usr/lib/nodejs/npm/bin/npm-cli.js /usr/bin/npm && \
ln -s /usr/lib/nodejs/npm/bin/npx-cli.js /usr/bin/npx

Stages 3 & 4: Build and Publish

These follow a standard multi-stage build pattern with a few considerations:

  • Copy only project files before dotnet restore to optimize layer caching — restore only re-runs when dependencies change
  • Pass --no-restore to dotnet publish to avoid redundant restore execution
  • Code analysis tools like SonarQube require a JDK and cannot run inside the DHI SDK image — move them to your CI pipeline as a separate step

Stage 5: Final Image (DHI Runtime)

Copying the ASP.NET Core Shared Framework:

The DHI .NET runtime image (dhi.io/dotnet:8.0-debian13) may contain only the .NET runtime, not the ASP.NET Core shared framework. To run an ASP.NET Core Web API, copy it from the SDK stage:

COPY --from=build /opt/dotnet/shared/Microsoft.AspNetCore.App \
/opt/dotnet/shared/Microsoft.AspNetCore.App

Discovering Dependency Libraries

To identify which .so files need to be copied to the final image, use ldd inside the builder stage:

# Run a temporary container to inspect dependencies
docker run --rm --entrypoint bash mcr.microsoft.com/dotnet/aspnet:8.0 \
-c "apt-get update -q && apt-get install -y libgdiplus && ldd /usr/lib/libgdiplus.so"

Sample output:

libgdiplus.so => /usr/lib/libgdiplus.so
libglib-2.0.so.0 => /lib/x86_64-linux-gnu/libglib-2.0.so.0
libcairo.so.2 => /lib/x86_64-linux-gnu/libcairo.so.2
libjpeg.so.62 => /lib/x86_64-linux-gnu/libjpeg.so.62
...

Use this output to enumerate all files in COPY --from=gdiplus-builder statements.


Migration Checklist

  • Update FROM to dhi.io/dotnet:8.0-sdk-debian13 (build) and dhi.io/dotnet:8.0-debian13 (runtime)
  • Identify all RUN apt-get install ... usages and replace with multi-stage build equivalents
  • Move any RUN curl ... or network-fetching steps to a dedicated builder stage
  • Verify whether the ASP.NET Core shared framework needs to be copied
  • Use ldd to audit all native library dependencies and copy every required .so
  • For npm/npx: copy from DHI Node image and recreate symlinks
  • Move JDK-dependent tools (e.g., SonarQube) out of the DHI build and into the CI pipeline
  • Confirm the image builds successfully with docker build
  • Verify application startup and native library functionality

DHI Roadmap

Expanding System Packages

DHI is evolving beyond images to provide system packages — individual OS-level packages that can be added to your images without breaking provenance.

DistributionStatus
AlpineAvailable
DebianAvailable
Fedora (DNF / RPM)Coming soon

RHEL is excluded due to Red Hat's licensing constraints, but compatible distributions like Rocky Linux and AlmaLinux are planned.

Hardened Libraries

Following system packages, Docker plans to release hardened libraries — starting with npm — to address upstream supply chain poisoning attacks (where malicious packages are published as if they were legitimate).

  • Cool-down periods: new package versions are held and monitored before adoption
  • AI diff management: version-to-version diffs are analyzed by AI to detect upstream poisoning early
  • Agents can be configured to source packages only from the DHI package manager, eliminating exposure to malicious packages via generic npm

Secure Build Services (On-Premises Hardening)

For organizations uncomfortable sending proprietary or confidential content to Docker's infrastructure, Secure Build Services will be available as a product.

FeatureDetails
RuntimeRuns in your own Kubernetes cluster
OutputImages with the same notarization and hardening as Docker-built DHI
SourcePulls from your private registry

This lets you achieve DHI-equivalent security guarantees without sending sensitive customizations off-premises.

Docker Ecosystem Integration

ProductDHI Integration
Docker DesktopKubernetes runtime uses DHI images
Docker Sandbox (SBX)MCP servers and sandbox template go through DHI hardening
AI AgentsPolicy setting available to allow packages only from the DHI package manager

Certifications and Regulatory Compliance

Certification / RegulationStatus
FIPSCertified
STIGCertified
EU CRA (Cyber Resilience Act) and othersUnder review

DHI already holds FIPS and STIG certifications. Some EU regulations accept STIG certification as satisfying their requirements, which means no additional work may be necessary. For Japanese multinationals serving the EU and other regions, achieving the highest common baseline (FIPS/STIG) once is more efficient than maintaining separate compliance pipelines per region.


References