How to use a Dockerfile linter

How to use a Dockerfile linter

Feb 15, 2024
Catherine Edelveis

Developers should follow the best practices of compiling Dockerfiles stated in the official Docker documentation to avoid issues with container images such as:

  • failures when building images,
  • inefficient images,
  • security gaps.

However, it is often difficult to assess the quality of your Dockerfile in practice as it may contain numerous complicated instructions. This is where a linter comes to our rescue and verifies the Dockerfile against the defined best practices. Let’s browse the available linting tools and learn how to assess and edit our Dockerfiles on the fly.

An overview of Dockerfile linters

A linter is a tool evaluating the code quality. For instance, some of them are aimed at assessing the quality of Java code — you probably use one of those when compiling the application. IDE hints are also an example of code linting.

As Dockerfile is a program written in a specialized language that describes the process of building an OCI image, it can also be evaluated by a linter. In the case of Dockerfiles, you may perceive a linter as an editor proofreading the author’s text. There are multiple Dockerfile linters on the market, some of them are commercial or bundled with a vendor’s products as a supplement. But I will provide a summary of free solutions.


Hadolint is the most popular open-source Dockerfile linter written in Haskell. After receiving a Dockerfile, the tool checks it against the set of rules (more on which is in the section below). One of distinguishing Hadolint features is the usage ShellCheck, another open-source tool that lints bash and sh code included into the RUN instructions. In addition, Hadolint can be used for Dockerfile LABEL linting (LABEL instructions are used to add metadata to the image).  

The tool is compatible with macOS, Windows, and Linux. It is also available as a container image, so you can run it inside the container without installing it locally. In addition, Hadolint integrates with IDEs and many CI tools, which makes it an optimal choice for corporate development.


Another open-source linter is dockerfile-lint written in JavaScript. Just like Hadolint, it checks the Dockerfile against a set of rules and can also be used for LABEL linting. The results can be viewed in JSON or XML. You can install the tool and run it locally on your PC or clone the GitHub repository and run the tool from the bin directory without installing it.


dockerfile_validator is an open-source extension for Visual Studio Code based on dockerfile-lint. The tool checks the Dockerfile against the default and custom rule files and can also be used to evaluate LABEL rules.

How to lint Dockerfiles with Hadolint from the command line

Install Hadolint

There’s a variety of ways to use Hadolint. You can install it locally or run the Hadolint Docker container.

In the case of macOS or Linux, the easiest way to install Hadolint is to get it via brew:

brew install hadolint

For Windows, you can use scoop:

scoop install hadolint

You can also download Hadolint from the latest release page.

If you want to use a container image, you can pull it with:

docker pull hadolint/hadolint

Or if you need a container with shell access, you cann get a container for Debian or Alpine:

docker pull hadolint/hadolint:latest-alpine

Run the following command to verify the installation:

hadolint --version
Haskell Dockerfile Linter 2.12.0

Rules and severity levels

Hadolint issues the result of assessment in the following format:


The meaning of LINE_NUMBER and DESCRIPTION is clear, but what about RULE_CODE and SEVERITY_LEVEL?

  • The rule code comprises a prefix (DL meaning that it comes from Hadolint or SC from SpellCheck) and the rule number. The list of rules can be found here. Note that this list is actively updated, so you should keep up with the latest version. 
  • The severity level indicated how critical the violation of the rule is. There are five severity levels:
    • IGNORE
    • STYLE
    • INFO
    • ERROR

In addition, a rule code can have no severity level.

You can ignore certain rules, add custom rules, or change the severity level with command line flags or via the configuration file. 

Hadolint configuration file

The configuration file can be applied globally or to a specific project, which is much more convenient that passing command-line options each time you want to check a Dockerfile.

The config file should be written in yaml format. Let’s take a look at the following example and break it up line by line:

failure-threshold: warning
format: json
 - DL3001
   - DL3059
  • failure-threshold: warning sets the threshold for severity levels. Only the rules with the severity level above threshold will cause a failure.
  • format: json specifies the format of the output file.
  • ignored: DL3001 tells the linter to not check against rule DL3001 when assessing the Dockerfile.
  • override: warning: DL3059 upgrades the severity level of the specified rule from INFO to WARNING.
  • trustedRegistries: specifies the registries from where images should be pulled; otherwise, the linter issues a warning. For instance, if your enterprise receives JDK from a vendor, including the vendor's image registry on the list will prevent anyone from pulling a JDK from another source.

You can place the configuration file into root of the project, or, if it is located elsewhere, set the path with

hadolint --config /path/to/config.yaml Dockerfile

Finally: Lint the Dockerfile

Alright, it’s time for the most interesting part: let’s feed a Dockerfile to Hadolint and see what it tells us.

For this purpose, you can copy and save a ‘bad’ Dockerfile below or use your own:

FROM bellsoft/liberica-runtime-container:jdk-17-stream-musl as builder
WORKDIR /home/myapp
ADD demo /home/myapp/demo
RUN cd docker-image-demo && ./mvnw package
FROM bellsoft/alpaquita-linux-base:latest
RUN addgroup -S spring
RUN adduser -S spring -G spring
USER spring:spring
WORKDIR /home/myapp
COPY --from=builder /home/myapp/demo/target .
CMD ["java", "-jar", "demo-0.0.1-SNAPSHOT.jar"]

I won’t apply the configuration file included in the section above so we can get the output as is.

Go to the directory where the Dockerfile is located and run

hadolint Dockerfile

Or, if you are using a container image:

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

If you used the Dockerfile above, you will get the following result:

Dockerfile:3 DL3020 error: Use COPY instead of ADD for files and folders
Dockerfile:4 DL3003 warning: Use WORKDIR to switch to a directory
Dockerfile:5 DL3007 warning: Using latest is prone to errors if the image will ever update. Pin the version explicitly to a release tag
Dockerfile:7 DL3059 info: Multiple consecutive `RUN` instructions. Consider consolidation

As you can see, all Hadolint remarks are to the purpose. You can now go through the Dockerfile and fix the mistakes accordingly.

Common Dockerfile linting errors and how to solve them

In this section, I’d like to summarize the most common errors discovered by Hadolint and ways to quickly remedy them. I will use Liberica Runtime Container based on Liberica JDK Lite and Alpaquita Linux as an example.

DL3019: Remove the apk cache after installing packages 

Alpaquita Linux contains only the essential packages, thanks to which it is much smaller than other popular Linux distros. The DL3019 issue may arise when you install the additional packages without cleaning the cache, which will be added as a layer to the final image. So using this command in the Dockerfile

RUN apk add liberica17-lite-jdk-all

Will result in the following message from Hadolint:

DL3019 info: Use the `--no-cache` switch to avoid the need to use `--update` and remove `/var/cache/apk/*` when done installing packages

Therefore, the following instruction will solve this issue:

RUN apk add --no-cache liberica17-lite-jdk-all

DL3018: Pin the package version

The instruction above will yield one more error, namely

DL3018 warning: Pin versions in apk add. Instead of `apk add <package>` use `apk add <package>=<version>`

If you don’t specify the version, the package manager will get the latest one, which may result in application failures. For enhanced stability in production, it is recommended to use a particular package version.

The remediation will be to add the package version, for instance:

RUN apk add --no-cache liberica17-lite-jdk-all=17.0.10

DL3059: Avoid multiple consecutive RUN instructions

Each RUN instruction, as well as COPY and ADD, adds a layer to the container, which may result in a bloated final image. So it is better to combine instructions in one if they follow each other. Therefore, instead of

RUN cd my-application
RUN ./mvnw package

You should write

RUN cd my-application && ./mvnw package

DL3015: Avoid installing additional packages

This issue doesn’t affect Alpaquita and Alpine because they use the APK package manager that doesn’t install recommended additional packages. But APT does, so your container image will be stuffed with packages you actually may not use. Therefore, instead of

RUN apt-get update && \
   apt-get install -y openjdk-17-jdk

You  should use

RUN apt-get update && \
   apt-get install -y openjdk-17-jdk --no-install-recommends && \
   apt-get clean && \
   rm -rf /var/lib/apt/lists/*

Note that we also clean the cache of the apt-get lists here.


As you can see, even if you switch to a lightweight Liberica Runtime Container and apply other viable Docker image reduction techniques, you should keep your Dockerfiles neat to avoid stuffing them with unnesessary components and comply with the standards. And a Dockerfile linter is a highly useful tool in this regard. You can even integrate a linter into the CI pipeline (for instance, there’s a GitHubAction for Hadolint), and your container images will always be efficient and secure.

Subcribe to our newsletter


Read the industry news, receive solutions to your problems, and find the ways to save money.

Further reading