The difference to Trivy and integration into the merge request widget
A Docker image is only as secure as its base image and the system packages installed inside it, and that is exactly where many vulnerabilities hide that neither SAST nor Dependency Scanning catch, since both focus on application code and language packages. GitLab's Container Scanning closes this gap by checking the fully built image directly in the pipeline against a vulnerability database. This article covers the integration, the difference to a standalone tool like Trivy, and how the results end up in the merge request widget.
Table of Contents
- 1. Why Docker Images Need Their Own Scanning Category
- 2. Wiring the Container Scanning Template Into an Existing Pipeline
- 3. How the Analyzer Works Under the Hood
- 4. The Difference to Trivy as a Standalone Tool
- 5. Results in the Merge Request Widget
- 6. Scanning Images From External Registries
- 7. The Right Base Image Strategy as the Most Effective Lever
- 8. Thresholds, Exceptions and Handling Unfixable Findings
- 9. Best Practices and a Comparison of the Approaches
- 10. Summary
- 11. FAQ
1. Why Docker Images Need Their Own Scanning Category
A typical production image consists of much more than the application code itself: it contains a base operating system such as Debian or Alpine, system libraries, a package manager layer, and often additional tools that were installed during the build process but never removed again. Every one of these components can carry known security vulnerabilities, regardless of how clean the actual application code is. SAST only checks your own source code, Dependency Scanning only checks language packages such as Composer or npm dependencies, neither looks into the operating system layer of the final image.
Container Scanning addresses exactly this gap and analyzes the built image as a whole, including every layer, against a continuously updated database of known CVEs for operating system packages. This matters especially because many base images run unchanged in production for months, while new vulnerabilities are published in the background for exactly the package versions contained inside them. An image that looked secure at build time can carry several known, publicly documented vulnerabilities just a few weeks later, without anything having changed in your own code or Dockerfile.
2. Wiring the Container Scanning Template Into an Existing Pipeline
Integration happens through include:template with Jobs/Container-Scanning.gitlab-ci.yml and requires the image to be scanned to already have been built and pushed to a registry in an earlier stage, typically the project's own GitLab Container Registry. The variable CS_IMAGE tells the scanner which specific image to analyze; by default it falls back to the values of CI_APPLICATION_REPOSITORY and CI_APPLICATION_TAG, which derive from the project name and the commit SHA if no custom values are set.
The scan job itself needs access to the registry that holds the image, which works automatically via CI job token authentication for the project's own GitLab registry, but for an external registry needs to be explicitly configured through CS_REGISTRY_USER and CS_REGISTRY_PASSWORD. A common mistake is running the scan job before the image has actually been pushed, causing the scanner to analyze a nonexistent or stale image. The needs directive ensures the scan job reliably only starts after the build and push job succeeds.
# .gitlab-ci.yml
stages:
- build
- security
- deploy
build-image:
stage: build
script:
- docker build -t "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA" .
- docker push "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA"
include:
- template: 'Jobs/Container-Scanning.gitlab-ci.yml'
container_scanning:
stage: security
variables:
CS_IMAGE: "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA"
needs: ["build-image"]
3. How the Analyzer Works Under the Hood
By default, GitLab's Container Scanning template internally uses Trivy as its analyzer engine, ever since GitLab switched away from the earlier Grype default. The image itself is never executed, it is analyzed statically: the scanner extracts the layers, identifies installed package versions through the respective package manager metadata, such as dpkg for Debian based images or apk for Alpine, and matches that list against the vulnerability database. This whole process runs entirely inside the pipeline, without the image ever having to be deployed or started anywhere.
Depending on configuration, the analyzer also detects language package vulnerabilities inside the image in addition to plain operating system package checks, for instance when a composer.lock or package-lock.json ends up copied into the final image, which occasionally happens unintentionally with multi stage Docker builds. In such cases Container Scanning partially overlaps with Dependency Scanning, which is usually unproblematic in practice, since both point to the same underlying vulnerability and the results are merged accordingly in the security dashboard.
4. The Difference to Trivy as a Standalone Tool
Trivy can also be used entirely independently of GitLab's templates, as a plain CI job, either as a Docker image or as a directly installed binary, with full access to every Trivy specific configuration option, output format and scan mode, not all of which the GitLab template exposes. For teams that already have an established Trivy based toolchain, or that want to cover special requirements such as scanning infrastructure as code files in the same tool, standalone use is often the more flexible choice.
The decisive advantage of the GitLab template, on the other hand, lies in seamless integration: results automatically land in the merge request widget and the security dashboard, without a custom script having to convert Trivy's JSON output into GitLab's report format. With a standalone Trivy job this conversion would have to happen manually, for example via trivy image --format template with a matching GitLab compatible template, which adds ongoing maintenance work. For most teams this integration advantage clearly outweighs the flexibility gain of a standalone Trivy setup.
# Standalone Trivy job as an alternative (without GitLab report integration)
trivy image --format table --severity CRITICAL,HIGH \
--exit-code 1 "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA"
5. Results in the Merge Request Widget
After a successful scan run, the job stores a container scanning report as an artifact, which GitLab automatically detects and shows in the merge request widget as its own section alongside SAST and Dependency Scanning findings. Every CVE found appears there with its severity, the affected package, the installed version and, if available, the version in which the vulnerability was fixed, so reviewers can tell at a glance whether a simple base image update would resolve the issue.
The diff view is particularly valuable: GitLab automatically compares the findings of the current merge request against the scan result of the target branch and marks which vulnerabilities are newly introduced versus which were already present in the base image before. This prevents a merge request from being wrongly blamed for legacy issues that already existed in the image long before the change in question, and directs attention specifically toward genuinely newly introduced risk.
6. Scanning Images From External Registries
Not every team builds and stores its images exclusively in the project's own GitLab Container Registry. Base images frequently come from Docker Hub, production images sometimes live in Amazon ECR or Google Artifact Registry, and Container Scanning needs to be able to work with such external sources too. For an external registry, credentials are stored via CS_REGISTRY_USER and CS_REGISTRY_PASSWORD as protected CI/CD variables, while CS_IMAGE holds the full registry URL including the tag, instead of relying on CI_REGISTRY_IMAGE as with the project's own registry.
It matters here to scope the credentials as narrowly as possible, ideally read only access to exactly the one repository that needs to be scanned, rather than administrative access to the entire registry. For self hosted, private registries with a self signed certificate, the DOCKER_INSECURE variable or a manually provided CA certificate may additionally be needed so the scan job can successfully establish the TLS connection, without undermining the pipeline's overall security by blanket disabling certificate verification.
# .gitlab-ci.yml: scanning an image from an external registry
container_scanning:
variables:
CS_IMAGE: "registry.example.com/team/app:1.4.2"
CS_REGISTRY_USER: "$EXTERNAL_REGISTRY_USER"
CS_REGISTRY_PASSWORD: "$EXTERNAL_REGISTRY_PASSWORD"
7. The Right Base Image Strategy as the Most Effective Lever
By far the most effective measure against Container Scanning findings is not scanning itself, but the deliberate choice and upkeep of the base image. Slim, specialized images such as Alpine based variants or Google's distroless images ship with far fewer installed packages from the start, and therefore a smaller attack surface than a full Ubuntu or Debian image with every standard tool included. Wherever the application allows it, a slimmer base image usually reduces the number of findings far more drastically than any subsequent scanner configuration.
It is equally important to update base images regularly, instead of pinning a working tag once and leaving it untouched for years. A weekly rebuild triggered via a Scheduled Pipeline, using the current base image patch level, automatically pulls in every security fix published for the operating system layer since then, without requiring any change to your own Dockerfile. This combination of a slim base image and regular rebuilds usually reduces the number of Container Scanning findings by an order of magnitude in practice.
8. Thresholds, Exceptions and Handling Unfixable Findings
Not every reported vulnerability already has an available fix version, especially for older or less actively maintained base images. For such cases, GitLab offers the option to mark findings via a .gitlab/security-policies file or directly in the security dashboard as a provisionally accepted risk, with an expiration date, so the exception does not persist unnoticed indefinitely but has to be reassessed regularly.
For production critical images, a scan result policy is also recommended, blocking deployments with unresolved critical findings that lack a documented exception, combined with a clearly defined escalation window, for example requiring a critical finding to be either fixed or documented as a justified exception within 72 hours. This combination of technical enforcement and organizational process prevents Container Scanning from becoming mere cosmetics in the merge request widget without actually changing behavior.
# .gitlab-ci.yml: only Critical and High findings should fail the job
container_scanning:
variables:
CS_SEVERITY_THRESHOLD: "HIGH"
9. Best Practices and a Comparison of the Approaches
In practice it works well to firmly anchor Container Scanning as a mandatory step between the image build and the deploy stage, rather than treating it as an optional, easily overlooked extra job. Combined with a clear base image strategy, regular rebuilds and a documented exception process for unfixable findings, this results in a process that handles container vulnerabilities systematically rather than by accident.
The choice between the GitLab template and a standalone Trivy setup ultimately depends on how heavily native merge request integration is weighted against the full configuration flexibility of a standalone tool. The table below compares both approaches by their key properties to make the decision easier for your own project.
| Criterion | GitLab Container Scanning Template | Standalone Trivy |
|---|---|---|
| Setup | A single include:template entry | Custom job including output conversion |
| MR widget integration | Automatic | Only with manual report conversion |
| Configuration flexibility | Limited to CS_ variables | Full access to every Trivy option |
| Security dashboard | Automatically aggregated | Not without extra effort |
| IaC scanning in the same tool | Not included | Possible via trivy config |
Mironsoft
CI/CD pipelines, zero-downtime deployments and release automation
Deployments that run without downtime and without the nail-biting?
We review existing GitLab pipelines for fragile deployment steps and missing safeguards, then build a release process with zero-downtime deployments, automated checks and a rollback you can actually trust in an emergency.
Pipeline Review
Checking an existing .gitlab-ci.yml for fragility, missing stages and security gaps.
Zero-Downtime Deployment
Building symlink releases, health checks and rollback strategies for Magento stores.
CI/CD Automation
Connecting tests, security scans and deployments into one reliable pipeline.
10. Summary
GitLab Container Scanning: Key Takeaways
A category of its own
Container Scanning checks the operating system layer of an image, which SAST and Dependency Scanning do not cover.
Simple integration
include:template plus CS_IMAGE is enough, needs ensures correct ordering after the image push.
Base image is the lever
A slim, regularly rebuilt base image reduces findings more than any scanner configuration.
Diff instead of full picture
GitLab shows newly introduced findings separately from those already present in the base image.