
August 18, 2026
10 min read
Table of Contents
By Kokil Thapa | Last reviewed: August 2026
Shipping containers without verifying their contents is a liability, not a strategy. Container image scanning with Trivy provides an automated, open-source mechanism to identify operating system vulnerabilities, application dependencies, misconfigurations, and embedded secrets before code ever reaches production. Whether you are deploying Laravel applications on Ubuntu servers or managing microservices on Kubernetes, integrating this scanner into your workflow prevents known exploits from becoming active incidents.
trivy image <image-name> against local or remote Docker images to detect OS package CVEs, language-specific dependency vulnerabilities, IaC misconfigurations, and hardcoded secrets. It integrates directly into CI/CD pipelines to block insecure builds automatically.Security tooling often feels disconnected from daily development work, but effective scanning must be as routine as running tests. In my experience maintaining production infrastructure for legal-tech portals and eCommerce platforms, the gap between "it works locally" and "it is safe to deploy" is where most breaches originate. If you are already implementing CI/CD pipeline best practices, adding vulnerability scanning is the logical next step to harden your release process without slowing down delivery.
How do you perform container image scanning with Trivy locally?
Local scanning is your first line of defense. Before pushing any image to a registry, you should validate it on your development machine or build server. Trivy supports scanning local Docker daemon images, tar archives, and remote registries without requiring root privileges or a running container.
Installation and basic execution
On Ubuntu 24.04 LTS, which I use for most client deployments, install the latest stable version (v0.58+ as of mid-2026) via the official APT repository:
sudo apt-get install wget apt-transport-https gnupg lsb-release
wget -qO - https://aquasecurity.github.io/trivy-repo/deb/public.key | sudo gpg --dearmor -o /usr/share/keyrings/trivy.gpg
echo "deb [signed-by=/usr/share/keyrings/trivy.gpg] https://aquasecurity.github.io/trivy-repo/deb $(lsb_release -sc) main" | sudo tee -a /etc/apt/sources.list.d/trivy.list
sudo apt-get update
sudo apt-get install trivy Run a comprehensive scan against a local image:
trivy image --severity HIGH,CRITICAL --exit-code 1 my-laravel-app:latest The --exit-code 1 flag is critical for automation; it forces the command to fail if vulnerabilities matching the severity filter are found. Without it, Trivy exits successfully regardless of findings, rendering it useless in scripted environments.
Understanding scan targets
Trivy does not merely check installed packages. A complete scan covers four distinct layers:
- OS Packages: Vulnerabilities in Alpine apk, Debian dpkg, RHEL rpm, or Ubuntu apt packages.
- Language Dependencies: Composer (PHP), npm/yarn (Node.js), pip (Python), bundler (Ruby), go.mod (Go).
- IaC Misconfigurations: Dockerfile best practices, Kubernetes manifests, Terraform files embedded in the image.
- Secrets: Hardcoded API keys, passwords, private keys, and tokens detected via regex patterns.
For PHP-heavy applications like those built with Laravel or Symfony, pay special attention to the Composer lock file analysis. Trivy cross-references your pinned versions against the GitHub Advisory Database and Packagist security advisories. This catches vulnerable packages even when the underlying OS is fully patched.
How do you integrate Trivy into GitLab CI/CD pipelines?
Scanning locally is good; scanning automatically on every push is better. For teams using GitLab CI, which powers many of the deployment workflows I manage for sister sites like notarykathmandu.com and translationnepal.com, Trivy integrates as a native job stage.
Pipeline configuration
Add this job definition to your .gitlab-ci.yml:
security-scan:
stage: test
image:
name: aquasec/trivy:latest
entrypoint: [""]
variables:
TRIVY_SEVERITY: "HIGH,CRITICAL"
TRIVY_EXIT_CODE: "1"
TRIVY_FORMAT: "table"
IMAGE_NAME: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
script:
- trivy image --severity ${TRIVY_SEVERITY} --exit-code ${TRIVY_EXIT_CODE} ${IMAGE_NAME}
allow_failure: false
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH This configuration ensures scanning runs on merge requests and main branch commits. The allow_failure: false setting makes the pipeline fail explicitly when vulnerabilities are detected, preventing merge or deployment. For teams adopting server security hardening practices, this automated gate is non-negotiable.
SARIF reporting for merge request comments
GitLab Ultimate and some third-party integrations support SARIF format for inline vulnerability comments. Modify the script section:
script:
- trivy image --format sarif --output gl-container-scanning-report.json ${IMAGE_NAME}
artifacts:
reports:
container_scanning: gl-container-scanning-report.json This uploads results directly to GitLab's Security Dashboard, allowing reviewers to see vulnerabilities alongside code changes without leaving the merge request interface.
What are common false positives and how do you handle them?
No scanner is perfect. Trivy occasionally flags issues that are not exploitable in your specific context, or reports vulnerabilities in packages you cannot upgrade due to compatibility constraints. Blindly trusting every finding leads to alert fatigue; ignoring findings leads to breaches. The solution is structured triage.
Ignore file for accepted risks
Create a .trivyignore.yaml file in your repository root:
vulnerabilities:
- id: CVE-2024-12345
statement: "Not exploitable: affected function never called in our codebase"
expires: 2026-12-31
- id: CVE-2025-67890
statement: "Upstream fix pending; mitigated by input validation in controller"
misconfigurations:
- id: DS002
statement: "Container runs as root intentionally for legacy PHP-FPM socket permissions" Reference it during scans:
trivy image --ignorefile .trivyignore.yaml my-app:latest The expires field is crucial. Accepted risks should have review dates. An ignore without expiration becomes permanent technical debt that nobody revisits.
Distinguishing theoretical vs. practical risk
A CRITICAL rating does not always mean immediate danger. Evaluate each finding against three criteria:
- Reachability: Is the vulnerable code path actually invoked by your application? A vulnerability in an unused library function is lower priority than one in your authentication middleware.
- Exploit availability: Does a public exploit exist? Check NVD, Exploit-DB, and vendor advisories. Theoretical vulnerabilities without weaponized exploits allow more remediation time.
- Compensating controls: Do WAF rules, network segmentation, or input validation mitigate the attack vector? Defense in depth means individual findings may be acceptable when layered protections exist.
In practice, I've seen legal-tech portals flagged for CRITICAL OpenSSL vulnerabilities that were completely irrelevant because the application used PHP's curl extension linked against a different TLS library. Context matters more than CVSS scores.
How does Trivy compare to Grype, Snyk, and Docker Scout?
Choosing a scanner depends on your budget, infrastructure, and compliance requirements. Each tool has trade-offs that matter in production environments.
| Feature | Trivy | Grype | Snyk | Docker Scout |
|---|---|---|---|---|
| License | Apache 2.0 (OSS) | Apache 2.0 (OSS) | Proprietary (Free tier) | Proprietary (Docker Hub tied) |
| DB Updates | Hourly, offline-capable | Continuous, SBOM-based | Cloud-dependent | Docker Hub integrated |
| Scan Targets | Images, FS, Repo, IaC, Secrets | Images, SBOMs only | Code, Images, IaC, Cloud | Images, runtime insights |
| CI Integration | Native GitLab/GitHub/Jenkins | Anchore ecosystem | All major platforms | Docker Build Cloud |
| False Positive Rate | Moderate | Lower (SBOM precision) | Lowest (curated DB) | Moderate |
| Cost (2026) | Free forever | Free forever | $52/dev/month (Team) | Free (limited) / $11/mo Pro |
| Best For | Budget-conscious, air-gapped | SBOM-first workflows | Enterprise compliance | Docker-native shops |
For Nepal-based teams and freelancers operating on tight budgets, Trivy offers the best balance of capability and cost. It requires no cloud account, works entirely offline after initial database download, and covers more scan types than Grype. Snyk's curated database produces fewer false positives, but the per-developer pricing (approximately NPR 7,000/month per seat in 2026) adds up quickly for small agencies. Docker Scout is convenient if you already live in Docker Hub, but vendor lock-in concerns make it less attractive for multi-registry environments.
How do you reduce vulnerability noise in production images?
Scanning reveals problems; good engineering prevents them. Reducing the attack surface at build time is more effective than triaging hundreds of findings post-build.
Multi-stage builds with minimal bases
Your final image should contain only runtime artifacts. For a Laravel 12 application running on PHP 8.4:
# Build stage
FROM composer:2.7 AS vendor
WORKDIR /app
COPY composer.json composer.lock ./
RUN composer install --no-dev --no-scripts --prefer-dist
# Frontend build
FROM node:22-alpine AS assets
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci && npm run build
# Production stage
FROM php:8.4-fpm-alpine
RUN apk add --no-cache nginx supervisor
COPY --from=vendor /app/vendor /var/www/html/vendor
COPY --from=assets /app/public/build /var/www/html/public/build
COPY . /var/www/html
USER www-data This pattern eliminates build tools, dev dependencies, and source maps from the final image. Alpine base reduces OS-level CVE exposure compared to full Debian/Ubuntu images. On projects like Nepal Gift Card, switching from ubuntu:24.04 to alpine reduced Trivy findings by over 60% without any application changes.
Pin and verify dependencies
Never use floating version constraints in production. Lock files (composer.lock, package-lock.json) must be committed and verified:
RUN composer install --no-dev --prefer-dist \
&& composer audit --locked The composer audit command (available since Composer 2.4) performs its own advisory check during build, providing a second opinion before Trivy runs. Defense in depth applies to dependency verification too.
Regular base image updates
Vulnerabilities accumulate over time. Schedule weekly rebuilds of your base images even when application code hasn't changed. Automate this with CI cron jobs:
rules:
- if: $CI_PIPELINE_SOURCE == "schedule"
variables:
FORCE_REBUILD: "true" Automated rebuilds catch upstream patches before they become urgent incident responses. Combine this with Trivy's --db-skip-update=false flag to ensure fresh vulnerability databases on scheduled runs.
Implementing Container Image Scanning with Trivy Today
Start simple. Run trivy image manually on your current production images today to establish a baseline. Don't try to fix everything at once; categorize findings by severity and reachability. Integrate into CI next week with --exit-code 1 on HIGH and CRITICAL only. Add SARIF reporting and ignore files as your team matures. Within a month, you'll have shifted security left without paralyzing development velocity.
If your team needs help establishing secure deployment pipelines, integrating vulnerability scanning into existing workflows, or auditing current container practices, reach out to discuss your infrastructure. Secure deployments shouldn't require reinventing your entire release process.

