# Docker Container Security Scanning with Trivy: A Production Guide

> Integrate Trivy container vulnerability scanning into your Docker workflow — covering CI/CD integration, base image selection, and reducing your container attack surface.

URL: https://www.matthewswong.com/en/blog/docker-container-security-scanning/

- Author: Matthews Wong (https://www.matthewswong.com/en/about/)
- Published: 2025-06-10
- Updated: 2025-06-10
- Category: DevOps

Every Docker image you pull is a trust decision. The official Node.js 20 image on Docker Hub contains a full Debian or Alpine Linux installation — and those operating system packages have vulnerabilities. In 2024, Snyk's container security report found that 84% of container images have at least one known vulnerability, and 45% have a critical or high-severity CVE. I run Trivy container scanning in my CI/CD pipelines for all production deployments and have blocked multiple image updates that would have introduced new CVEs. This guide covers how to integrate Trivy into your workflow and actually reduce your container attack surface — not just scan and ignore.

## Why Container Scanning Matters

Containers do not provide security isolation — they provide process isolation. A vulnerability in a library inside your container is an exploitable vulnerability in your application. The Log4Shell vulnerability (CVE-2021-44228) in late 2021 affected any Java application using log4j — including applications running in containers. Containers that had not been rebuilt with patched dependencies remained vulnerable even though the host OS was fully patched. Container scanning catches these library-level vulnerabilities before deployment and provides continuous monitoring for new CVEs affecting your deployed images.

### Trivy: The Scanner I Use

Trivy is an open-source vulnerability scanner by Aqua Security that scans Docker images, filesystems, git repositories, and Kubernetes manifests. It checks: OS packages (Debian, Alpine, Ubuntu, RHEL), language-specific packages (npm, pip, Go modules, Maven), infrastructure misconfigurations, and exposed secrets in images. Trivy updates its vulnerability database daily from NVD, GitHub Advisory, and OS-specific vulnerability databases. It runs as a single binary with no daemon required — ideal for CI/CD integration. Install with: `brew install trivy` or via the official Docker image.

> From my experience running Trivy in production CI/CD: use `--ignore-unfixed` in your CI pipeline to only fail on vulnerabilities that have available fixes. Failing on every CVE including unfixable ones creates alert fatigue — developers start ignoring the scanner output. Only actionable findings get actioned. Reserve unfixed vulnerability reporting for a separate weekly security review where you triage and make deliberate decisions about accepted risk.

## Integrating Trivy into GitHub Actions

The CI integration pattern I use: build the Docker image in CI, run Trivy scan on the built image, fail the pipeline on critical or high-severity vulnerabilities with available fixes, and upload the SARIF report to GitHub Security tab for tracking. This ensures no image with critical vulnerabilities gets pushed to production. The pipeline runs Trivy using the official GitHub Action which handles database caching for performance.

### Reducing Attack Surface: Base Image Selection

The most effective container security control is choosing minimal base images. Comparison for a Node.js application: node:20 (Debian-based) — 600MB, 300+ packages, many unneeded. node:20-slim — 200MB, minimal Debian packages. node:20-alpine — 65MB, Alpine Linux with only ~20 packages. Distroless (gcr.io/distroless/nodejs20) — no shell, no package manager, minimal attack surface. I use node:20-alpine for most services and distroless for security-critical components. Fewer packages means fewer vulnerability surface. A Trivy scan on node:20-alpine typically shows 5-15 vulnerabilities vs 80-120 on node:20.

> **Base Image Pinning Warning**

> Using `FROM node:20` in your Dockerfile is dangerous — it silently updates to the latest patch when you rebuild, which can introduce new vulnerabilities or breaking changes. Always pin to a specific digest: `FROM node:20.18.1-alpine3.20@sha256:'<'digest>`. Use Renovate Bot or Dependabot to automate base image updates — they create PRs with changelogs when new base images are available, letting you review and test before updating. Unpinned base images in production have burned me before: an image update changed the default user from root to node and broke file permission assumptions in the application.

## Dockerfile Security Best Practices

Beyond base image selection, Dockerfile security comes down to: running as a non-root user (USER node or USER 1001 — never run production containers as root), not copying secrets into the image (use build-time secrets with --mount=type=secret, not ARG), using multi-stage builds to exclude build tools and source code from the final image, scanning for secrets in the Dockerfile itself (Trivy does this), and setting read-only filesystem where possible (--read-only flag at container runtime). A container running as root is a privilege escalation risk if any vulnerability exists in the application or its dependencies.

## Runtime Security with Trivy and Policies

Trivy can scan running containers and Kubernetes manifests in addition to images. For Kubernetes: `trivy k8s --report summary cluster` scans your entire cluster for vulnerabilities and misconfigurations. OPA Gatekeeper or Kyverno can enforce policies that prevent pods from using unscanned or non-compliant images from running in production. The combination of pre-deployment scanning and runtime policy enforcement creates a continuous security posture rather than a point-in-time check.

## My Container Security Workflow

Complete workflow: Dockerfile uses pinned Alpine base image and non-root user. Multi-stage build excludes devDependencies and build tools from final image. GitHub Actions builds image and runs Trivy with --exit-code 1 on critical/high fixable CVEs. Trivy SARIF output uploaded to GitHub Security tab for tracking. Images tagged with git SHA and pushed to registry only after scan passes. Renovate Bot creates weekly PRs for base image updates. Monthly Trivy scan of all running images in production cluster. This workflow reduced our critical CVE count from 40+ (when we started scanning) to consistently under 5 — and those remaining 5 are tracked accepted risks with mitigation plans.

## Sources & Further Reading

- Aqua Security — Trivy Documentation — https://trivy.dev/latest/docs/
- Snyk — Container Security Report 2024 — https://snyk.io/reports/container-security/
- NIST — CVE-2021-44228 Log4Shell — https://nvd.nist.gov/vuln/detail/CVE-2021-44228

## Source links

- https://trivy.dev/latest/docs/
- https://snyk.io/reports/container-security/
- https://nvd.nist.gov/vuln/detail/CVE-2021-44228

## FAQ

### Why should I use Trivy instead of just trusting official Docker Hub images?

Official images like node:20 still contain a full OS installation with hundreds of packages, many of which carry known CVEs. Snyk's 2024 container security report found 84% of container images have at least one known vulnerability and 45% have a critical or high-severity CVE. Trivy scans OS packages, language-specific packages, and exposed secrets so you know exactly what risks you are accepting before deploying.

### What does the --ignore-unfixed flag do and why is it recommended for CI pipelines?

The --ignore-unfixed flag tells Trivy to only fail the pipeline on vulnerabilities that have a fix available, skipping CVEs with no patch yet. Without it, pipelines fail on unfixable issues and developers begin ignoring scanner output — a phenomenon called alert fatigue. The post recommends reserving unfixed vulnerability reporting for a separate weekly security review where deliberate risk decisions can be made.

### How much does switching to a minimal base image reduce my vulnerability exposure?

Switching from node:20 (Debian-based, 600 MB, 300+ packages) to node:20-alpine (65 MB, ~20 packages) typically reduces Trivy findings from 80–120 vulnerabilities down to 5–15. For security-critical components, distroless images go further by removing the shell and package manager entirely. Fewer packages means fewer potential vulnerability surfaces.

### Why is using FROM node:20 without a digest considered dangerous?

An untagged or tag-only reference silently pulls the latest patch whenever the image is rebuilt, which can introduce new CVEs or breaking changes without any review. The post recommends pinning to a specific digest — e.g. FROM node:20.18.1-alpine3.20@sha256:<digest> — and using Renovate Bot or Dependabot to manage controlled updates via reviewed PRs. The author experienced a real incident where an unpinned update changed the default user from root to node and broke file permission assumptions in the application.

### What CI/CD workflow does the author use to enforce container security in production?

The workflow builds the Docker image in GitHub Actions, runs Trivy with --exit-code 1 on critical/high fixable CVEs, uploads the SARIF report to the GitHub Security tab for tracking, and only pushes images tagged with the git SHA to the registry after the scan passes. Renovate Bot creates weekly PRs for base image updates, and a monthly Trivy scan runs against all images in the production cluster. This process reduced their critical CVE count from 40+ to consistently under 5.

---

- HTML version: https://www.matthewswong.com/en/blog/docker-container-security-scanning/
- Bahasa Indonesia: https://www.matthewswong.com/id/blog/docker-container-security-scanning/
- Site index for agents: https://www.matthewswong.com/llms.txt
- Sitemap: https://www.matthewswong.com/sitemap.xml
- API description (OpenAPI): https://www.matthewswong.com/openapi.json
