Kokil Thapa - Professional Web Developer in Nepal
Freelancer Web Developer in Nepal with 15+ Years of Experience

Kokil Thapa is an experienced full-stack web developer focused on building fast, secure, and scalable web applications. He helps businesses and individuals create SEO-friendly, user-focused digital platforms designed for long-term growth.

Container Image Scanning with Trivy

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.

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.
Trivy Scan Coverage LayersOS Packagesapk / dpkg / rpmCVE Database MatchDependenciesComposer / npm / pipAdvisory LookupIaC ConfigsDockerfile / K8sBest Practice RulesSecretsAPI Keys / TokensRegex DetectionUnified JSON / SARIF / Table ReportExit Code 1 = Block Deployment
Four distinct analysis layers in container image scanning with Trivy produce a unified security report that can block insecure deployments.

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:

  1. 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.
  2. Exploit availability: Does a public exploit exist? Check NVD, Exploit-DB, and vendor advisories. Theoretical vulnerabilities without weaponized exploits allow more remediation time.
  3. 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.
Vulnerability Triage Decision TreeFinding DetectedIs Code Path Reachable?NOYESLow Priority / DocumentCheck Exploit AvailabilityRemediate or MitigateBlock Deploy Until Resolved
Systematic triage prevents both alert fatigue and missed critical vulnerabilities during container image scanning with Trivy.

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.

FeatureTrivyGrypeSnykDocker Scout
LicenseApache 2.0 (OSS)Apache 2.0 (OSS)Proprietary (Free tier)Proprietary (Docker Hub tied)
DB UpdatesHourly, offline-capableContinuous, SBOM-basedCloud-dependentDocker Hub integrated
Scan TargetsImages, FS, Repo, IaC, SecretsImages, SBOMs onlyCode, Images, IaC, CloudImages, runtime insights
CI IntegrationNative GitLab/GitHub/JenkinsAnchore ecosystemAll major platformsDocker Build Cloud
False Positive RateModerateLower (SBOM precision)Lowest (curated DB)Moderate
Cost (2026)Free foreverFree forever$52/dev/month (Team)Free (limited) / $11/mo Pro
Best ForBudget-conscious, air-gappedSBOM-first workflowsEnterprise complianceDocker-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.

Monolithic BuildMulti-Stage BuildOS + Build Tools + Dev DepsSource Code + Node ModulesCompiler + Test FrameworksRuntime App + Prod Deps~450 CVEs TypicalImage Size: 1.2 GBBuild Stage (Discarded)Asset Compile (Discarded)Alpine + Runtime Only~45 CVEs TypicalImage Size: 180 MB
Multi-stage builds dramatically reduce the vulnerability surface exposed during container image scanning with Trivy by discarding build-time dependencies.

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.

Frequently Asked Questions

Trivy is an open-source vulnerability scanner for container images, filesystems, and Git repositories that detects OS package and application dependency CVEs.

Trivy is free and open-source under Apache 2.0 license; enterprise support via Aqua Security starts at approximately USD 5,000/year (NPR 670,000).

Run Trivy on every pull request and before production deployment to catch vulnerabilities early without blocking developer feedback loops unnecessarily.

On Ubuntu 24.04, add the Aqua Security apt repository and install via apt-get install trivy. Alternatively, download the static binary from GitHub releases for air-gapped environments. In my experience managing Laravel deployment servers, the apt method simplifies updates across multiple developer machines and staging environments sharing the same base configuration. Verify installation with trivy version to confirm the latest stable release is active before integrating into your workflow.

Yes, Trivy supports authentication via environment variables or Docker config.json without embedding secrets in scan commands. Set TRIVY_USERNAME and TRIVY_PASSWORD for basic auth, or mount your existing Docker credentials file. On production infrastructure I maintain, we pass registry tokens through CI/CD secret variables rather than storing them on disk. This prevents credential leakage in build logs while allowing automated scans of proprietary application images hosted on private ECR or Harbor registries.

Trivy covers OS packages, language dependencies, misconfigurations, and secrets in one tool, while Grype focuses primarily on SBOM-based vulnerability matching. Trivy generally has broader ecosystem coverage for PHP Composer, Node npm, and Python pip dependencies out of the box. In practice, I prefer Trivy for full-stack Laravel and WooCommerce projects because it catches both base image CVEs and application-level issues simultaneously. Grype excels when you already generate SBOMs with Syft and want specialized policy enforcement.

Create a .trivyignore.yaml file listing CVE IDs with justification comments and expiration dates. Pass this file using the --ignorefile flag during scans. For legal-tech portals handling sensitive documents, I document each ignored vulnerability with risk acceptance rationale and review quarterly. This maintains audit trails for compliance while preventing known non-issues from breaking deployments. Never ignore vulnerabilities without documented business justification and scheduled reassessment dates to prevent security debt accumulation.

Yes, download the vulnerability database manually using trivy db download and transfer to air-gapped systems. Configure TRIVY_CACHE_DIR to point to the offline database location. On isolated Nepal government infrastructure projects, I pre-download databases weekly and distribute via internal artifact storage. Note that offline databases become stale quickly; schedule regular updates even in disconnected environments. Scans will fail gracefully if no database exists, so verify cache integrity before relying on offline mode for compliance reporting.

A standard Laravel application image with PHP 8.3 and Composer dependencies typically scans in 15-45 seconds depending on layer count and database freshness. Base Alpine images scan faster than Debian-based images due to fewer packages. In production pipelines I manage, parallelizing Trivy with unit tests keeps total CI time under five minutes. First scans after database updates take longer as metadata downloads occur. Use --skip-dirs to exclude vendor directories already covered by lockfile analysis for additional speed gains.

Block CRITICAL and HIGH severity vulnerabilities with available fixes immediately. Allow MEDIUM only with documented exceptions and remediation timelines. LOW severities should trigger warnings but not failures unless regulatory requirements demand zero-tolerance. For eCommerce platforms processing payments via eSewa or Khalti, I enforce strict CRITICAL/HIGH gates while maintaining a tracked backlog for MEDIUM issues. Context matters: a CVE in an unused library differs from one in your web server. Always pair automated gates with human review for edge cases.

Add a trivy-container job in your .gitlab-ci.yml using the aquasecurity/trivy-action or official Docker image. Configure it to run after docker-build stage, scanning the built image tag before pushing to registry. Use artifacts to store JSON reports for downstream jobs. On sister sites sharing Deployer 7 pipelines, we fail merges on CRITICAL findings but allow manual override for accepted risks. Cache the Trivy database between runs using GitLab's caching mechanism to reduce scan times significantly across frequent commits.

Yes, Trivy identifies end-of-life PHP versions and unsupported OS releases as misconfigurations alongside traditional CVEs. It flags PHP 8.1 on Ubuntu 22.04 when newer point releases exist. During upgrades from Laravel 10 to 12, I use these findings to prioritize base image rebuilds before framework migration. The scanner cross-references installed versions against upstream support matrices, providing clear remediation guidance. This catches drift where developers pin old tags for stability but miss security patches accumulating over months of active development.

Legacy Magento 2.4.x installations often contain unpatchable dependencies requiring exception management. Create tiered ignore policies separating core platform issues from custom module vulnerabilities. Prioritize fixing custom code first since platform patches may require major version jumps. On older eCommerce migrations, I accept certain base image CVEs temporarily while planning incremental upgrades. Document each exception with business impact assessment and target resolution date. Use Trivy's --timeout flag generously for large monolithic images to prevent premature scan termination during dependency analysis.

Most failures stem from missing database downloads, incorrect registry authentication, or scanning wrong image tags after retagging. Another frequent issue is scanning development images instead of production artifacts, missing runtime-only vulnerabilities. Ensure your CI scans the exact digest deployed, not just the mutable latest tag. On shared EC2 infrastructure, I've seen permission errors when Trivy cache directories aren't writable by CI runners. Always validate scan targets match deployment manifests and test authentication flows in staging before enforcing gates in production pipelines.

Trivy offers comparable PHP vulnerability coverage to Snyk without licensing costs for teams under budget constraints. Snyk provides superior fix recommendations and IDE integration but requires paid tiers for CI usage beyond small teams. For Nepal-based agencies billing in NPR, Trivy's zero-cost model makes comprehensive scanning accessible where Snyk's USD pricing creates friction. Both detect Composer vulnerabilities effectively. I recommend Trivy for most Laravel shops reserving Snyk for enterprises needing advanced prioritization, team management features, or regulatory compliance reporting that justifies the additional investment.

Share this article

Quick Contact Options
Choose how you want to connect me: