Towards Zero Container Vulnerabilities: Comparing Chainguard (cgr.dev) and Docker Hardened Images (dhi.io)
When running container image security scans in a CI/CD pipeline, you will often encounter dozens or even hundreds of Common Vulnerabilities and Exposures (CVE) warnings.
Even when using standard Debian, Ubuntu, or minimal Alpine-based images, it is practically impossible to completely avoid detected CVEs caused by unnecessary packages or embedded shells.
To address this challenge, many enterprise teams are actively evaluating secure base images designed around a "0-CVE" standard.
In this post, we will compare Chainguard (cgr.dev), a pioneer in container supply chain security, with Docker's own hardened offering, Docker Hardened Images (dhi.io). We will also clarify common points of confusion during adoption and examine proper multi-stage build patterns for production.
Why Does cgr.dev Lack Official Verification on Docker Hub?
One of the first questions engineers ask when encountering Chainguard images is: "Why don't I see the Docker Official Image badge on Docker Hub?"
Given Chainguard's strong industry reputation, it is natural to wonder whether it lacks official credibility.
The short answer is that this is not an issue of credibility, but a result of registry domain separation and Docker's business policies.
Understanding Docker Hub's Badge Hierarchy
The Docker Hub ecosystem is broadly divided into three tiers:
Docker Official Images (DOI): Standard open-source images maintained by the community and Docker, referenced without a namespace (e.g., alpine, python, node).
Docker Verified Publisher (DVP): Third-party commercial vendor images hosted on Docker Hub through a paid verification program (under the chainguard/* namespace).
Independent OCI Registry (cgr.dev): Chainguard hosts and distributes its primary enterprise images directly through its own dedicated registry.
Chainguard's core engineering team includes key contributors who led initiatives like Distroless, Sigstore, and Cosign at Google.
To automatically rebuild thousands of images daily, attach real-time Cosign digital signatures and SBOMs, and enforce strict enterprise SLAs, Chainguard relies on its own independent infrastructure (cgr.dev).
Because cgr.dev is an external registry completely decoupled from Docker Hub, Docker Hub verification badges cannot technically apply to it.
Chainguard vs. Docker Hardened Images (DHI)
Docker recently introduced its own suite of enterprise-hardened images under DHI (dhi.io), creating direct competition with Chainguard.
Here is a side-by-side comparison of their core differences:
Chainguard (cgr.dev)
Base OS: Wolfi / Chainguard OS (custom rolling distro)
Core Philosophy: Pure Distroless (eliminates the attack surface entirely)
Catalog Breadth: 3,000+ (comprehensive runtimes, tools, and FIPS-compliant variants)
CVE Patch SLA: Critical vulnerabilities patched within an average of 20 hours (paid tier)
Debugging: Highly restricted (no shell; ephemeral debug containers required)
Migration Effort: Requires Dockerfile adjustments due to differing package names and paths
Docker Hardened Images (dhi.io)
Base OS: Hardened Debian / Alpine foundations
Core Philosophy: Compatibility-first with curated package minimization
Catalog Breadth: 500+ (focused on mainstream runtimes and key tools)
CVE Patch SLA: Tiered SLAs based on Docker subscription (up to 7 days for enterprise)
Debugging: Familiar (standard Linux toolchains and optional debug utilities)
Migration Effort: Low barrier to entry; often just a FROM tag replacement
Why Does wolfi-base Contain a Shell?
Developers testing Chainguard for the first time often encounter an unexpected surprise:
"If Chainguard is Distroless with zero shells, why do /bin/sh and apk exist in wolfi-base?"
This confusion clears up once you understand Chainguard's tiered image architecture.
Image Tier Architecture
Plain Text
1
2
3
4
[OS Base] cgr.dev/chainguard/wolfi-base ➔ Shell (Yes), apk (Yes) : For building and packaging
[Runtime (Dev)] cgr.dev/chainguard/python:*-dev ➔ Shell (Yes), apk (Yes) : For development and local testing
[Runtime (Prod)] cgr.dev/chainguard/python:latest ➔ Shell (No), apk (No) : Distroless production runtime
[Minimal Binary] cgr.dev/chainguard/static ➔ Shell (No), glibc/musl: For Go/Rust static binarieswolfi-base functions as a Builder image, comparable to base alpine or debian-slim. Its sole purpose is compiling source code and downloading dependencies, which strictly requires a functional shell and a package manager.
In contrast, production runtime tags (:latest) strip out /bin/sh, package managers, and standard core utilities entirely, leaving only what is required to execute the entrypoint binary.
Practical Application: Multi-Stage Build Pattern
To drive vulnerabilities down to near-zero in production, never deploy wolfi-base directly as your final image.
Instead, implement a Multi-stage Build where wolfi-base is isolated to the build stage, and only the final compiled artifact is copied into a shell-free runtime image.
Here is an example using a Spring Boot application:
Dockerfile
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# ----------------------------------------------------------------
# Stage 1: Build Environment (Uses shell and apk to compile the app)
# ----------------------------------------------------------------
FROM cgr.dev/chainguard/wolfi-base AS builder
# Install build dependencies and OpenJDK 21
RUN apk update && apk add --no-cache openjdk-21 git
WORKDIR /build
COPY gradlew .
COPY gradle ./gradle
COPY build.gradle settings.gradle ./
COPY src ./src
# Build an executable fat JAR
RUN ./gradlew bootJar --no-daemon
# ----------------------------------------------------------------
# Stage 2: Production Runtime (Shell-free and package-manager-free)
# ----------------------------------------------------------------
FROM cgr.dev/chainguard/jre:latest
WORKDIR /app
# Copy only the compiled artifact from the builder stage
COPY --from=builder /build/build/libs/*.jar app.jar
# Enforce non-root execution for defense in depth
USER nonroot
# Must use Exec format (JSON array) since no shell is present
ENTRYPOINT ["java", "-jar", "app.jar"]Key Benefits and Considerations
Expected Benefits
Attack Surface Elimination: Because /bin/sh does not exist inside the running container, even if an attacker discovers a Remote Code Execution (RCE) flaw, they cannot spawn an interactive shell or download external malicious payloads via curl or wget.
Clean Vulnerability Scanners: Eliminates false positives and noisy alerts across scanners like Trivy, Grype, and Snyk, letting security teams focus on actual application-level risks.
Considerations
Shift in Debugging Workflows: Interactive troubleshooting via docker exec -it <container> /bin/sh is no longer possible. Centralized logging (ELK, CloudWatch, Datadog) becomes mandatory, and Kubernetes teams must adopt patterns like kubectl debug with ephemeral containers.
Strict Entrypoint Syntax: Using the shell form ENTRYPOINT java -jar app.jar implicitly invokes /bin/sh -c, which fails immediately in a shell-less container. You must always use the Exec form (["java", "-jar", "app.jar"]).
Conclusion
Container hardening is not about artificially gaming scanner metrics to display a clean dashboard—it is an architectural safeguard that prevents unauthorized execution paths before vulnerabilities can be weaponized.
If your organization operates under strict compliance mandates, requires rapid vulnerability remediation SLAs, and demands total attack surface elimination, Chainguard (cgr.dev) remains the gold standard.
If your team is already invested in the Docker Business ecosystem and prioritizes backward compatibility and low migration friction, Docker Hardened Images (dhi.io) offers a practical path forward.
Whichever option you choose, adhering to the core principle of separating build tooling from runtime environments using multi-stage builds will ensure a significantly more resilient and secure production infrastructure.
This article was generated with AI

민갤
Back-End Developer
백엔드 개발자입니다.

Deep Dive into Vue 3.5: Reactive Props Destructure and useTemplateRef
Explore the compilation mechanics behind Vue 3.5's native Reactive Props Destructure, alongside type-safe template referencing with useTemplateRef and core reactivity optimizations.
Vue

Nuxt 4 Migration Guide: app/ Directory Restructuring and Gradual Adoption
A comprehensive guide to Nuxt 4's app/ folder layout, progressive migration using the future compatibility flag, Nitro server isolation, and bundle optimizations.
Nuxt