Scanning Docker Images for Vulnerabilities: A Practical Security Guide

14 min read 2,724 words
Container Security: Scanning Docker Images for Vulnerabilities Before Deployment featured image

Container images are a practical target for attackers who look for known vulnerabilities in outdated packages and dependencies. When a Docker image reaches production with unpatched components, it carries those risks into your live environment. Scanning Docker images for vulnerabilities helps you catch those issues before deployment rather than discovering them after an incident.

Image scanning tools inspect the contents of a container and compare them against updated vulnerability databases. When a match is found, the tool reports the severity, the affected package, and usually a recommended fix. This process works alongside other security practices like secure configuration, access control, and regular updates to form a more complete approach to container security.

Why Container Images Carry Security Risks

Every Docker image is built from layers. Each layer may contain an operating system package, a runtime, a library, or application code. Any of these components can expose a known vulnerability if it has not been updated recently.

Common sources of vulnerabilities in container images include base operating system packages inherited from images like ubuntu, debian, or alpine if those base images are not refreshed regularly. Language runtimes and dependencies such as Node.js, Python, PHP, and Java applications often pull in third-party packages that later receive security patches.

Third-party and official images pulled from Docker Hub or other registries mean trusting that the maintainer keeps them updated. Build toolchain components like build scripts, compilers, and development dependencies can introduce vulnerabilities if left in the final image.

Vulnerability databases such as the National Vulnerability Database and security advisories from Linux distributions feed into scanning tools. A scanner cross-references the packages in your image against these databases to produce a report that helps you decide what to address before deployment.

Understanding where these risks come from is part of building a practical security posture. If you are reviewing your current container setup, it is worth checking how base images are chosen and how often they are refreshed.

Scanning Docker Images with Trivy

Trivy is an open-source vulnerability scanner designed specifically for containers. It is lightweight, easy to install, and works well in both local development environments and automated pipelines. Trivy can scan filesystem directories, container images, and running containers without requiring significant configuration.

Installing Trivy on Linux

On a Linux system, you can install Trivy using the official script or a package manager. The example below downloads the binary directly and moves it to your local bin directory.

wget https://github.com/aquasecurity/trivy/releases/download/v0.50.0/trivy_0.50.0_Linux-64bit.tar.gz
tar -xzf trivy_0.50.0_Linux-64bit.tar.gz
sudo mv trivy /usr/local/bin/

Verify the installation by running the version command.

trivy --version

On macOS, you can install Trivy using Homebrew.

brew install trivy

For Docker users, Trivy is also available as a container image itself.

docker run --rm aquasecurity/trivy image nginx:1.25

Scanning a Docker Image

Once Trivy is installed, you can scan any Docker image by its tag or digest. The command below pulls the image if it is not already available locally and performs a full vulnerability scan.

trivy image nginx:1.25

Trivy outputs a table showing the vulnerability ID, severity level, package name, installed version, and the fixed version if one is available. High and critical severity findings typically warrant immediate attention before deployment.

Filtering Results by Severity

You can filter scan results by severity if you want to focus on the most pressing issues during a quick review or a CI/CD check.

trivy image --severity HIGH,CRITICAL nginx:1.25

This approach is useful in automated pipelines where you want to fail a build only on severe findings rather than informational ones. Running a full scan without filters is better suited for detailed audits.

Scanning a Local Project Directory

Trivy can also scan a directory containing application files before building an image. This is helpful for checking dependencies in a project without containerising it first.

trivy fs /path/to/project

This works well for catching vulnerabilities in dependencies listed in a package.json, requirements.txt, or composer.json file before they reach a container image. You can incorporate this into your local development workflow as a quick check before committing code.

Scanning Docker Images with Clair

Clair is another open-source vulnerability scanner, developed by Quay. It is designed for deeper integration into container registries and works by indexing container image layers before querying vulnerability data from multiple sources. Clair is commonly used in environments where images are stored in a private registry and need continuous monitoring over time.

How Clair Works

Clair pulls vulnerability data from sources including the Ubuntu Security Notices, Debian Security Bug Tracker, Red Hat Security Data, and the Alpine SecDB. When you register an image with Clair, it analyses each layer and builds a feature catalogue. The scanner then compares that catalogue against the vulnerability feeds to produce findings.

Unlike Trivy, which scans on demand, Clair typically runs as a service that continuously monitors registered images. This makes it better suited for environments where you need ongoing visibility across many images stored in a registry rather than one-off scans during builds.

Running a Basic Scan with Clair

For local testing, you can run Clair using Docker Compose alongside a PostgreSQL database.

version: '3'
services:
  clair:
    image: quay.io/coreos/clair:v4.8.0
    depends_on:
      - postgres
    networks:
      - clair-net
  postgres:
    image: postgres:15
    networks:
      - clair-net
networks:
  clair-net:

Once Clair is running, you interact with it using a scanner tool or the Clair API. For most teams, Trivy is faster to set up for local scanning, while Clair provides more value when integrated with a container registry at scale and you need continuous monitoring across many images.

Integrating Container Scanning into a CI/CD Pipeline

Running a scan manually is useful for learning and for one-off reviews, but in a production workflow, scans should run automatically. Integrating container vulnerability scanning into your CI/CD pipeline means every image built gets checked before it reaches any environment.

This approach sits alongside other security hardening practices. If you want to explore container hardening further, a review of your build process and runtime configuration can complement the scanning workflow.

GitHub Actions Example

You can add a Trivy scan step to a GitHub Actions workflow that builds and tests your Docker image. The example below shows a workflow that builds an image and runs a vulnerability scan on every push to the main branch.

name: Build and Scan

on:
  push:
    branches:
      - main

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Build Docker image
        run: docker build -t myapp:${{ github.sha }} .

      - name: Run Trivy vulnerability scanner
        uses: aquasecurity/trivy-action@master
        with:
          image-ref: 'myapp:${{ github.sha }}'
          format: 'table'
          exit-code: '1'
          severity: 'HIGH,CRITICAL'

The exit-code: '1' setting causes the step to fail if any high or critical vulnerabilities are found. This stops the pipeline and prevents the image from progressing to the next stage. Adjust the severity threshold based on your team's tolerance and the sensitivity of the application.

GitLab CI Example

In a GitLab CI pipeline, you can use the Trivy container scanner or run Trivy directly in a job. The example below builds the image, pushes it to the registry, and then scans it before deployment.

stages:
  - build
  - scan

build:
  stage: build
  image: docker:24
  services:
    - docker:24-dind
  script:
    - docker build -t myapp:$CI_COMMIT_SHA .
    - docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA

trivy-scan:
  stage: scan
  image:
    name: aquasecurity/trivy:latest
    entrypoint: [""]
  script:
    - trivy image --exit-code 1 --severity HIGH,CRITICAL $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
  dependencies:
    - build

If the scan finds critical vulnerabilities, the pipeline fails and the team is notified. This prevents vulnerable images from reaching staging or production environments.

Scanning Base Images Before Use

A common security practice is to scan base images and third-party images before pulling them into your environment. You can do this with Trivy directly on the registry reference.

trivy image --severity CRITICAL node:20-alpine

If the scan reveals serious unresolved vulnerabilities in the base image, you can choose a different image or a different tag that is based on a more recent release. Building this check into your workflow before pulling external images reduces the risk of introducing known issues into your environment.

Interpreting Scan Results and Setting Thresholds

Not every vulnerability requires immediate action. A practical approach is to set severity thresholds based on where the image will run and what it will do. Understanding the impact of each finding helps you prioritise effectively.

Severity Levels in Vulnerability Scanning

Most scanners follow the Common Vulnerability Scoring System for rating severity. This standard provides a consistent way to communicate the risk associated with each finding.

  • Critical: Remote code execution, unpatched server flaws that can be exploited remotely. These should block deployment in almost all cases.
  • High: Significant vulnerabilities that could be exploited under certain conditions. Worth addressing before production deployment.
  • Medium: Exploitable under specific conditions or with additional prerequisites. Can be tracked and resolved in a regular maintenance cycle.
  • Low: Minor issues or informational findings that have limited practical impact. Good to fix but not urgent.

Setting Different Thresholds for Different Environments

A development environment might allow images with medium-severity issues to pass through for faster iteration. A staging environment might require that critical and high findings are resolved before promotion. Production environments should enforce the strictest thresholds, ideally blocking any image with unresolved critical vulnerabilities.

You can configure these thresholds in your CI/CD pipeline and adjust them as your team improves the baseline security of your images over time. Starting strict and relaxing thresholds as needed is safer than the reverse approach.

Reducing Vulnerabilities Through Better Image Practices

Scanning finds problems, but fixing them requires a process for keeping images current. Regularly rebuilding images with updated base layers and dependency packages reduces the number of vulnerabilities over time.

Use Minimal Base Images

Alpine-based images are smaller and tend to have fewer packages installed than full distribution images. A smaller surface area means fewer potential vulnerabilities. When you use node:20-alpine instead of node:20, you reduce the number of system packages in the image that could contain known vulnerabilities.

Pin Base Image Tags Carefully

Using node:20 without a specific tag means Docker pulls the latest tag each time, which can introduce unexpected changes. Pinning to a specific digest ensures consistency, but it also means you need a process to update that digest when a new version is released. Automated tools like Renovate can help manage these updates without requiring manual attention each time.

node:20@sha256:abc123def456...

Multi-Stage Builds

Multi-stage Docker builds let you compile and build your application in one stage, then copy only the final artefacts into a smaller runtime image. This keeps build tools, compilers, and development dependencies out of the production image, reducing the attack surface.

FROM node:20 AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM node:20-alpine
WORKDIR /app
COPY --from=build /app/dist ./dist
COPY --from=build /app/node_modules ./node_modules
CMD ["node", "dist/index.js"]

The final runtime image contains only the application code and production dependencies, without the build toolchain that was needed during compilation. This is one practical step toward hardening your container images alongside regular scanning.

Running Containers with Restricted Privileges

Even with a clean scan, a container that runs with excessive privileges can cause damage if it is compromised. Configuring runtime security limits what a container can do, reducing the impact of a potential exploit.

Docker and Kubernetes offer security options such as dropping capabilities, running as a non-root user, and enabling read-only file systems. These settings constrain what a compromised container can do, even if an attacker gains access.

Combining secure image practices with runtime restrictions creates a stronger overall posture for your containerised applications. Scanning alone does not address runtime behaviour, which is why these measures work best together.

Scanning in Kubernetes Environments

In a Kubernetes cluster, image security tools like Kyverno and OPA Gatekeeper can enforce policies that require images to be scanned before they are allowed to run. Kubernetes admission controllers can reject workloads that do not meet defined security criteria.

For teams running container workloads in Kubernetes, admission controllers that integrate with Trivy or Clair automate policy enforcement at the cluster level. This ensures that even if a developer bypasses a CI/CD check, the cluster itself provides a safety net.

Admission controllers evaluate images against your defined policies at deployment time. If an image has unresolved critical vulnerabilities or comes from an untrusted registry, the workload is rejected. This layer of enforcement is particularly valuable in larger teams where not every developer may be familiar with every security requirement.

Ongoing Monitoring and Regular Maintenance

A single scan at build time is a snapshot, not a complete solution. New vulnerabilities are discovered regularly, which means an image that was clean last week might have a known issue today. Setting up regular rescans of images in your registry, and monitoring for new advisories, helps catch emerging issues before they are exploited.

Tools like Anchore and Snyk Container offer continuous monitoring features that alert you when a vulnerability is published that affects an image you have deployed. This ongoing visibility is important for long-term container security and complements the build-time scanning process.

Establishing a cadence for rebuilding images, combined with automated rescanning, keeps your container stack reasonably current without requiring constant manual attention. Many teams schedule automated rebuilds weekly or monthly, pulling the latest base image tag and running the full scan and test suite.

Related practical reading

These related guides can help you connect this topic with the wider website, server, security, and support decisions around it.

Frequently Asked Questions

Can I rely solely on image scanning to secure my containers?
No. Scanning Docker images is an important layer of container security, but it does not cover runtime behaviour, network exposure, or misconfigured permissions. A complete approach also includes secure image building practices, runtime restrictions, network segmentation, access control, and regular updates. Think of scanning as one part of a broader security strategy rather than a standalone solution.
Does scanning slow down my CI/CD pipeline?
Trivy scans are generally fast, especially when image layers are cached. A typical scan on a moderately sized image can complete in under a minute. If scans take too long in your setup, you can limit the scope to specific severity levels during builds while running full scans on a schedule outside the main deployment path.
Should I scan every image I pull from Docker Hub?
It is a good practice to scan third-party images before using them in any environment, particularly production. Scanning helps you understand what you are introducing into your environment and whether the image maintainer keeps it updated. For official images from verified publishers, the risk is generally lower, but vulnerabilities can still appear in any software component over time.
What happens if a scan finds a vulnerability with no fix available?
If a vulnerability has no patched version in the image you are using, you have a few options. You can look for an alternative base image that does not include the affected package, switch to a different tag or distribution, or rebuild the image from scratch using updated sources. If the vulnerability cannot be avoided immediately, you can mitigate the risk through runtime restrictions, network policies, and monitoring that limit how the vulnerability could be exploited.
How often should I rebuild my Docker images?
Regular rebuilding is a practical way to keep images current. A common approach is to schedule automated rebuilds weekly or monthly, pulling the latest base image tag and running your full scan and test suite. This keeps your dependency stack fresh without requiring manual intervention each time a new patch is released.