GitLab Container Scanning: Checking Docker Images for Vulnerabilities Right in the Pipeline
AI generated
CI/CD
.yml
GitLab · CI/CD · Container Security
GitLab Container Scanning for Docker Images in the Pipeline
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.

16 min read Container Scanning Docker Security Trivy Vulnerability Management

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.

11. FAQ: GitLab Container Scanning: Key Takeaways

1What does GitLab Container Scanning actually check?
It analyzes a fully built Docker image, including every layer, for known vulnerabilities in installed operating system packages, partially supplemented by language package findings if matching manifest files are present in the final image.
2Does the image need to be pushed to a registry before scanning?
Yes, the scan job requires an image already pushed to a registry, referenced via the CS_IMAGE variable. The needs directive ensures the scan only starts after the push job has succeeded.
3Which analyzer does GitLab use internally for Container Scanning?
Trivy by default, following the switch away from the earlier Grype default. The analyzer extracts package metadata from the image layers and matches it against a vulnerability database.
4What is the main advantage of the GitLab template over standalone Trivy?
Seamless integration into the merge request widget and the security dashboard without manually converting Trivy's output into a compatible report format, which adds ongoing maintenance work with a standalone Trivy job.
5When does a standalone Trivy setup still make sense?
When full configuration flexibility is needed, for example special output formats, or when infrastructure as code files should also be scanned in the same tool, which the GitLab template does not cover.
6What is the most effective way to reduce the number of findings?
Through a slim base image, such as an Alpine or distroless variant with fewer installed packages, and through regular, for example weekly, rebuilds that automatically pick up the current security fixes of the base image.
7What happens when a vulnerability has no fix version yet?
Such findings can be documented as a provisionally accepted risk with an expiration date, so the exception gets reassessed regularly instead of persisting unnoticed indefinitely.
8How does GitLab distinguish new findings from pre-existing ones?
Through an automatic diff view in the merge request widget that compares the merge request's scan results with the target branch's scan result and separately marks newly introduced vulnerabilities.
9Can I configure only Critical findings to fail the job?
Yes, through the CS_SEVERITY_THRESHOLD variable, which defines a minimum severity above which the job is considered failed, for example HIGH for High and Critical findings.
10Does Container Scanning overlap with Dependency Scanning?
Partially, when language package manifests accidentally end up in the final image, for example with poorly separated multi stage Docker builds. Both analyses then show the same underlying finding, which is merged accordingly in the security dashboard.