tips5 min readDockerfile Linting Automation: Hadolint Rules Every Team Should UseAutomate Dockerfile linting with Hadolint: the 10 rules every team should enforce (version pinning, non-root, COPY over ADD, exec-form CMD), how to wire the gate into CI/CD, and how linting maps to CIS, PCI DSS, and NIST compliance.ShieldOps AI2026-07-31 ·3One un-pinnedapt-get install line in a Dockerfile was all it took for a public-facing container image to ship a known-critical CVE for nine months. A linting rule that runs in under a second — before the image ever leaves the build machine — would have caught it on the first commit. Dockerfiles look deceptively simple: a dozen lines, a base image, a fewRUN commands. But those lines encode every dependency, every privilege, and every supply-chain decision your container makes. Teams that treat the Dockerfile as "just build glue" repeatedly discover, months later, that a bad ADD or a floating latest tag became a production incident. The fix is not another manual checklist — it is automated linting, and the de-facto standard isHadolint(Haskell Dockerfile Linter). This guide walks through the Hadolint rules every team should enforce, how to wire them into CI/CD so they cannot be skipped, and how linting connects to the broader scan workflow you can run inShieldOps.Why Dockerfile Linting MattersContainer images are built once but run thousands of times. A mistake in the Dockerfile is therefore amultipliedmistake: it ships to every environment that pulls the image — dev, staging, production, and customer clusters. TheCIS Docker Benchmarkdedicates an entire section to image and buildfile hygiene precisely because it is where most downstream risk originates. Meanwhile, vulnerability databases likeNVDkeep showing the same pattern: the packages pulled by un-pinned install commands are the ones that later explode into critical CVEs.Linting automates the judgment calls a senior engineer would make in review. It checks version pinning, user privileges, layer hygiene, and a hundred other details at the speed of a parse — and it runs on every commit, not just when someone remembers to look. In theDocker security best practicesmodel, linting is the cheapest control in the pipeline: zero runtime cost, immediate feedback, and it feeds directly into the same SBOM and vulnerability analysis that tools likeShieldOpsperform after the image is built.What Automated Linting Catches1 · Version DriftFloatinglatest tags and un-pinned package installs that silently change behavior. 2 · Privilege EscalationRoot users, missingUSER directives, and inherited capabilities. 3 · Supply-Chain RiskADD from URLs, missing checksums, and non-deterministic builds. 4 · Layer BloatCached apt lists, unneeded packages, and secrets burned into layers.What Hadolint Does — and How to StartHadolint is an open-source linter for Dockerfiles written in Haskell, distributed as a single static binary or a container image. It parses your Dockerfile and applies hundreds of rules — each with an ID (e.g.DL3008), a severity, and a human-readable explanation. It integrates with ShellCheck for the shell commands inside RUN instructions, which is why it catches quoting bugs and unsafe shell patterns that a pure-Dockerfile parser would miss. Installing it takes seconds:# Install the binary (Linux) wget -O hadolint https://github.com/hadolint/hadolint/releases/latest/download/hadolint-Linux-x86_64 chmod +x hadolint sudo mv hadolint /usr/local/bin/ # Or run it without installing anything docker run --rm -i ghcr.io/hadolint/hadolint < Dockerfile # Or on macOS brew install hadolint Then lint your Dockerfile:hadolint Dockerfile # Output example: # /Dockerfile:3 DL3008 warning: Pin versions in apt-get install. # Instead of `apt-get install ` use `apt-get install =` # /Dockerfile:7 DL3002 error: Last USER action in Dockerfile should not be root. Every rule maps to a fix, so the output doubles as a review checklist. To see the full rule catalogue with examples, check theHadolint wiki.The 10 Hadolint Rules Every Team Should EnforceYou could enable every rule, but the highest-value ones for a security-conscious team are these ten. Enforce them first; add the rest as your Dockerfile style matures.1. DL3007 — Never Use thelatest Tag (warning) AFROM debian:latest base image is a moving target. Tomorrow's "latest" is not today's, and a rebuild can silently pull a completely different OS, glibc version, or set of CVEs. Reproducibility dies the moment you reference a floating tag. The fix:pin a digest or an explicit version tag.# Bad FROM node:latest # Good FROM node:22.14.0-alpine@sha256:9c1f2d3e4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1 2. DL3008 — Pin Versions inapt-get install (warning) An unpinnedapt-get install -y curl installs whatever the repository currently serves. A critical CVE in that package — or a dependency it pulls — is now in your image with no audit trail of what version went in. The fix:pin every package to an exact version and keep the apt cache clean in the same layer.# Bad RUN apt-get update && apt-get install -y curl # Good RUN apt-get update && apt-get install -y curl=8.5.0-2ubuntu10.1 \ && rm -rf /var/lib/apt/lists/* 3. DL3018 — Pin Versions inapk add (warning) Alpine'sapk has the same problem as apt: without package=version, builds are non-reproducible and vulnerable versions slip in silently. The fix:pin the version and pass--no-cache so the package index is not persisted. # Bad RUN apk add curl # Good RUN apk add --no-cache curl=8.5.0-r0 4. DL3009 — Deleteapt-get Lists in the Same Layer (info) Leaving/var/lib/apt/lists/* in the image bloats every layer that inherits from it and leaves stale package metadata around. It is also a classic "fat layer" that slows image pulls and expands your attack surface. The fix:chain the cleanup into the sameRUN as the install, so it never survives into a layer. RUN apt-get update \ && apt-get install -y --no-install-recommends openssl=3.0.2-0ubuntu1.18 \ && rm -rf /var/lib/apt/lists/* 5. DL3020 — UseCOPY Instead of ADD (error) ADD does far more than copy: it auto-extracts local tarballs and can fetch remote URLs. Both behaviors are surprising and dangerous — a remote URL inside ADD bypasses your build cache, introduces a supply-chain dependency on an external server, and has no checksum verification. The fix:useCOPY for files and curl with checksums for downloads. # Bad ADD https://example.com/app.tar.gz /app/ # Good COPY app.tar.gz /tmp/ RUN curl -fsSL https://example.com/app.tar.gz -o /tmp/app.tar.gz \ && echo "expected-sha256 /tmp/app.tar.gz" | sha256sum -c - 6. DL3025 — Use Exec Form forCMD and ENTRYPOINT (error) Shell form (CMD ping google.com) wraps the command in /bin/sh -c, which means signal handling is broken, PID 1 semantics are wrong, and the process can be killed without cleanup. Exec form (CMD ["ping", "google.com"]) runs the binary directly. The fix:always use the JSON array syntax forCMD/ENTRYPOINT. # Bad CMD nginx -g "daemon off;" # Good CMD ["nginx", "-g", "daemon off;"] 7. DL3002 — Do Not Run asroot (error) Containers share the host kernel. A root process that escapes via a kernel exploit has full privileges on the host; a non-root process is contained by the user namespace and OS permissions. TheDocker security best practicesare unambiguous:do not run as root.The fix:create an unprivileged user and switch to it before the final entrypoint.RUN useradd --create-home --uid 10001 appuser USER 10001 CMD ["/app/start.sh"] 8. DL3006 — Tag the Image You Build (info)BuildingFROM node:22 and then shipping the result with no tag, or FROM scratch with no metadata, makes provenance impossible. When an incident happens, your team cannot answer "which exact image was running?" The fix:always tag builds with a version or commit hash, and consider multi-arch tags.docker build -t registry.example.com/app:$(git rev-parse --short HEAD) . 9. DL3059 — Consolidate MultipleRUN Layers (info) EachRUN creates a layer; dozens of layers bloat the image, slow pushes and pulls, and make the history harder to audit. They also hide intermediate artifacts (temp files, credentials) that survive in lower layers. The fix:group related commands into a singleRUN and use multi-stage builds to discard toolchains. See our multi-stage build guidefor the full pattern.# Bad: three layers RUN apt-get update RUN apt-get install -y build-essential RUN apt-get clean # Good: one layer RUN apt-get update \ && apt-get install -y --no-install-recommends build-essential \ && rm -rf /var/lib/apt/lists/* 10. DL4006 — Set theSHELL for Consistency (info) Different base images ship different shells (bash, sh, ash), and shell-form instructions inherit whatever the base defines. Pinning SHELL makes build behavior predictable and makes the Hadolint + ShellCheckintegration accurate.The fix:declare the shell explicitly when it matters.SHELL ["/bin/bash", "-o", "pipefail", "-c"] RUN curl -fsSL https://example.com/install.sh | bash Automating Hadolint in CI/CDA lint rule you run locally is a suggestion. A lint rule that fails the pipeline is a control. The whole point of "linting automation" is that the gate is enforced on every push, by every developer, with no exceptions. Hadolint runs as a plain binary, so it drops into any CI system.GitHub Actionsname: dockerfile-lint on: [push, pull_request] jobs: hadolint: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Lint Dockerfiles uses: hadolint/hadolint-action@v3.1.0 with: dockerfile: Dockerfile failure-threshold: warning Pre-commit Hook (local enforcement)# .pre-commit-config.yaml repos: - repo: https://github.com/hadolint/hadolint rev: v2.12.0 hooks: - id: hadolint Custom Config with ExceptionsNot every rule fits every project. Maintain a checked-in.hadolint.yaml so the team's exceptions are visible and versioned instead of living in someone's head: # .hadolint.yaml ignored: - DL3008 # base image repo does not expose exact versions - DL3018 failure-threshold: warning format: tty Whatever CI you use, the pattern is the same:lint the Dockerfile before the build, fail fast on warnings you care about, and never let a skipped step be the reason an image ships un-reviewed. That gate slots into the same pipeline where you would run afirst security scanof the resulting image.Insecure vs Linted: Side-by-SideHere is the same app built two ways. The insecure version lints to seven warnings and two errors; the hardened version passes clean.# ❌ INSECURE — hadolint: 2 errors, 7 warnings FROM node:latest ADD https://example.com/app.tar.gz /app/ WORKDIR /app RUN npm install CMD npm start # ✅ HARDENED — hadolint: clean FROM node:22.14.0-alpine@sha256:9c1f2d3e4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1 COPY package*.json ./ RUN npm ci --only=production && npm cache clean --force COPY . . USER 10001 EXPOSE 3000 CMD ["node", "src/server.js"] Notice what the hardened version gained beyond style: pinned base, no remoteADD, no root user, deterministic install, and exec-form entrypoint. Every one of those was a Hadolint rule. Real-World Consequences of Skipping LintingLinting failures rarely look dramatic in the moment — they look like a warning you scroll past. The consequences show up later:Un-pinned base image drift (2021–2023):teams buildingFROM node:latest shipped images that silently moved from Node 14 to Node 18 mid-cycle. Dependencies compiled against the old ABI broke in production, and the security team could not reproduce a single "known-good" image for audits. RemoteADD supply-chain compromise: a build-time ADD https://... pulls from an external server every build. When that server was defaced or the file replaced, the team's "clean" pipeline produced a backdoored artifact — and there was no checksum anywhere to catch it. NVDhas tracked this exact pattern across multiple malicious-package incidents.Root containers in production:a public-facing service running as root in a container, later matched to a container-escape CVE. The blast radius was the whole host, not the container. A one-lineUSER 10001 — flagged by DL3002 — would have contained it. Cost of a Late CatchLint time~1 second per Dockerfile, in CI, before build.Detection pointThe commit, the pull request, the developer's laptop.Without lintingDiscovered at scan time, in audit, or after a breach.Compliance MappingAutomated Dockerfile linting is not just good engineering — it is evidence for auditors. It maps directly to controls you will be asked about:StandardRelevant ControlHow Linting HelpsCIS Docker Benchmark4.x Image/Container configDL3002, DL3007, DL3020 enforce the same checksNIST SP 800-190Image provenance, least privilegePinned digests + non-root + COPY-not-ADDPCI DSS 4.06.3.3 change managementLint logs prove code review of build changesSOC 2CC8.1 change managementCI gate = documented controlSee how these map to your full environment on theShieldOps compliance page.Dockerfile Linting Checklist⬜ Install Hadolint (binary or container) and lint locally withhadolint Dockerfile ⬜ Enforce the 10 core rules: DL3007, DL3008, DL3018, DL3009, DL3020, DL3025, DL3002, DL3006, DL3059, DL4006⬜ Pin base image to a digest or exact version — neverlatest ⬜ Pin everyapt/apk/npm/pip package version ⬜ Run as non-root (USER 10001) before the final entrypoint ⬜ ReplaceADD with COPY; use curl + checksums for downloads ⬜ Use exec form forCMD/ENTRYPOINT ⬜ Add a GitHub Actions / GitLab CI / pre-commit gate withfailure-threshold: warning ⬜ Check in a.hadolint.yaml with team-approved exceptions ⬜ Feed the built image into anautomated vulnerability scanfor the full pictureRelated ShieldOps Reads10 Dockerfile Security Mistakes Putting Containers at RiskDockerfile FROM Instruction Risks: Choosing Secure Base ImagesMulti-Stage Docker Builds: Security and Size Optimization GuideShell Command Security: 10 Dangerous Patterns in DockerfilesShift Dockerfile Security Left: End-to-End SBOM Generation in CI/CDDockerfile Security Analysis: Turning Scan Results Into Actionable RemediationFrequently Asked QuestionsIs Hadolint free to use?Yes. Hadolint is open source (GPLv3) and free for both commercial and personal use. You can run the binary, the container image, or the GitHub Action without paying a license fee.How is Hadolint different from Docker Scout or a vulnerability scanner?Hadolint reads thesourceDockerfile and flags structural and best-practice problems before the build. Scanners like Trivy or Docker Scout analyze thebuilt imageand its packages for known CVEs. They are complementary: lint the recipe, scan the result. See ourscanner comparisonfor the image side.Should I fail the CI build on warnings, or only errors?Start withfailure-threshold: error to avoid blocking the team, then raise it to warning once the backlog is clean. The endgame is that warnings fail too — that is when linting becomes a real control. What about Dockerfiles in multi-stage builds?Hadolint lints the entire file, including every stage. If you maintain a base image separately, lint that Dockerfile with the same rules so your golden images start clean.Can Hadolint catch secrets like API keys in the Dockerfile?Not reliably — that is a different class of problem. Hadolint can flag thepattern(e.g. a file copied in that looks like a credential) but for real coverage use a dedicated secret scanner. Oursecrets detection guidecovers the tools and workflow.How do I handle false positives from rules I disagree with?Ignore them deliberately and visibly. Add the rule ID to.hadolint.yaml under ignored: with a comment explaining why. A documented exception is auditable; a disabled linter is not. ConclusionDockerfile linting is the cheapest security control you will ever add: it costs seconds per build, requires no runtime agents, and prevents the class of mistakes that turn into CVEs, supply-chain incidents, and failed audits. Enforce the ten core Hadolint rules, wire the gate into CI/CD so nobody can skip it, and keep the exceptions visible in a versioned config file.Linting tells you the recipe is clean. Scanning tells you the result is safe — run both.Create your free ShieldOps accountand analyze your first image today, or check theplansto see how automated SBOM and vulnerability tracking fit your pipeline.#dockerfile#hadolint#linting#ci-cd#devsecopsReady to apply these concepts?Try ShieldOps AI and start scanning your infrastructure right away.Start Free ScanRelated PostsWeekly Security Hygiene: A 15-Minute Checklist for Container Teams2026-07-14Shell Command Security: 10 Dangerous Patterns in Dockerfiles and How to Fix Them2026-07-134 Kubernetes Annotations That Instantly Improve Your Security Posture2026-07-12Your takeRate this article or leave a commentShare Submit commentHave more questions? Check ourFAQ