Skip to content

Migrate to DHI

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:

###########################################################
# Stage: base (DHI dev variant — has shell + npm for builds)
###########################################################
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

Property node:25-slim DHI runtime
CVEs (critical/high) 0C 2H 0C 0H
Package count ~272 ~12
Shell in runtime Yes (sh) No (distroless)
Non-root by default Manual Built-in
SBOM Build-time only Cryptographically signed
VEX document No Yes
SLSA provenance Build-time only Verified
FIPS variant No Yes (-fips tag)
STIG variant No Yes
7-day CVE SLA No Yes

Continue to Attestations & Scanner Integration.