Linting Best Practices
Treat the Dockerfile and its image as first-class source code that earns the same review rigor as your application.
This list gathers the practices that keep linting and image testing fast, honest, and enforceable across a growing fleet.
How to Use This List
Work top to bottom on a new repo, or pick the group that matches your current gap on an existing one.
Each practice is a bold rule followed by the reasoning, so you can lift the rule into a review checklist or a CODEOWNERS conversation.
Treat security and correctness practices as blocking gates and efficiency practices as thresholds you tighten over time.
A - Make Linting a Real Gate
- Run hadolint before the build, not after. Linting is static and returns in under a second, so it should fail a pull request long before an expensive build runs.
- Set an explicit failure threshold. Choose
errororwarningdeliberately with--failure-thresholdrather than accepting the default, and tighten it as the codebase cleans up. - Pin the linter version. Run hadolint from a pinned container tag so a tool upgrade cannot silently change which rules fire.
- Make the check required. A gate that does not block merge is documentation, not enforcement - mark it required in branch protection.
B - Curate Rules and Exceptions
- Keep a checked-in
.hadolint.yaml. Encode the team's standing exceptions and thresholds in the repo so the policy is versioned and reviewable, not tribal knowledge. - Justify every inline ignore. Use
# hadolint ignore=DLxxxxfor a single instruction and leave a comment explaining why it is safe, so reviewers see the exception in the diff. - Promote critical rules to errors. Use
override.errorto make rules like "never use:latest" hard failures rather than warnings. - Declare trusted registries. Set
trustedRegistriesso a build that pulls from an unapproved source fails at lint time.
C - Write Efficient, Reproducible Dockerfiles
- Pin base images by digest. Reference
FROM image:tag@sha256:...so every build and later scan or signature applies to the exact same bits. - Pin package versions and clean in the same layer. Combine
apt-get installwithrm -rf /var/lib/apt/lists/*in oneRUNso the cache never ships and rebuilds stay reproducible. - Use multi-stage builds. Keep compilers and dev dependencies in a build stage that is discarded, so the runtime image carries only what it needs.
- Maintain a tight
.dockerignore. Exclude.git,node_modules, and test fixtures so those bytes never enter the build context or a layer. - Measure layout with dive in CI. Add a
.dive-cipolicy with an efficiency floor and a wasted-bytes ceiling so regressions in image layout fail the pipeline.
D - Test the Built Image, Not Just the Source
- Assert structure with container-structure-test. Verify user, exposed ports, workdir, required files, and binary versions against a declarative spec so image drift cannot slip through.
- Confirm the container is non-root. Assert the effective UID and that
runAsNonRootwill hold, since a base image can silently resetUSERto root. - Smoke test every image. Run the image, poll its health endpoint in a bounded loop, and propagate the exit code so a container that never boots fails the build.
- Match the smoke path to the readiness probe. Curl the same
/healthzpath your Kubernetes readiness probe uses, so the test exercises the code the platform depends on.
E - Secure the Supply Chain
- Scan for CVEs and fail on severity. Run Trivy or Grype with
--exit-code 1 --severity HIGH,CRITICALso known-vulnerable images cannot be promoted. - Sign the pushed digest. Use cosign to sign by digest, not tag, so the signature covers exactly the artifact you promoted.
- Enforce at admission. Add a Kyverno or Gatekeeper policy that rejects images lacking a non-root user, a digest reference, or a valid signature, since that is the one gate developers cannot bypass.
- Remember the runtime boundary. Build and smoke test with Docker, but validate real behavior on the cluster where containerd runs pods via the CRI.
F - Keep the Gates Trustworthy
- Order gates as a funnel. Run cheap static checks first and expensive checks only on images that already passed, so failures surface fast and cheap.
- Reference images by digest across stages. Gate, scan, sign, and deploy the same digest so you never test one build and ship another.
- Tune out false positives quickly. A noisy gate gets ignored, so triage new findings promptly and record agreed exceptions rather than disabling the gate.
- Review Dockerfile changes like app code. Require the same approval, tests, and CODEOWNERS coverage for image changes as for application changes.
When You Are Done
You should have linting, efficiency, structure, smoke, and scanning gates running in order on every image, each with an owned threshold.
Exceptions should live in checked-in config with justifications, not scattered ad hoc suppressions.
The same digest should flow from build through scan and signature to deployment, with a cluster admission policy as the final backstop.
FAQs
Which gates should block merge versus warn? Security and correctness gates block; efficiency thresholds can start as warnings and harden as the fleet cleans up.
How do I stop linting from being ignored? Keep it fast, keep exceptions justified in-repo, and make the check required in branch protection.
Do I need all six groups on day one? No - start with linting and a smoke test, then layer in efficiency, structure, and supply-chain gates as you scale.
Why gate by digest instead of tag? Tags move and digests do not, so gating by digest guarantees you test and ship the identical artifact.
Is a Docker smoke test enough before Kubernetes? It catches startup failures early, but pair it with a kubectl rollout status check because nodes run containerd, not Docker.
Related
- Why Images Need Their Own Quality Gates
- Linting Basics
- dive & Image Efficiency
- Container Structure Tests
- Smoke Tests After Build
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).