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.
- GraphQL in PHP vs REST: When GraphQL Is the Better Choice - useful background for related development decisions
- PHP 8.4: Property Hooks and Asymmetric Visibility - useful background for related development decisions