Learn to build and maintain 0-CVE container images to enhance security in your containerized applications and discover best practices for secure by default
In the rapidly evolving world of containerized applications, security is no longer a secondary concern—it has become a critical priority. One of the most impactful steps you can take to secure your container environments is to use zero-CVE (Common Vulnerabilities and Exposures) images. While most container images available in the wild carry dozens—or even hundreds—of known vulnerabilities, starting with zero-CVE images is both feasible and effective.
In this blog post, we’ll explore how to build zero-CVE images easily, why they matter, and which modern practices can help you maintain this security standard.
Multi-stage builds are a powerful Docker feature that allow you to separate the build environment from the runtime image. This strategy significantly reduces image size and removes unnecessary tools and libraries that could otherwise serve as potential entry points for attackers.
Why it matters:
This Dockerfile demonstrates how to compile a Go application in a secure, full-featured build environment, and then copy only the resulting binary into a minimal, production-ready image using Distroless. This approach helps eliminate unnecessary tools from the final image, further enhancing security and efficiency

// main.go
package main
import "fmt"
func main() {
fmt.Println("Hello from Docker!")
}
This lightweight Go application outputs a message to the terminal and is used to illustrate the advantages of using multi-stage builds and minimal base images in containerized environments

In this step, we run the final image built using the multi-stage Dockerfile. As shown, the application executes successfully and outputs:
"Hello from Docker!"
By leveraging Distroless images—which contain no package managers, shells, or unnecessary utilities—we significantly reduce the potential attack surface. This technique aligns perfectly with the goal of building zero-CVE images for secure production environments
A "headless" image is one that includes the bare minimum required to run your application. Popular headless base images include:
These images are intentionally minimal, which naturally leads to fewer vulnerabilities.

The table above illustrates the security and size advantages of using a distroless image. While a traditional Node.js image is approximately 1GB and may contain 100+ known CVEs, the distroless alternative is significantly smaller and contains zero known vulnerabilities.
In this example, we use a standard Node.js image to install dependencies and build the application. Then, the final production stage runs the app using a much smaller distroless image. This approach reduces image size, eliminates development tools, and enhances overall security.

This simple Express.js application defines a single route that returns a greeting and listens on port 3000. It's ideal for demonstrating the use of a headless image in action.
// index.js
const express = require('express');
const app = express();
app.get('/', (req, res) => res.send('Hello from Dockerized Node app!'));
app.listen(3000, () => console.log('app listening on port 3000'));
We build the container using a multi-stage Dockerfile and run it on port 3000. To verify it's working, we send a request using curl. The container responds correctly, confirming that the application functions as expected—even when running on a minimal base image.

root@test:~/headless/myapp# docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
mynodeapp latest 59a0b7136368 32 seconds ago 166MB
The resulting Docker image is just 166MB -- much smaller than a typical Node.js image—and free from the vulnerabilities commonly found in full-featured base images.
Chainguard takes things a step further by providing container images that are built with zero known vulnerabilities from the start. These images are:
This configuration listens on port 8080 and returns a simple HTTP 200 OK response. It demonstrates how lightweight services can be deployed securely using Chainguard’s hardened base images.
pid /tmp/nginx.pid;
events {}
http {
server {
listen 8080;
location / {
return 200 'Hello from Chainguard NGINX!\n'
}
}
}
root@test:~/chainguard# cat Dockerile
FROM cgr.dev/chainguard/nginx:latest
COPY ./my.conf /etc/nginx/nginx.conf
We build the container using the official Chainguard NGINX base image and run it on port 9191. A simple curl request confirms that the server is running properly—securely and efficiently.

Chainguard images are delivered with both signed metadata and a Software Bill of Materials (SBOM), ensuring complete transparency into what’s inside your container. This significantly enhances software supply chain security and helps maintain regulatory compliance.
In modern software development, detecting vulnerabilities is easy — but knowing which ones truly matter is the real challenge. That’s where VEX (Vulnerability Exploitability eXchange) comes in.
VEX is a standardized way to communicate the exploitability status of vulnerabilities in software components. Instead of treating every CVE as a critical issue, a VEX document tells you whether a vulnerability is:
These statements are signed and can be shared with consumers or processed by security tools — giving both devs and security teams better context.
Incorporating VEX into your CI/CD pipeline helps your team filter out non-exploitable vulnerabilities, enabling you to:
You can consume VEX documents alongside SBOMs using tools like grype, guac, or Chainguard’s Wolfi images. For example, a GitHub Actions step could validate a VEX file before deployment, or your container scanning job could skip known non-exploitable issues.
Inside your project directory, create a file named vex-cyclonedx.json with the following content:

This document explicitly marks CVE-2023-12345 as not_affected, with the justification component_not_present, meaning the vulnerable component simply doesn’t exist in the image — no patching required, and no risk.
Next, you can generate a Software Bill of Materials (SBOM) in CycloneDX format using Trivy:
root@test:~/vex-attestation# trivy image --format cyclonedx --output sbom.json mynodeapp:latest
2025-04-09T09Z INFO "--format cyclonedx" disables security scanning. Specify "--scanners vuln" explicitly if you want to include vulnerabilities in the "cyclonedx" reports
2025-04-09T09Z INFO Detected OS familiy="debian" version="11.6"
2025-04-09T09Z INFO Number of language-specific files num=1
This means Trivy is generating a clean SBOM that can be paired with your vex-cyclonedx.json file to provide full context to downstream tools.
When you scan your container image without VEX, every vulnerability is treated as a potential risk. However, by applying a VEX document, you can immediately reduce noise and focus on the actual threats.
Here’s an example scan output using Trivy with VEX enabled:

Here’s how you could incorporate VEX validation into your GitHub Actions workflow:
- name: Generate SBOM
run: trivy image --format cyclonedx --output sbom.json mynodeapp:latest
- name: Validate VEX Document
run: |
curl -sSL https://example.com/vex-validator.sh | bash \
--sbom sbom.json \
--vex vex-cyclonedx.json
You can even make your CI/CD pipeline fail only if the vulnerability is exploitable.
VEX gives you signal instead of noise. Combined with a minimal or zero-CVE image strategy, you no longer need to chase every alert — you can focus on real, actionable threats.
And by using tools like Trivy, CycloneDX, and signed VEX documents, you can turn your container security posture from reactive to context-aware and resilient by design.
For teams with strict compliance needs or handling sensitive workloads, relying on third-party images can introduce unknown risks and bloated dependencies.
By building your software stack manually — from the base image to every library — you gain full visibility and control over what goes into production. This DIY approach allows you to eliminate unnecessary components, patch known vulnerabilities, and maintain a minimal attack surface tailored to your environment.
A Fully Minimal Go Binary Image Using scratch
If your application is written in Go, you can create a fully static binary and package it in a container with absolutely no additional layers — not even an OS. This results in an ultra-minimal image that contains nothing but your binary.
root@test:~/DIY# GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -ldflags="-s -w" -o myapp hello.go
# Dockerfile
FROM scratch
COPY myapp /myapp
ENTRYPOINT ["/myapp"]
This image contains no shell, no package manager, and no operating system — only your statically compiled binary.

This method results in an extremely small, secure image with zero attack surface beyond your own binary.
To ensure our image is truly minimal and secure, we can scan it using vulnerability scanners like Trivy, Grype, or Snyk. Below is an example using Trivy:
root@test:~/DIY# trivy image --scanners vuln --skip-files /myapp --format table --no-progress zero-cve-app
2025-04-09T09Z INFO [vuln] Vulnerability scanning is enabled
2025-04-09T09Z INFO Number of langugage-specific files num=0
Since our image includes no OS packages and we've explicitly excluded the Go binary from the scan (to avoid false positives from embedded stdlib metadata), Trivy has nothing left to analyze. As expected, it completes silently — confirming that the image is truly minimal and free of any known CVEs.
Security teams often struggle with bloated base images that pull in hundreds of unnecessary libraries and dependencies — each a potential entry point for attackers. By stripping everything down to just your application, you:
By taking a DIY approach to building container images, you significantly reduce the risk introduced by third-party layers and unknown software components. Whether you’re working with statically compiled Go binaries and the scratch image, or minimizing dynamic languages like Python with tools like docker-slim, the result is the same: smaller, safer, and more compliant containers.
This method is particularly valuable in highly regulated environments where full control over every dependency is a necessity — not a luxury.