# Hadolint: a Dockerfile linter that also parses the shell inside RUN

> Hadolint parses a Dockerfile into an AST, applies Dockerfile rules on top of it, and hands the Bash inside RUN instructions to ShellCheck. It installs as a binary, a Homebrew or Scoop package, or a container image, and it is the wrong tool when you need image or dependency scanning.

**hadolint/hadolint** — Dockerfile linter, validate inline bash, written in Haskell

- Repository: https://github.com/hadolint/hadolint
- Stars: 12,447 · Forks: 504
- Language: Haskell
- License: GPL-3.0
- Published: 2026-09-21 · Updated: 2026-09-21 · Language: en
- Canonical page: https://hysenlabs.com/projects/hadolint-hadolint

## The problem Hadolint targets: Dockerfile mistakes that only surface at build or run time

A Dockerfile is a small program with a large blast radius. A pinned base image, a cleaned apt cache, a non-root user, an ADD that should have been a COPY: each of these is a line a reviewer can miss and a build will not complain about. Hadolint exists to catch those lines before the build runs. The README describes it as "a smarter Dockerfile linter that helps you build best practice Docker images", and the two halves of that sentence are the scope. It checks Dockerfile practice, not the contents of the resulting image.

The audience is narrow and identifiable. If you maintain Dockerfiles in a repository, review them in pull requests, and want the review to be mechanical rather than a matter of reviewer memory, this is aimed at you. It is also aimed at teams that already run ShellCheck on shell scripts but have no equivalent gate for the shell that lives inside RUN instructions, which is where a surprising amount of build logic ends up.

What it is not is a security scanner. Nothing in the README claims it inspects installed packages, base image CVEs or file contents. Teams that adopt it expecting a vulnerability report will be disappointed, and the alternatives section of the README exists precisely because different tools cover different parts of the problem.

## How Hadolint works: an AST for the Dockerfile, ShellCheck for the Bash

The README states the mechanism in one sentence: "The linter parses the Dockerfile into an AST and performs rules on top of the AST." That ordering matters. Hadolint is not a set of regular expressions over lines. It builds a syntax tree of the instructions, then evaluates rules against nodes, which is why rules can reason about instruction arguments, ordering and context rather than matching text.

The second half is the part that distinguishes it from most Dockerfile checkers. For the Bash code inside RUN instructions, Hadolint "stands on the shoulders of ShellCheck". The shell fragments are extracted and linted with ShellCheck's rules, so a quoting bug or an unquoted variable in a RUN line is reported by the same tool that would report it in a standalone script.

Around that core sits a CLI with an unusually wide output surface. The --format flag accepts tty, json, checkstyle, codeclimate, gitlab_codeclimate, gnu, codacy, sonarqube, sarif and junit. Several of those formats exist so that a CI system can consume results directly; the --file-path-in-report option is documented as applying only to checkstyle, codeclimate, sonarqube, junit and gitlab_codeclimate, and as useful when Hadolint runs inside Docker and needs to name the right file path in the report. That detail tells you the maintainers expect the container invocation to be the common one.

Severity is adjustable per rule through --error, --warning, --info and --style, and the exit behaviour is adjustable through --no-fail and -t/--failure-threshold. In other words, the tool separates what it found from whether the build should stop, which is the difference between a report and a gate.

## Installing Hadolint and running it on a real Dockerfile

The README lists prebuilt binaries for OSX, Windows and Linux on the release page as the first option, with container, brew and source installation as fallbacks. On macOS, Homebrew is the documented path:

```bash
brew install hadolint
```

On Windows, Scoop is the documented path, and the README gives the command as a batch snippet:

```batch
scoop install hadolint
```

If you would rather not install anything, the container route needs no local toolchain. The README pipes a Dockerfile into the image on stdin:

```bash
docker run --rm -i hadolint/hadolint < Dockerfile
```

The same image is published on two registries; the README shows `docker run --rm -i ghcr.io/hadolint/hadolint < Dockerfile` as the alternative. Expect findings on stdout in the default tty format, and a non-zero exit status when rules are violated unless you pass --no-fail.

For a first real use, lint a file with two rules ignored and one trusted registry declared, which is the shape most teams end up with:

```bash
hadolint --ignore DL3003 --ignore DL3006 --trusted-registry my-company.com:500 Dockerfile
```

The README gives exactly this example and explains the flags: --ignore excludes specific rules, and --trusted-registry warns when a FROM instruction points at a registry you have not allowed. If you need the output for a CI system rather than a terminal, switch the format, for example `hadolint -f json Dockerfile`, and note that the report formats listed above are the ones that respect --file-path-in-report.

Building from source is possible and documented, but it is the heaviest route: it requires Haskell and the cabal build tool, then `cabal configure`, `cabal build` and `cabal install` after cloning the repository. For most users the binary or the image is the shorter path.

## Suppressing rules: inline pragmas, config files and the flag that overrides both

Every linter eventually meets a rule the team disagrees with, and Hadolint's suppression model has three layers that interact in a way worth understanding before you write a config file.

The first layer is inline. A Dockerfile line can carry a pragma to disable a rule for that line, and the CLI exposes --disable-ignore-pragma to turn the whole mechanism off, which is what you would use in a repository where suppression must go through review rather than a comment.

The second layer is global, through a configuration file passed with -c/--config. The README's table of contents lists global ignores as a distinct topic from inline ignores, so the intended use is a project-wide list of rules the team has decided not to enforce.

The third layer is the --ignore flag, and here the interaction is explicit: "A rule to ignore. If present, the ignore list in the config file is ignored." That is a sharp edge. Passing a single --ignore on the command line does not add to the config file's list, it replaces it. A CI job that adds one flag for a special case silently drops every global ignore the project relies on, and the resulting flood of findings looks like a regression in the Dockerfile rather than a change in the invocation.

Labels get their own treatment. --require-label takes a schema of the form label:format, for example maintainer:text, and Hadolint checks that the label conforms. --strict-labels goes further and refuses labels outside the declared schema. The README flags a caveat about variables in labels, so if your labels interpolate build arguments, read that note before enabling strict mode.

## Where Hadolint stops: parsing limits, non-POSIX shells and the wrong-tool cases

The most consequential limitation is structural. Hadolint parses the Dockerfile into an AST. A file it cannot parse is not linted with reduced confidence; there is no AST to apply rules to. That means Hadolint is a poor fit for generated Dockerfiles that are only valid after template expansion, and it means a parse failure in CI should be read as a hard stop rather than a warning to ignore.

The README devotes a section to non-POSIX shells, which is the honest acknowledgement that the shell inside RUN is not always Bash. ShellCheck's rules are built around shell semantics; the further your RUN instructions move from that, the less the shell half of Hadolint has to say. If your builds are largely PowerShell or a language-specific toolchain invoked directly, you are paying for a ShellCheck integration you are not using.

There is also a category error to avoid. Hadolint reviews the instructions in a Dockerfile. It does not resolve what those instructions produce. A Dockerfile that Hadolint passes can still pull a base image with known vulnerabilities, install a package with a known CVE, or copy a secret into a layer. The README's alternatives section exists because the tool occupies one slot in a pipeline, not the whole pipeline.

Finally, the failure threshold is a policy decision the tool hands back to you. With -t/--failure-threshold you choose the severity at which the process exits non-zero, and --no-fail disables failure entirely. A pipeline that runs Hadolint with --no-fail and never reads the report has installed a linter and gained nothing.

## Hadolint compared with image and container scanners

The natural comparison is with tools that inspect the built artifact rather than the recipe. A container image scanner works on an image or a filesystem: it resolves installed packages and reports known vulnerabilities in them. Hadolint works on a text file and reports instruction-level practice. The two answer different questions, and the README's alternatives section is where the project itself points readers who want the other question answered.

The practical difference shows up in when each runs and what each can see. Hadolint can run on a pull request before any image is built, because a Dockerfile is just text; it needs no registry access and no build step, which is why the container invocation reads from stdin. An image scanner cannot run until there is an image, so it lands later in the pipeline and its findings arrive after the code is merged. Conversely, Hadolint cannot tell you that your pinned base image tag now points at a digest with an unpatched library, because it never looks inside the image.

A second axis is what the rule sets encode. Hadolint's rules are about Dockerfile practice and shell correctness. Image scanners encode vulnerability databases, which change independently of your Dockerfile, so their findings can appear without anyone editing a line. If your team's problem is "reviewers keep missing the same Dockerfile mistakes", Hadolint addresses it. If the problem is "we do not know what is in our images", it does not.

## Maintenance, release cadence and the GPL-3.0 licence

The repository is not archived, and the last push was on 2026-08-24. The release history shows v2.15.1 on 2026-07-31 and v2.15.0 on 2026-07-30, following v2.14.0 on 2025-09-22. The gap between v2.14.0 and the 2.15.x pair is roughly ten months, so the cadence is best described as occasional rather than continuous: expect periods with no release, then a pair of closely spaced ones.

That matters for upgrade cost in a specific way. Rule codes are the interface you build on. Ignore lists, severity overrides and failure thresholds all name rule codes, so a release that adds or reclassifies rules can change your CI result without any change to your Dockerfiles. The mitigation is already in the CLI: pin the version you run (the container image makes this trivial, since you choose the tag), and when you bump it, diff the findings against your existing ignore list before merging. The --error, --warning, --info and --style flags let you reclassify a rule that a new release promoted, without editing the Dockerfile.

On licensing, Hadolint is GPL-3.0. The repository carries a LICENSE file and a ThirdPartyNotices.txt, the latter reflecting the dependencies the project bundles or links. If you run the binary or the container image as a tool in your pipeline, you are using it rather than distributing it, but that distinction depends on how your organisation ships and hosts things, and it is not something to settle from a README. If you embed Hadolint in a product you distribute, have whoever handles licence compliance read LICENSE and ThirdPartyNotices.txt before you build on it. Nothing here is legal advice.

## Conclusion

Adopt Hadolint if your team writes Dockerfiles by hand and you want Dockerfile rules plus ShellCheck over RUN in one pass, wired into pre-commit or CI through the container image or a release binary. Do not adopt it as a replacement for image or dependency scanning, and do not expect it to validate a Dockerfile it cannot parse. Before rolling it out, run it once against your own Dockerfiles with -t warning to see how many findings you already have, then decide which rule codes belong in a .hadolint.yaml ignore list. The GPL-3.0 licence is the point to check with whoever handles your distribution obligations, since the repository ships a ThirdPartyNotices.txt alongside LICENSE.

## FAQ

### What is Hadolint used for?

It lints Dockerfiles. The README describes it as a Dockerfile linter that parses the file into an AST, applies rules on top of that AST, and uses ShellCheck to lint the Bash code inside RUN instructions.

### How do I install Hadolint on Windows?

The README documents Scoop for Windows, with the command `scoop install hadolint`. Prebuilt binaries for Windows are also available from the release page, and the container image works from PowerShell by piping the file into `docker run --rm -i hadolint/hadolint`.

### How do I install Hadolint on Linux?

The README lists prebuilt Linux binaries on the release page as the first option, with the container image, nix, or a source build using Haskell and cabal as fallbacks. The container route needs no local install: `docker run --rm -i hadolint/hadolint < Dockerfile`.

### How do I tell Hadolint to ignore a rule?

There are three routes. Inline pragmas disable a rule for a line and can be turned off entirely with --disable-ignore-pragma; a config file passed with -c/--config holds a global ignore list; and --ignore RULECODE ignores a rule on the command line. Note that if --ignore is present, the ignore list in the config file is ignored.

### How do I use Hadolint in a Docker container?

Pipe the Dockerfile into the image on stdin, for example `docker run --rm -i hadolint/hadolint < Dockerfile`. The README shows the same invocation against `ghcr.io/hadolint/hadolint`, and notes that --file-path-in-report is useful when running under Docker so the generated report names the correct file path.

### What output formats does Hadolint support?

The --format flag accepts tty, json, checkstyle, codeclimate, gitlab_codeclimate, gnu, codacy, sonarqube, sarif and junit, with tty as the default. The checkstyle, codeclimate, sonarqube, junit and gitlab_codeclimate formats are the ones that honour --file-path-in-report.

## Sources

- [hadolint/hadolint on GitHub](https://github.com/hadolint/hadolint)
- [Issues](https://github.com/hadolint/hadolint/issues)
- [License: GPL-3.0](https://github.com/hadolint/hadolint/blob/master/LICENSE)
- [README](https://github.com/hadolint/hadolint/blob/master/README.md)
- [Releases](https://github.com/hadolint/hadolint/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/hadolint-hadolint
