Skip to main content
Cloud DevOps Solutions
Get a quote Talk to an engineer

CI/CD Pipelines

Container Image Build, Trivy Scanning and Registry Push in CI

A container image pipeline builds the image once, scans it with Trivy for vulnerabilities and misconfigurations, and pushes it to a registry such as ECR, ACR or Google's registry only if it passes policy. We tag by commit and digest, fail builds on exploitable severities, and record scan results and an SBOM for each image.

Key takeaways

  • Scan after build and before push, so a vulnerable image never becomes a deployable artefact.
  • Severity thresholds need tuning: failing on every low finding trains teams to ignore the scanner.
  • A registry scan and a CI scan complement each other, because new vulnerabilities appear after an image ships.
  • Small, pinned, multi-stage images have fewer findings and a smaller attack surface.
  • Deploy by immutable digest, not by a mutable tag like latest.

The image is what you actually ship

Your source code is not what runs in production. A container image is: your code plus an operating system layer, a runtime, dozens of libraries and whatever else ended up in the build. Most of those components are not written by you, and vulnerabilities in them are discovered constantly. If the pipeline simply builds and pushes, you inherit those risks silently.

An image stage that builds, scans and publishes under clear rules is one of the most valuable parts of a CI/CD pipeline. It catches known problems before deployment, produces a trail for audits, and keeps the registry tidy. Here is how we build it.

What good looks like

  • Build once. The image tested is the image deployed, referenced by digest.
  • Small and pinned. Multi-stage builds and slim, version-pinned base images keep both size and findings low.
  • Scanned before publishing. A vulnerable image does not reach the registry’s deployable tags.
  • Policy with exceptions. Thresholds are explicit, and accepted risks are recorded with an owner and expiry.
  • Continuously rechecked. Images are rescanned as new vulnerabilities are published.
  • Traceable. Every image links back to a commit, a pipeline run and an SBOM.

Building a clean image

Scanning cannot rescue a poor image. Findings are mostly a function of what is inside, so we start with the Dockerfile:

  • A multi-stage build keeps compilers and build tools out of the runtime image.
  • A minimal base such as a slim or distroless image removes packages you never use.
  • A non-root USER limits what a compromised process can do.
  • Pinned base image versions (and ideally digests) make builds reproducible. Floating tags mean the same Dockerfile produces different images on different days.

Caching these builds properly is covered in our build caching strategy.

The scan stage

Trivy is our usual scanner: open source, fast, and covering OS packages, language dependencies, misconfiguration and secrets in a single tool. Other scanners exist, and the shape of the pipeline stays the same if you use another.

name: image
on:
  push:
    branches: [main]

permissions:
  id-token: write
  contents: read

jobs:
  build-scan-push:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: docker/setup-buildx-action@v3

      - name: Build (load locally, do not push yet)
        uses: docker/build-push-action@v6
        with:
          context: .
          load: true
          tags: app:${{ github.sha }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

      - name: Scan with Trivy
        uses: aquasecurity/[email protected]
        with:
          image-ref: app:${{ github.sha }}
          severity: CRITICAL,HIGH
          ignore-unfixed: true
          exit-code: "1"
          format: table

      - uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123456789012:role/ci-ecr-push
          aws-region: eu-west-1

      - uses: aws-actions/amazon-ecr-login@v2
        id: ecr

      - name: Tag and push
        run: |
          docker tag app:${{ github.sha }} ${{ steps.ecr.outputs.registry }}/app:${{ github.sha }}
          docker push ${{ steps.ecr.outputs.registry }}/app:${{ github.sha }}

A few choices are worth explaining. The image is built and loaded locally first, so nothing is pushed until the scan passes. ignore-unfixed: true ignores vulnerabilities that have no available fix, since failing the build on something nobody can fix is pointless, though we still report them. severity: CRITICAL,HIGH sets the blocking threshold. The version of the scan action is pinned, and we recommend pinning to a commit SHA for third-party actions in sensitive pipelines. Check the action’s current release when adopting this.

Authentication uses OIDC rather than stored keys, as described in CI/CD secrets management. The same flow works for ACR (through a federated service principal) and Google’s registry (through workload identity federation).

Handling findings sensibly

A scanner that blocks every build over a medium finding in a library you cannot upgrade will be switched off within a month. We agree a policy up front:

  • Block: critical and high, fixable.
  • Report: everything else, as a build summary or ticket.
  • Exceptions: recorded in a .trivyignore file or a VEX statement with a reason, owner and review date, reviewed in pull requests like code.

Also scan the repository and infrastructure files (trivy fs, trivy config) to catch Dockerfile and Kubernetes misconfiguration, such as running as root or missing resource limits.

Registry hygiene

The registry is part of the system. We configure:

  • Immutable tags, so a tag cannot be overwritten with different content.
  • Lifecycle policies to expire untagged and old images, which controls storage cost and clutter.
  • Least-privilege push and pull roles: CI can push, nodes can pull, humans rarely do either.
  • Registry-side scanning where available, such as ECR scanning, as a second signal after the image ships.
  • Cross-region or cross-account replication when your deployment topology needs it.

For clusters, pulling by digest matters. A tag points at whatever was last pushed, whereas a digest never changes. Our Kubernetes deployment work deploys by digest, and if you run on AWS, our Amazon EKS guide covers the registry and node side.

SBOMs and signing

A software bill of materials lists what is inside an image. Trivy or Syft can generate one in CycloneDX or SPDX format, and storing it alongside the image helps when the next widely publicised vulnerability appears and you need to know within minutes which services are affected. Signing images with Cosign and enforcing signatures at admission goes one step further, proving provenance. We recommend adding both after scanning is stable, rather than trying to adopt everything at once.

Decisions and trade-offs

Scan time versus coverage. Scanning adds a minute or so. Caching the vulnerability database and scanning only on merge to main (with a lighter check on pull requests) keeps feedback fast.

CI scan versus registry scan. CI scanning stops bad images at the gate. Registry or scheduled scanning catches those that aged badly. You want both.

Hardened images. Distroless or minimal images reduce findings and attack surface but are harder to debug, since there is no shell. Ephemeral debug containers in Kubernetes mitigate this.

False positives. Scanners report vulnerabilities in code paths you do not use. Reachability analysis is imperfect, so the policy must allow justified exceptions rather than pretending the scanner is always right.

When not to bother yet. A prototype with no customers does not need SBOMs and signing. A basic scan on the main branch is still cheap insurance.

Common failure modes

  • Pushing before scanning, which makes the scan advisory.
  • latest tags in deployments.
  • Secrets baked into layers, retrievable by anyone with pull access.
  • Alert fatigue from unfiltered findings.
  • One scan at build time, never again, so old images quietly decay.
  • Unbounded registry growth with no lifecycle rules.

Delivery and handover

We standardise the Dockerfile pattern, build the scan and push stage as a shared template, configure registry settings in code (usually Terraform), and agree the severity policy with your security contact. You receive documentation for adding exceptions and a scheduled job that reports on images already in use. If you want to see how this fits with the rest of your delivery, look at the parent service overview or get in touch.

How to tell it is working

Measure the number of critical and high findings in deployed images, median age of base images, the share of deployments referencing a digest, time to patch after a new critical disclosure, and the volume of active exceptions. Healthy signs are shrinking image sizes, a short exception list and fast turnaround on base image updates.

References

How we deliver it

  1. 01

    Standardise the image build

    Multi-stage Dockerfiles, pinned base images, a non-root user and a tight build context give a clean, small starting point.

  2. 02

    Build once and tag precisely

    Images are tagged with the commit SHA and carry labels for source and build, and the pipeline records the digest.

  3. 03

    Scan with Trivy

    Trivy checks OS packages, language dependencies, misconfigurations and embedded secrets, with a severity policy you agree.

  4. 04

    Handle exceptions properly

    Accepted findings go in a reviewed ignore file with an owner and expiry date, not a silent skip.

  5. 05

    Push to your registry

    Authenticated by short-lived credentials, the image is pushed to ECR, ACR or GCR with immutable tags and lifecycle rules.

  6. 06

    Keep scanning after release

    Scheduled and registry-based scans catch newly published vulnerabilities in images already running.

Tools and technologies

TrivyDocker BuildKitAmazon ECRAzure Container RegistryGoogle Artifact RegistryCosignSyftGitHub ActionsGitLab CIKubernetes admission policiesSBOM (CycloneDX, SPDX)

Frequently asked questions

What does Trivy scan in a container image?
Trivy inspects operating system packages, language dependencies such as npm, pip and Go modules, infrastructure misconfigurations in files like Dockerfiles and Kubernetes manifests, and embedded secrets. It can also produce an SBOM. Findings come with severity ratings and, where available, the version that fixes them.
Should a vulnerability scan fail the build?
Fail on findings that are serious and fixable, typically critical and high severity with an available fix. Failing on everything produces constant red builds and pressure to disable the scanner. Report lower severities without blocking, and review them on a schedule.
How do you push an image to ECR from GitHub Actions?
Authenticate to AWS with OIDC and a role scoped to ECR push, log in to the registry using the AWS login action, then build and push with Docker. Using federation avoids storing AWS keys in GitHub. Repositories should have immutable tags and lifecycle policies to remove old images.
Why scan again after the image is in the registry?
New vulnerabilities are published daily. An image that was clean at build time can become vulnerable without any change. Scheduled rescans, or the registry's own scanning, flag running images that need a rebuild with updated base layers, which a one-time CI scan cannot do.
Do we need to sign container images?
Signing is worthwhile once you want to prove an image came from your pipeline and was not altered. Tools such as Cosign sign the image digest, and a cluster admission policy can reject unsigned images. It is an extra step, so teams often add it after scanning and promotion are stable.

Talk to an engineer about trivy container image scanning ci

Book a free 30-minute call. We review your current setup and tell you honestly whether and how we can help.

Written by Saddique Khan, DevOps / SRE / Cloud Consultant Engineer. Last updated 2026-10-08.