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
| Feature | Details |
|---|---|
| CVEs | Near-zero through continuous event-driven patching |
| SBOM | Signed Software Bill of Materials included |
| Provenance | SLSA Level 3 build integrity verification |
| License | Apache 2.0 (Community plan is free) |
| Distributions | Alpine 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:
- No package installation — Neither the SDK nor Runtime image supports
apt-get - Missing ASP.NET Core runtime — The DHI .NET runtime image may include only the .NET runtime, not the ASP.NET Core shared framework
- Missing native libraries — Packages that depend on OS libraries (e.g., image processing) will fail at startup
- 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.dllsymlink is required because .NET's P/Invoke searches for a file namedgdiplus.dll - All dependent
.sofiles must be explicitly copied (uselddto 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/npmand/usr/bin/npxare 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 restoreto optimize layer caching — restore only re-runs when dependencies change - Pass
--no-restoretodotnet publishto 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
FROMtodhi.io/dotnet:8.0-sdk-debian13(build) anddhi.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
lddto 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.
| Distribution | Status |
|---|---|
| Alpine | Available |
| Debian | Available |
| 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.
| Feature | Details |
|---|---|
| Runtime | Runs in your own Kubernetes cluster |
| Output | Images with the same notarization and hardening as Docker-built DHI |
| Source | Pulls from your private registry |
This lets you achieve DHI-equivalent security guarantees without sending sensitive customizations off-premises.
Docker Ecosystem Integration
| Product | DHI Integration |
|---|---|
| Docker Desktop | Kubernetes runtime uses DHI images |
| Docker Sandbox (SBX) | MCP servers and sandbox template go through DHI hardening |
| AI Agents | Policy setting available to allow packages only from the DHI package manager |
Certifications and Regulatory Compliance
| Certification / Regulation | Status |
|---|---|
| FIPS | Certified |
| STIG | Certified |
| EU CRA (Cyber Resilience Act) and others | Under 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.