Why Images Need Their Own Quality Gates
Summary
- A container image is a build artifact, and like any artifact it can carry defects that your application test suite never sees.
- Insight: unit and integration tests exercise your code, not your image. A green test suite tells you nothing about a broken
USER, a missing CA bundle, or a 900 MB layer. - Key Concepts: Dockerfile linting, layer efficiency, structure tests, smoke tests, provenance and scanning as distinct gates.
- When to Use: on every image you ship, from the first CI run - the cost of adding gates grows as the fleet grows.
- Limitations/Trade-offs: gates add pipeline time and can produce false positives, so they need tuned rules and agreed exceptions.
- Related Topics: hadolint linting, dive efficiency analysis, container-structure-test, post-build smoke tests.
Foundations
An image is code plus a filesystem plus metadata, frozen into an immutable, content-addressed artifact.
Your CI already gates the code. The image wraps that code in a base OS, a user, an entrypoint, environment variables, and dozens of layers you did not write by hand.
None of that surface is covered by your application tests.
A Go binary can pass 100% of its tests and still ship in an image that runs as root, lacks ca-certificates, and exposes the wrong port.
The mental model is simple: treat the Dockerfile and the resulting image as first-class source that deserves its own gates, the same way you gate application code with linters, type checks, and tests.
Quality gates for images fall into five families.
Linting checks the Dockerfile source for known-bad instructions before anything is built.
Efficiency analysis measures the built image for wasted space, duplicated files, and layers that leak secrets or cache.
Structure tests assert facts about the filesystem and metadata: this file exists, this port is declared, this user is non-root.
Smoke tests actually run the container and confirm it boots and answers.
Provenance gates - scanning and signing - confirm the image is free of known CVEs and is the artifact you think it is.
Mechanics & Interactions
Each gate runs at a different moment and reads a different input.
Linting is static and runs first, on the Dockerfile text, so it fails in seconds without a build.
hadolint DockerfileEfficiency and structure gates run after docker build, because they inspect the produced image and its layers.
docker build -t app:ci .
dive --ci app:ciSmoke tests run last among the fast gates, because they need a running container.
docker run --rm -d --name app app:ci
curl -fsS http://localhost:8080/healthzThe gates form a funnel: cheap and fast checks fail early, expensive checks run only on images that already passed the cheap ones.
This ordering matters because a Dockerfile lint failure should never wait behind a two-minute build.
Scanning sits alongside structure tests, reading the same built image but asking a different question - not "is it correct?" but "is it vulnerable?".
trivy image --exit-code 1 --severity HIGH,CRITICAL app:ciSigning runs at the end, on the pushed digest, so the signature covers exactly what you promoted.
cosign sign --yes registry.example.com/app@sha256:<digest>A key interaction: these gates only compose well if every stage refers to the image by digest, not by a floating tag.
Tags move; digests do not. Gate an image by tag and you may scan one build and deploy another.
Advanced Considerations & Applications
The runtime split shapes what you can and cannot test locally.
You build and smoke-test with Docker Engine, but pods on Kubernetes nodes are run by containerd through the CRI, not by Docker.
That means a Docker-only smoke test proves the image starts, but not that it satisfies your cluster's Pod Security Standards or admission policies.
For that, add a cluster-side gate: an admission controller such as Kyverno or a Gatekeeper policy that rejects images without a non-root user, a digest reference, or a valid signature.
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: require-non-root
spec:
validationFailureAction: Enforce
rules:
- name: check-runasnonroot
match:
any:
- resources:
kinds: ["Pod"]
validate:
message: "Containers must set runAsNonRoot: true"
pattern:
spec:
containers:
- securityContext:
runAsNonRoot: trueThis is the last gate, and it is the only one your developers cannot bypass by editing a pipeline file.
The economic argument for gates is fleet leverage.
A single hand-audited image is cheap to review by eye; ten thousand images across dozens of teams are not.
Gates encode a review once and apply it to every future build without human attention.
They also shift defects left: a hadolint warning costs seconds, the same defect found in production costs an incident.
Common Misconceptions
"If my app tests pass, the image is fine." App tests never see the base image, the user, the CA bundle, or the layer layout. Those are exactly where image defects live.
"Scanning is enough." Scanning finds known CVEs. It does not catch a root user, a 900 MB image, a missing healthcheck, or a wrong port.
"Linting the Dockerfile is cosmetic." Rules like pinning versions or avoiding latest directly affect reproducibility and supply-chain safety, not style.
"Docker runs my containers in the cluster, so a Docker smoke test is definitive." Nodes run containerd via the CRI. A Docker smoke test is necessary but not sufficient - cluster admission is the authoritative gate.
"Gates just slow the pipeline down." Ordered correctly, cheap gates fail in seconds and save the minutes a full build-and-deploy would otherwise waste.
FAQs
Do I need every gate on day one? Start with linting and a smoke test, then add efficiency, structure, and scanning as the fleet grows.
Where should gates run? In CI for fast feedback, plus a cluster-side admission policy that developers cannot skip.
How do I avoid false positives blocking merges? Maintain an agreed exceptions file for lint rules and tune scanner severity thresholds per team.
Should a failing gate block the merge or just warn? Security and correctness gates should block; efficiency thresholds can start as warnings and harden over time.
Can these gates run without Docker in CI? Yes - BuildKit, and structure/scan tools accept OCI images, so the runtime need not be the Docker CLI.
How do gates relate to Kubernetes? In-cluster admission is the enforcement backstop; the CI gates catch the same issues earlier and cheaper.
Related
- Linting Basics
- dive & Image Efficiency
- Container Structure Tests
- Smoke Tests After Build
- Linting Best Practices
Stack versions: This page was written for Kubernetes 1.36.2, Docker Engine 29.6.1 (BuildKit default), containerd (CRI runtime on nodes), Helm 3, Compose v2, Argo CD (latest - verify at build), and Gateway API (GA - verify controller support at build).