If you run containers on your own servers, you have probably assumed that “hardened,” minimal, low-CVE images were a thing you pay an enterprise vendor for. I assumed the same for years. That assumption is now wrong. Docker Hardened Images are free, the whole catalog is open source under Apache 2.0, and you can pull them onto any box today. This post covers what they are, the one small hoop you jump through to pull them, and how to combine them with a rootless runtime so the image and the thing running it are both locked down, at zero cost.
What Docker Hardened Images actually are
A Docker Hardened Image (DHI) is a container image Docker builds and maintains to a security standard most of us never hit on our own. The pitch, in Docker’s own words, is “near-zero CVE, secure-by-default, minimal container images.” A few things make them different from the python:3.13 or nginx:latest you probably pull today:
- A near-zero known-CVE baseline. Docker rebuilds them continuously from source to keep known vulnerabilities close to nothing. One Docker customer story credits DHI with cutting 90% of their CVEs.
- Minimal, distroless runtime. The docs call them “minimal images, sometimes called distroless images,” stripped of package managers, shells, and OS tools. Less in the image means less to attack and less to patch.
- Non-root by default. Runtime images run as an unprivileged user, UID 65532, unless you configure otherwise.
- Supply-chain proof baked in. Every image ships a signed SBOM, SLSA Build Level 3 provenance, a cryptographic signature, and OpenVEX exploitability data. Docker says that applies to “every image across all tiers, including the free Community tier.”
That last point is the one that changes the math for self-hosters. A signed bill of materials and Level 3 provenance is the kind of thing compliance teams used to buy. Now it comes attached to a free python image.
Are Docker Hardened Images really free?
Short answer: yes, the catalog is free. Docker’s FAQ says it plainly: “the full Docker Hardened Images catalog is free and open source under the Apache 2.0 license… at no cost, with no usage restrictions or paywalled catalog access.” The free tier is called Community, and it spans a catalog Docker advertises as 1000+ images covering runtimes, frameworks, databases, and infrastructure. The usual suspects are all there: nginx, node, python, postgres, redis, and plenty more.
So where does the money come in? Two paid tiers, and neither is aimed at a typical self-hoster:
- Select starts at $5k per repository. It adds FIPS-validated and STIG-aligned variants, faster SLA-backed CVE fixes, and a handful of customizations.
- Enterprise is contact-for-pricing and adds unlimited customization plus access to hardened system packages.
If you need FedRAMP or FIPS paperwork, you know who you are, and you’ll want Select. For everyone else running a homelab, an agency stack, or a fleet of client boxes, the free Community tier is the whole point. Same SBOM, same SLSA Level 3 provenance, same signature, same near-zero-CVE base.
The one bit of friction: a Docker account and a token
Here’s the only speed bump. You can’t pull a Community DHI image anonymously. You authenticate against Docker’s hardened registry, dhi.io, first. You can log in with your Docker ID and password, but the clean way, and the way you’ll need for CI or any box with two-factor auth, is a personal access token (PAT).
Creating one takes about a minute:
- Sign in at app.docker.com.
- Click your avatar, top right, then Account settings.
- Open Personal access tokens.
- Click Generate new token.
- Give it a description, an expiration date, and the access level you need (for pulling images, Read is enough).
- Click Generate, then copy the token. You only see it once.
Now log in to the hardened registry with it. When it asks for a password, paste the token, not your account password:
docker login dhi.io --username YOUR_DOCKER_ID
# Password: <paste your personal access token here>
That’s it. From here, a hardened image pulls like any other:
docker pull dhi.io/python:3.13
docker run --rm dhi.io/python:3.13 python -c "print('Hello from a hardened image')"
Swapping a stock image for its hardened equivalent
The catalog mirrors familiar names, so migration is mostly a FROM line change plus respecting the fact that the runtime has no shell. Because there’s no package manager at runtime, you build in a -dev variant and copy the result into a minimal runtime. That’s a standard multi-stage build, and if you already write those, you’re 90% done.
Here’s a typical stock Python Dockerfile:
# Before: stock image, runs as root, full OS underneath
FROM python:3.13
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["python", "app.py"]
And the hardened version. Build in the -dev image, run in the minimal one:
# After: build in -dev (has shell + pip), ship in the distroless runtime
FROM dhi.io/python:3.13-dev AS build
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir --target=/app/deps -r requirements.txt
FROM dhi.io/python:3.13
WORKDIR /app
COPY --from=build /app/deps /app/deps
COPY . .
ENV PYTHONPATH=/app/deps
# Runtime image already runs as non-root UID 65532
CMD ["python", "app.py"]
The same idea works in Compose. You don’t harden the Compose file itself, you point it at the hardened image you built or at a runtime image directly:
services:
web:
image: dhi.io/nginx:1.27
ports:
- "8080:8080" # non-root can't bind 80, so serve on 8080+
restart: unless-stopped
The gotchas nobody warns you about
Swapping the image is easy. The surprises come from everything a minimal, non-root image doesn’t have. These are the ones I’d flag before you push to production:
- No shell, no package manager at runtime. The distroless runtime deliberately drops
sh,bash,apt,apk,curl, and friends. AnyRUNstep, any shell-form entrypoint, anyapt-get installhas to move into a-devbuild stage. Docker’s migration checklist is blunt about it: “Runtime images don’t include a shell, so copy all necessary artifacts from the build stage.” - Non-root means file permissions matter. Everything your app reads or writes has to be accessible to UID 65532. A volume owned by root, or a config file with tight permissions, will fail with a permission error that looks nothing like the real cause.
- Ports below 1024 are off-limits. A non-root process can’t bind privileged ports on Docker Engine older than 20.10 or on Kubernetes. Make your service listen on 1025 or higher. This is why the Compose example above maps 8080, not 80.
- Certificates are already there. DHI runtime images include
ca-certificatesby default, so drop any step that installs them. One less layer. - Tags carry the distro and the variant. You’ll see tags like
dhi.io/node:22-debian13,dhi.io/node:24-dev, anddhi.io/python:3.13-alpine3.23. Both Alpine (musl) and Debian (glibc) bases exist, so you can match whatever your dependencies already expect. Pick-devfor build stages, the plain tag for runtime. - Debugging without a shell. When something misbehaves and you can’t
docker execinto a shell, use the-devvariant, or attach Docker Debug, which drops a temporary toolbox into the running container without altering the image.
None of these is a dealbreaker. They’re the same disciplines a good multi-stage build already encourages. The image just stops letting you cut corners.
Hardened image plus hardened runtime: the free combo
Here’s where it gets good for self-hosters. A hardened image protects what’s inside the container. It does nothing about the daemon running it. And on most servers I inspect, that daemon is the weak link: classic Docker runs as root, and anyone in the docker group is root-equivalent because they can mount the host filesystem into a container they control.
So pair the two. Run your near-zero-CVE, non-root DHI image on top of a rootless Docker daemon. Now a container escape lands as an unprivileged user inside a user namespace, not as root on the host, and the image it escaped from barely had an attack surface to begin with. Hardened payload, hardened runtime, and you paid for neither.
I wrote a one-shot rootless Docker provisioning script exactly so this second half isn’t a weekend project. It sets up the dedicated user, the subuid maps, lingering, and the DOCKER_HOST plumbing in a single command. Combine it with a DHI base image and you’ve assembled the kind of container security posture that vendors put behind a sales call, using nothing but free, open, auditable pieces.
Closing the loop
For years, “hardened container image” was a phrase that came with a quote and a contract. That’s over. The free Docker Hardened Images catalog gives you distroless, non-root, near-zero-CVE images with a signed SBOM and Level 3 provenance, and the only tax is a one-minute token and a docker login dhi.io. Change your FROM line, move your install steps into a -dev build stage, listen on a high port, and run the whole thing on a rootless daemon. That’s enterprise-grade container security for the price of reading this post.