GitLab’s Container Scanning feature is surprisingly powerful, but it relies on a specific understanding of how it interacts with your container registry and build process.
Let’s see it in action. Imagine you have a Dockerfile like this:
FROM ubuntu:20.04
RUN apt-get update && apt-get install -y --no-install-recommends some-vulnerable-package && rm -rf /var/lib/apt/lists/*
COPY . /app
WORKDIR /app
CMD ["./your-app"]
And a .gitlab-ci.yml that builds and pushes this image, then triggers a scan:
stages:
- build
- scan
variables:
IMAGE_TAG: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
build_image:
stage: build
image: docker:latest
services:
- docker:dind
script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
- docker build -t $IMAGE_TAG .
- docker push $IMAGE_TAG
container_scanning:
stage: scan
image: registry.gitlab.com/security-products/container-scanning:latest
variables:
# This is crucial: tell the scanner WHICH image to check
# It needs to be accessible from the GitLab Runner
SCAN_IMAGE: $IMAGE_TAG
# If you push to a private registry, you might need these
# PRIVATE_REGISTRY_USER: $CI_REGISTRY_USER
# PRIVATE_REGISTRY_PASSWORD: $CI_REGISTRY_PASSWORD
# PRIVATE_REGISTRY: $CI_REGISTRY
script:
- echo "Scanning image: $SCAN_IMAGE"
- echo "This will run Trivy under the hood."
- /analyzer run
artifacts:
reports:
container_scanning: gl-container-scanning-report.json
When this pipeline runs, the build_image job first logs into your GitLab Container Registry, builds the Docker image using the Dockerfile, and pushes it. The container_scanning job then uses a specialized GitLab image (registry.gitlab.com/security-products/container-scanning:latest). This image contains a vulnerability scanner (by default, Trivy). The SCAN_IMAGE variable tells this scanner which image to analyze. The /analyzer run command executes the scanner. If vulnerabilities are found, a gl-container-scanning-report.json file is generated, which GitLab then displays in the Merge Request interface and pipeline view.
The core problem this solves is understanding what software components are inside your container images and whether those components have known security vulnerabilities. Container images are often built from base images and add layers of installed packages. Without scanning, you have no visibility into the security posture of these components. GitLab Container Scanning leverages tools like Trivy to inspect the image’s layers, identify installed packages (like apt packages in Debian/Ubuntu, or RPMs in Alpine/CentOS), and cross-reference their versions against vulnerability databases (CVEs).
Internally, the registry.gitlab.com/security-products/container-scanning:latest image acts as a wrapper. It orchestrates the execution of a chosen scanner (like Trivy). It pulls the specified SCAN_IMAGE from your registry, passes it to the scanner, and then processes the scanner’s output into the standardized gl-container-scanning-report.json format. The script section in your .gitlab-ci.yml is minimal because the heavy lifting is done by the analyzer run command within the specialized scanner image. The artifacts:reports:container_scanning directive is key for GitLab to recognize and display the findings.
The SCAN_IMAGE variable is the linchpin. It must point to an image that is accessible from the GitLab Runner executing the scanning job. If your build_image job pushes to $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA, then setting SCAN_IMAGE: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA is the correct way to ensure the scanner can find and pull the exact image you built. If you’re using a different registry or a private one, you’ll need to configure PRIVATE_REGISTRY_USER, PRIVATE_REGISTRY_PASSWORD, and PRIVATE_REGISTRY variables accordingly. The scanner will then use these credentials to authenticate and pull the image.
Many users overlook the fact that the container scanning job itself needs to be able to access the image it’s supposed to scan. If your build_image job pushes to a private registry that requires authentication, and your container_scanning job doesn’t provide those same authentication details (via PRIVATE_REGISTRY_USER/PASSWORD), the scanner will fail to pull the image, leading to a job failure without necessarily indicating a vulnerability. The scanner image acts independently from the build image, so it needs its own context for registry access.
The next concept you’ll want to explore is custom vulnerability definitions and integrating other security tools into your GitLab CI pipeline.