Skip to main content

Stage: base (DHI dev variant - has shell + npm for builds)

Migrate to Docker Hardened Imagesโ€‹

The pro-active approach: Start Secure. DHI images are purpose-built from the ground up to be extremely minimal - not stripped-down versions of something bloated.

In this section we'll convert catalog-service-node from node:25-slim (where the best-practices track ended) to a Docker Hardened Image, then compare the results.

Before you startโ€‹

Make sure you have:

  • Completed the Setup steps
  • Decided on free vs paid DHI tier (and know your <DHI_PREFIX>)
  • Logged in to dhi.io if using the free tier (docker login dhi.io)

Update the Dockerfileโ€‹

Open catalog-service-node/Dockerfile and replace it with this DHI-based version:

###########################################################

###########################################################
FROM <DHI_PREFIX>node:24-debian13-dev AS base

WORKDIR /usr/local/app
COPY package.json package-lock.json ./

###########################################################
# Stage: dev
###########################################################
FROM base AS dev
ENV NODE_ENV=development
RUN npm install
CMD ["yarn", "dev-container"]

###########################################################
# Stage: production-dependencies
###########################################################
FROM base AS production-dependencies
ENV NODE_ENV=production
RUN npm ci --production --ignore-scripts && npm cache clean --force

###########################################################
# Stage: final (DHI runtime - distroless, no shell)
###########################################################
FROM <DHI_PREFIX>node:24-debian13 AS final
ENV NODE_ENV=production
COPY --from=production-dependencies /usr/local/app/node_modules ./node_modules
COPY ./src ./src
EXPOSE 3000
CMD ["node", "src/index.js"]

The two critical changes:

- FROM node:25-slim AS base
+ FROM <DHI_PREFIX>node:24-debian13-dev AS base # build stage
+ FROM <DHI_PREFIX>node:24-debian13 AS final # runtime - distroless

Notice the structure: the dev variant is used as the build environment (it has npm), but the final image is built FROM the distroless runtime variant. Only the compiled node_modules and src are copied in.

Build the DHI versionโ€‹

docker build -t catalog-service:dhi --sbom=true --provenance=mode=max .

Compare all three imagesโ€‹

docker images catalog-service
IMAGE ID DISK USAGE CONTENT SIZE
catalog-service:dhi ac3a0d465de4 191MB 40.3MB
catalog-service:latest 48806e62b871 1.62GB 413MB
catalog-service:slim 8d03cef7a79f 368MB 84.1MB

The DHI runtime is 10ร— smaller than the original and half the size of the slim version - and we still haven't looked at security.

Scout quickview - all 7 policies greenโ€‹

docker scout quickview catalog-service:dhi --org <YOUR_ORG>
Target โ”‚ catalog-service:dhi โ”‚ 0C 0H 0M 0L

Policy status SUCCEEDED (7/7 policies met)

Status โ”‚ Policy โ”‚ Results
โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
โœ“ โ”‚ Default non-root user โ”‚
โœ“ โ”‚ No AGPL v3 licenses โ”‚
โœ“ โ”‚ No fixable critical or high vulnerabilities โ”‚
โœ“ โ”‚ No high-profile vulnerabilities โ”‚
โœ“ โ”‚ No unapproved base images โ”‚
โœ“ โ”‚ Supply chain attestations โ”‚
โœ“ โ”‚ No outdated base images โ”‚

7/7 policies met. Up from 4/7 at the start of the workshop.

Full before/after comparisonโ€‹

docker scout compare \
--ignore-unchanged \
--to catalog-service \
catalog-service:dhi \
--org <YOUR_ORG>
vulnerabilities โ”‚ 0C 0H 0M 0L โ”‚ 2C 26H 25M 122L 4?
size โ”‚ 40 MB (-358 MB) โ”‚ 398 MB
packages โ”‚ 211 (-595) โ”‚ 806

The numbers:

  • โœ… 595 packages removed - 595 fewer potential CVE vectors
  • โœ… 179 vulnerabilities removed across all severities
  • โœ… Image is 10ร— smaller

The no-shell demoโ€‹

Because the DHI runtime is distroless, an attacker who gains code execution cannot drop to a shell:

docker run --rm catalog-service:dhi sh

Expected: error - sh does not exist in the image.

Compare to slim:

docker run --rm catalog-service:slim sh -c "echo 'shell available - attack surface'"

The slim image still gives the attacker a shell. The DHI image does not. That's a meaningful difference in what an exploit can do once it lands.

DHI vs slim - property comparisonโ€‹

Propertynode:25-slimDHI runtime
CVEs (critical/high)0C 2H0C 0H
Package count~272~12
Shell in runtimeYes (sh)No (distroless)
Non-root by defaultManualBuilt-in
SBOMBuild-time onlyCryptographically signed
VEX documentNoYes
SLSA provenanceBuild-time onlyVerified
FIPS variantNoYes (-fips tag)
STIG variantNoYes
7-day CVE SLANoYes

Continue to Attestations & Scanner Integration.