# droast, a Dockerfile linter whose whole pitch is the error message

> droast is a Rust linter for Dockerfiles that finds the same bad practices other linters find and reports them in an intentionally rude voice, with a span aware parser that handles heredocs, parser directives, BuildKit flags and PowerShell. It ships as a CLI, a VS Code extension, a Neovim plugin, a GitHub Action and a WebAssembly build, and its safe fixes are limited to four mechanical rules.

**immanuwell/dockerfile-roast** — droast - a dockerfile linter that actually has opinions 🔥

- Repository: https://github.com/immanuwell/dockerfile-roast
- Website: https://ewry.net/droast-dockerfile-linter/
- Stars: 1,135 · Forks: 22
- Language: Rust
- License: MIT
- Published: 2026-10-19 · Updated: 2026-10-19 · Language: en
- Canonical page: https://hysenlabs.com/projects/immanuwell-dockerfile-roast

## The tone is the product, and it is a deliberate choice

droast describes itself as a dockerfile linter that actually has opinions, and the README says it catches bad practices and tells you about them in the least diplomatic way possible. The comparison offered is code review from a senior dev who has seen too many prod incidents and has stopped being polite about it.

That framing matters more than it might appear, because a linter is judged partly on whether people read its output. A message that says the same thing with a different tone can get more attention in a pull request, and the README's claim is that this one does. It is a design decision that some teams will love and that a corporate environment may find unsuitable, and either way it is a decision rather than an accident.

The Cargo.toml description is blunter still, calling it a Dockerfile linter with personality and describing it as catching bad practices with snarky, funny error messages. The repository description just says it has opinions.

Underneath the tone, the tool does the standard job. It lints Dockerfiles, it has a rule identifier scheme with DF-prefixed codes, it has severity levels, and it can be told to skip rules you disagree with. The README is explicit that skipping is respected, with droast --skip DF001,DF012 Dockerfile as the example, and the wording is disarmingly civil: disagree with a rule, valid, we respect it.

There is a seriousness underneath as well. The Cargo.toml lists the categories as command-line-utilities and development-tools, the topics list includes ci, docker, static-analysis, security and wasm, and the repository has a corpus/ directory and a compatibility/ directory, which suggests a fixture corpus for testing the parser against real Dockerfiles rather than only against hand written examples.

## A span aware parser that handles the Dockerfile dialect properly

The parser is where a Dockerfile linter either works or becomes a source of false positives, and the README is specific about what is understood: heredocs, parser and escape directives, shell and JSON forms, BuildKit flags such as RUN --mount, Windows paths, and PowerShell. Malformed Dockerfile syntax is reported as its own rule, DF071, which is the right call because a syntax error is a different class of problem from a bad practice.

Getting this right is what lets the linter report a line and a column rather than a whole instruction, and the mention of spans in the README is about the diagnostics pointing at the exact text. That matters for the fix feature, which has to know precisely which characters to rewrite.

The Dockerfile at the repository root shows the build being done on rust:1.88-alpine with musl-dev added, and the runtime stage on alpine:3.20 with the release binary copied to /usr/local/bin/droast and used as the entrypoint. The Cargo.toml also lists binstall metadata with per-platform download URLs for macOS arm64 and x86_64, Linux gnu and musl on both architectures, and Windows, which is how the one-line installer finds the right binary.

The crate is published as dockerfile-roast on crates.io while the binary and the command are both called droast, and the library target is built as both a cdylib and an rlib. The cdylib is what makes the WebAssembly build possible, and the README points at the web linter as the browser version that runs in your browser.

The parser work is also why the linter can be embedded. The README says that if you are embedding droast in a Rust tool there are Rust library docs, and the DOCS.md file at the root is the reference for the library, the rules and the output formats.

## Finding files, and the ignore file rules that differ per engine

Given a directory, or no path at all, droast recursively discovers Dockerfile, Dockerfile.*, *.Dockerfile, *.dockerfile, Containerfile and Containerfile.*. That last pair matters, because Containerfile is the name the Podman and Buildah ecosystem uses and the same repository might contain both.

It also reads Compose YAML and Docker Bake HCL and JSON files to find non-standard Dockerfile paths and their declared build contexts. In Podman mode it additionally follows Quadlet .build and .kube units and local image *.kube.yaml layouts. Repository ignore rules are respected, while hidden project directories such as .devcontainer remain discoverable, which is a sensible rule since a devcontainer is exactly the place a Dockerfile tends to hide.

The ignore file handling is where the details get sharp, and it is worth reading carefully because it differs by engine. For DF033, Docker mode uses the effective ignore file Docker would use, where a .dockerignore beside the Dockerfile takes precedence over .dockerignore at the build context root. Podman mode instead prefers .containerignore, then falls back to .dockerignore. You select the engine with --engine podman or with [workflow] engine = "podman" in the configuration.

Missing, empty, comment-only and negation-only ignore files are all reported, and the check can be turned off with --check-ignorefile=false.

That last behaviour is a design opinion in itself. A .dockerignore that contains only a negation pattern, which is a way of saying include everything except one path, is technically valid but is a common source of accidental context bloat, so the linter flags it rather than accepting it silently.

The configuration files in examples/ show the granularity, with droast-enterprise.toml, droast-organization.toml and droast-repository.toml, which is a three level scoping model that lets a policy apply at an organisation level, be narrowed per repository, and then be overridden locally.

## Fixes are opt-in and deliberately narrow

The fixing feature is opt-in, and the README is unusually clear that it currently changes only four mechanical findings. The table lists them with their deterministic change.

DF076 matches instruction keywords to the file's first established upper or lowercase convention, so a Dockerfile that starts in lowercase and then switches to uppercase gets flagged and the fix makes it consistent. DF078 lowercases literal TCP and UDP protocol suffixes in EXPOSE. DF079 matches the case-insensitive AS keyword to an unambiguously upper or lowercase FROM, which matters because FROM and as are both accepted and mixing them is confusing. DF083 removes the exact redundant FROM --platform=$TARGETPLATFORM flag.

Every one of those is a change that cannot alter the meaning of the build, which is the constraint that makes --fix safe to run in a commit hook. A linter that also tried to rewrite your base image or reorder your layers would be a different tool with a different risk profile.

The commands for previewing and applying are:

```bash
droast fixes Dockerfile
droast --fix --dry-run --format diff Dockerfile
droast --fix Dockerfile
droast --fix DF076,DF079 Dockerfile
```

The --dry-run with the diff format shows you the exact patch before anything changes, and the last form limits the fix to named rules. Fixes can also be restricted to a subset of rules, which is how you would land the casing cleanup in one commit and the platform flag removal in another.

The severity filter and the skip list sit alongside this. --min-severity warning drops informational findings, and --no-roast turns off the commentary entirely and leaves just the facts, which the README calls boring mode. That is the setting to use in a pipeline that another tool consumes the output of.

## Distribution across every place a linter has to live

droast is distributed in more ways than most linters, and the reason is that a linter only helps if it runs where the Dockerfile is being written.

For the editor, the VS Code extension is published in the marketplace as droast, Dockerfile Linter, and the binary is bundled so there is no separate install. The command is code --install-extension ImmanuelTikhonov.droast, and findings appear in real time with roast messages on hover.

For Neovim, the plugin is dependency free and installs with a single curl command into sh, with no plugin manager required:

```bash
curl -fsSL https://raw.githubusercontent.com/immanuwell/dockerfile-roast/main/scripts/install-neovim-extension.sh | sh
```

There is also a dedicated droast.nvim repository for people who prefer their plugin manager, with the lazy.nvim, packer.nvim and mini.deps lines given in the README. The plugin starts automatically with droast on PATH, lints on save and uses native diagnostics, and the defaults can be overridden with require("droast").setup({ args = { "--preset", "production" } }).

For CI, the GitHub Action is used as immanuwell/dockerfile-roast@1.7.0, with a full inputs section referenced in the README, and a pre-commit hook configuration sits at the root of the repository.

For the container, the image is immanuwell/droast:1.7.0 on Docker Hub and also on ghcr.io, and the invocation mounts the working directory read only:

```bash
docker run --rm \
  -v "$PWD:/workspace:ro" \
  -w /workspace \
  immanuwell/droast:1.7.0 \
  --no-roast .
```

There is also a Wasmer build as a sandboxed WASI command, which reads from standard input or takes a mounted volume, and a web linter that runs in the browser.

The output formats are the part that matters for CI. The README lists --format github for GitHub Actions annotations, --format json for machine readable output, --format compact for one line per finding, and --format sarif for SARIF 2.1.0, which is what GitHub Advanced Security and IDEs consume. There is also a droast invocations command that inspects effective builds without Docker or Podman, with its own JSON format, and a wasmer.toml file at the root for the WASI package.

## Conclusion

droast suits a team that wants Dockerfile review to be consistent and want findings that people actually read, since the diagnostics are memorable and the rule set is small enough to learn, and it suits anyone already using a Dockerfile linter in CI who wants a drop-in action. It is the wrong choice if your pipeline depends on a specific stable rule identifier set, since the rules are versioned and the project is at 1.7.0 with no guarantee of identifier stability across versions, and it is the wrong choice if you want a linter that fixes your Dockerfiles for you, because --fix only touches four mechanical findings. Before adopting it, read DOCS.md for the rule list and the output formats, and check the ignore file handling, because the rules for which ignore file wins differ between Docker and Podman mode.

## FAQ

### How do I install droast?

Pick one: the Docker image immanuwell/droast:1.7.0, the web linter in the browser, a platform binary from the latest release, the one-line installer curl -fsL ewry.net/droast/install.sh, brew install droast, cargo install dockerfile-roast, the VS Code extension, the Neovim plugin, or the GitHub Action immanuwell/dockerfile-roast@1.7.0.

### What can droast actually autofix?

Only four mechanical findings: DF076 for instruction keyword case, DF078 for lowercase TCP and UDP in EXPOSE, DF079 for matching the AS keyword to the FROM case, and DF083 for removing a redundant FROM --platform=$TARGETPLATFORM flag. Use --fix --dry-run --format diff to preview the patch.

### How do I run droast in GitHub Actions?

Use the action with immanuwell/dockerfile-roast@1.7.0, and pick the output format that suits the pipeline, github for annotations, json for machine readable output, compact for one line per finding, or sarif for SARIF 2.1.0 consumed by GitHub Advanced Security and IDEs.

### Which files does droast look for?

Given a directory or no path it recursively discovers Dockerfile, Dockerfile.*, *.Dockerfile, *.dockerfile, Containerfile and Containerfile.*, and it reads Compose YAML and Docker Bake HCL/JSON files to find non-standard paths and build contexts. In Podman mode it also follows Quadlet .build and .kube units.

### How do I turn off a rule I disagree with?

Use the skip flag, for example droast --skip DF001,DF012 Dockerfile, and the README states that skipping a rule is respected. The severity filter droast --min-severity warning keeps only warnings and above, and --no-roast prints the findings without the commentary.

## Sources

- [immanuwell/dockerfile-roast on GitHub](https://github.com/immanuwell/dockerfile-roast)
- [License: MIT](https://github.com/immanuwell/dockerfile-roast/blob/main/LICENSE)
- [Project website](https://ewry.net/droast-dockerfile-linter/)
- [README](https://github.com/immanuwell/dockerfile-roast/blob/main/README.md)
- [Releases](https://github.com/immanuwell/dockerfile-roast/releases)

---

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