Hadolint: Dockerfile Analyzer
Hadolint (Haskell Dockerfile Linter) is a tool that analyzes Dockerfiles to detect errors, bad practices, and vulnerabilities. It follows the recommendations of Docker Best Practices and ShellCheck (for RUN commands).
We can also do these dockerfile checks using Trivy, but it's good to know tools to have in your tool arsenal.
Hadolint has some advantages over other linting tools for Dockerfiles, such as Dockle and Trivy, depending on the usage context. Here are the main differentiators:
- Hadolint is focused exclusively on Dockerfile writing best practices.
- Follows official Docker recommendations, ensuring efficiency and security.
- Also uses ShellCheck to validate RUN commands, helping to avoid common shell script errors.
- Hadolint runs locally and quickly without depending on an external database.
- Can be easily integrated into GitHub Actions, GitLab CI, Jenkins, etc., to ensure Dockerfiles follow standards before building.
- Allows disabling specific rules if necessary.
Limitations:
- Does not detect vulnerabilities in packages or images (unlike Trivy and Dockle).
- Focused only on Dockerfiles (if you need linting for docker-compose.yml, another tool is needed).
There is a vscode extension that automatically detects the dockerfile and gives insights, but it requires the hadolint binary to be installed.
Installationβ
Installation is simple, just download the binary from the release page and put it in the path, like most cli binaries we have around. If you have brew you can use the command below. It is also possible to run using a container, this is good for pipelines, if you are going to integrate it into your vscode you need to install the binary.
brew install hadolint
hadolint --version
Haskell Dockerfile Linter 2.12.0
hadolint - Dockerfile Linter written in Haskell
Usage: hadolint [-v|--version] [-c|--config FILENAME] [DOCKERFILE...]
[--file-path-in-report FILEPATHINREPORT] [--no-fail]
[--no-color] [-V|--verbose] [-f|--format ARG] [--error RULECODE]
[--warning RULECODE] [--info RULECODE] [--style RULECODE]
[--ignore RULECODE]
[--trusted-registry REGISTRY (e.g. docker.io)]
[--require-label LABELSCHEMA (e.g. maintainer:text)]
[--strict-labels] [--disable-ignore-pragma]
[-t|--failure-threshold THRESHOLD]
Lint Dockerfile for errors and best practices
Available options:
-h,--help Show this help text
-v,--version Show version
-c,--config FILENAME Path to the configuration file
--file-path-in-report FILEPATHINREPORT
The file path referenced in the generated report.
This only applies for the 'checkstyle' format and is
useful when running Hadolint with Docker to set the
correct file path.
--no-fail Don't exit with a failure status code when any rule
is violated
--no-color Don't colorize output
-V,--verbose Enables verbose logging of hadolint's output to
stderr
-f,--format ARG The output format for the results [tty | json |
checkstyle | codeclimate | gitlab_codeclimate | gnu |
codacy | sonarqube | sarif] (default: tty)
--error RULECODE Make the rule `RULECODE` have the level `error`
--warning RULECODE Make the rule `RULECODE` have the level `warning`
--info RULECODE Make the rule `RULECODE` have the level `info`
--style RULECODE Make the rule `RULECODE` have the level `style`
--ignore RULECODE A rule to ignore. If present, the ignore list in the
config file is ignored
--trusted-registry REGISTRY (e.g. docker.io)
A docker registry to allow to appear in FROM
instructions
--require-label LABELSCHEMA (e.g. maintainer:text)
The option --require-label=label:format makes
Hadolint check that the label `label` conforms to
format requirement `format`
--strict-labels Do not permit labels other than specified in
`label-schema`
--disable-ignore-pragma Disable inline ignore pragmas `# hadolint
ignore=DLxxxx`
-t,--failure-threshold THRESHOLD
Exit with failure code only when rules with a
severity equal to or above THRESHOLD are violated.
Accepted values: [error | warning | info | style |
ignore | none] (default: info)
The command is simple, just hadolint
plus the dockerfile you want to evaluate. Most of the options above can be passed directly in the command or defined in a yaml file and passed in the --config flag that we will see shortly.
Let's go to a simple initial dockerfile example.
cat <<EOF > Dockerfile
FROM ubuntu:latest
RUN apt-get update
RUN apt-get install -y curl
RUN apt-get clean
RUN cd /home && mkdir app
COPY app /home/app
CMD ["bash"]
EOF
hadolint Dockerfile
Dockerfile:1 DL3007 warning: Using latest is prone to errors if the image will ever update. Pin the version explicitly to a release tag
Dockerfile:2 DL3009 info: Delete the apt-get lists after installing something
Dockerfile:3 DL3008 warning: Pin versions in apt get install. Instead of `apt-get install <package>` use `apt-get install <package>=<version>`
Dockerfile:3 DL3015 info: Avoid additional packages by specifying `--no-install-recommends`
Dockerfile:3 DL3059 info: Multiple consecutive `RUN` instructions. Consider consolidation.
Dockerfile:5 DL3003 warning: Use WORKDIR to switch to a directory
This Dockerfile works, but see that we have some warnings and info that could be improved and we will change them.
# Solving DL3007 warning: Pin the version explicitly to a release tag
# We force the specific tag instead of keeping latest
FROM ubuntu:24.04
# Solving
# DL3015 info: Avoid additional packages by specifying `--no-install-recommends`
# Prevents recommended packages from being installed together with the main package automatically.
# DL3008 warning: Pin versions in apt get install.
# We pin package versions.
# DL3009 info: Delete the apt-get lists after installing something
# apt-get clean is no longer necessary when we use rm -rf /var/lib/apt/lists/* after package installation.
# The reason is that this command performs a more effective and complete cleanup
# DL3059 info: Multiple consecutive `RUN` instructions. Consider consolidation.
# We put everything inside a single RUN to avoid multiple layers.
RUN apt-get update && apt-get install -y --no-install-recommends curl=7.88.1-10ubuntu4 \
&& rm -rf /var/lib/apt/lists/*
# DL3003 warning: Use WORKDIR to switch to a directory
# We will work with workdir instead of creating a folder
WORKDIR /home/app
COPY app/ .
CMD ["bash"]
If we want to keep curl installation always at the latest version we would have the error below, but we can ignore it if necessary.
cat <<EOF > Dockerfile
FROM ubuntu:24.04
RUN apt-get update && apt-get install -y --no-install-recommends curl \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /home/app
COPY app/ .
CMD ["bash"]
EOF
hadolint Dockerfile
Dockerfile:2 DL3008 warning: Pin versions in apt get install. Instead of `apt-get install <package>` use `apt-get install <package>=<version>`
hadolint Dockerfile --ignore DL3008
We could also put this ignore as a comment inside the Dockerfile file and everything would work the same without needing to pass the --ignore flag.
cat <<EOF > Dockerfile
FROM ubuntu:24.04
# hadolint ignore=DL3008
RUN apt-get update && apt-get install -y --no-install-recommends curl \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /home/app
COPY app/ .
CMD ["bash"]
EOF
hadolint Dockerfile
# We can disable comments
hadolint Dockerfile --disable-ignore-pragma
Dockerfile:3 DL3008 warning: Pin versions in apt get install. Instead of `apt-get install <package>` use `apt-get install <package>=<version>`
Although hadolint follows giving examples of best practices in Dockerfile definition, it does not help to harden the container.
Note that it allowed us to run this container as root, because it doesn't know the purpose of this container. We can check these vulnerability items with trivy. Let's run with trivy to see if it catches any difference.
β― trivy config Dockerfile
2025-02-02T02:56:18-03:00 INFO [misconfig] Misconfiguration scanning is enabled
2025-02-02T02:56:19-03:00 INFO Detected config files num=1
Dockerfile (dockerfile)
Tests: 28 (SUCCESSES: 26, FAILURES: 2)
Failures: 2 (UNKNOWN: 0, LOW: 1, MEDIUM: 0, HIGH: 1, CRITICAL: 0)
AVD-DS-0002 (HIGH): Specify at least 1 USER command in Dockerfile with non-root user as argument
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Running containers with 'root' user can lead to a container escape situation. It is a best practice to run containers as non-root users, which can be done by adding a 'USER' statement to the Dockerfile.
See https://avd.aquasec.com/misconfig/ds002
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
AVD-DS-0026 (LOW): Add HEALTHCHECK instruction in your Dockerfile
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
You should add HEALTHCHECK instruction in your docker container images to perform the health check on running containers.
See https://avd.aquasec.com/misconfig/ds026
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
In addition to not running as root, trivy suggested adding a healthcheck. In this case it's a simple container without an application to check, but we can have a healthcheck checking if bash is active to remove this LOW type warning from trivy.
On the other hand, trivy did not complain about pinning package versions as hadolint did, showing that these tools complement each other.
So the correct would be...
FROM ubuntu:24.04
RUN apt-get update && apt-get install -y --no-install-recommends curl=8.5.0-2ubuntu10.6 \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /home/app
COPY . app/
USER appuser
HEALTHCHECK CMD ps aux | grep -q 'bash' || exit 1
CMD ["bash"]
One thing to note is that the curl version curl=7.88.1-10ubuntu4 does not exist in ubuntu 24.04 and hadolint did not complain about it. It does not build the container to check anything, it only checks your dockerfile definition and suggests best practices.
If we went back to our initial definition full of problems, what would trivy say about it?
trivy config Dockerfile
2025-02-02T03:02:35-03:00 INFO [misconfig] Misconfiguration scanning is enabled
2025-02-02T03:02:36-03:00 INFO Detected config files num=1
Dockerfile (dockerfile)
Tests: 28 (SUCCESSES: 22, FAILURES: 6)
Failures: 6 (UNKNOWN: 0, LOW: 1, MEDIUM: 2, HIGH: 3, CRITICAL: 0)
# Trivy also detected pinning the image tag
AVD-DS-0001 (MEDIUM): Specify a tag in the 'FROM' statement for image 'ubuntu'
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
When using a 'FROM' statement you should use a specific tag to avoid uncontrolled behavior when the image is updated.
See https://avd.aquasec.com/misconfig/ds001
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Dockerfile:1
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
1 [ FROM ubuntu:latest
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
AVD-DS-0002 (HIGH): Specify at least 1 USER command in Dockerfile with non-root user as argument
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Running containers with 'root' user can lead to a container escape situation. It is a best practice to run containers as non-root users, which can be done by adding a 'USER' statement to the Dockerfile.
See https://avd.aquasec.com/misconfig/ds002
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# Trivy also detected that we should remove relative paths and use WORKDIR
AVD-DS-0013 (MEDIUM): RUN should not be used to change directory: 'cd /home && mkdir app'. Use 'WORKDIR' statement instead.
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Use WORKDIR instead of proliferating instructions like 'RUN cd β¦ && do-something', which are hard to read, troubleshoot, and maintain.
See https://avd.aquasec.com/misconfig/ds013
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Dockerfile:5
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
5 [ RUN cd /home && mkdir app
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# Also asked to run things in the same RUN to generate fewer layers
AVD-DS-0017 (HIGH): The instruction 'RUN <package-manager> update' should always be followed by '<package-manager> install' in the same RUN statement.
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
The instruction 'RUN <package-manager> update' should always be followed by '<package-manager> install' in the same RUN statement.
See https://avd.aquasec.com/misconfig/ds017
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Dockerfile:2
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
2 [ RUN apt-get update
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
AVD-DS-0026 (LOW): Add HEALTHCHECK instruction in your Dockerfile
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
You should add HEALTHCHECK instruction in your docker container images to perform the health check on running containers.
See https://avd.aquasec.com/misconfig/ds026
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# Also asked to avoid installing recommended packages
AVD-DS-0029 (HIGH): '--no-install-recommends' flag is missed: 'apt-get install -y curl'
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
'apt-get' install should use '--no-install-recommends' to minimize image size.
See https://avd.aquasec.com/misconfig/ds029
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Dockerfile:3
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
3 [ RUN apt-get install -y curl
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
What Trivy did not do was ask to pin package versions and clean apt-get lists after installation.
What we can see is that these tools complement each other and should be used together to ensure better container analysis and creation.