Need help with hadolint?
Click the “chat” button below for chat support from the developer who created it, or find similar developers for support.

About the developer

6.0K Stars 268 Forks GNU General Public License v3.0 995 Commits 85 Opened issues


Dockerfile linter, validate inline bash, written in Haskell

Services available


Need anything else?

Contributors list

Haskell Dockerfile Linter

Build Status GPL-3 licensed GitHub release Github downloads pipecat

A smarter Dockerfile linter that helps you build best practice Docker images. The linter parses the Dockerfile into an AST and performs rules on top of the AST. It stands on the shoulders of ShellCheck to lint the Bash code inside


:globewithmeridians: Check the online version on Screenshot

How to use

You can run

locally to lint your Dockerfile.
hadolint --ignore DL3003 --ignore DL3006  # exclude specific rules
hadolint --trusted-registry  # Warn when using untrusted FROM images

Docker comes to the rescue, providing an easy way how to run

on most platforms. Just pipe your
docker run
docker run --rm -i hadolint/hadolint < Dockerfile
# OR
docker run --rm -i < Dockerfile

or using Podman:

podman run --rm -i < Dockerfile
# OR
podman run --rm -i < Dockerfile

or using Windows PowerShell:

cat .\Dockerfile | docker run --rm -i hadolint/hadolint


You can download prebuilt binaries for OSX, Windows and Linux from the latest release page. However, if this does not work for you, please fall back to container (Docker),

or source installation.

On OSX, you can use brew to install

brew install hadolint

On Windows, you can use scoop to install

scoop install hadolint

As mentioned earlier,

is available as a container image:
docker pull hadolint/hadolint
# OR
docker pull

If you need a container with shell access, use the Debian or Alpine variants:

docker pull hadolint/hadolint:latest-debian
# OR
docker pull hadolint/hadolint:latest-alpine
# OR
docker pull
# OR
docker pull

You can also build

locally. You need Haskell and the stack build tool to build the binary.
git clone \
&& cd hadolint \
&& stack install

If you want the VS Code Hadolint extension to use Hadolint in a container, you can use the following wrapper script:

docker run --rm -i hadolint/hadolint hadolint "[email protected]" - < "$dockerfile"


hadolint --help
hadolint - Dockerfile Linter written in Haskell

Usage: hadolint [-v|--version] [--no-fail] [--no-color] [-c|--config FILENAME] [-V|--verbose] [-f|--format ARG] [DOCKERFILE...] [--error RULECODE] [--warning RULECODE] [--info RULECODE] [--style RULECODE] [--ignore RULECODE] [--trusted-registry REGISTRY (e.g.] [--require-label LABELSCHEMA (e.g. maintainer:text)] [--strict-labels] [-t|--failure-threshold THRESHOLD] Lint Dockerfile for errors and best practices

Available options: -h,--help Show this help text -v,--version Show version --no-fail Don't exit with a failure status code when any rule is violated --no-color Don't colorize output -c,--config FILENAME Path to the configuration file -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 | codacy] (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. 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 -t,--failure-threshold THRESHOLD Exit with failure code only when rules with a severity above THRESHOLD are violated. Accepted values: [error | warning | info | style | ignore | none] (default: info)


Configuration files can be used globally or per project. By default,

looks for a configuration file named
in the current directory.

config file schema
failure-threshold: string               # name of threshold level (error | warning | info | style | ignore | none)                
format: string                          # Output format (tty | json | checkstyle | codeclimate | gitlab_codeclimate | codacy)
ignored: [string]                       # list of rules
label-schema:                           # See Linting Labels below for specific label-schema details
  author: string                        # Your name
  contact: string                       # email address
  created: timestamp                    # rfc3339 datetime
  version: string                       # semver
  documentation: string                 # url
  git-revision: string                  # hash
  license: string                       # spdx
no-color: boolean                       # true | false
no-fail: boolean                        # true | false
  error: [string]                       # list of rules
  warning: [string]                     # list of rules
  info: [string]                        # list of rules
  style: [string]                       # list of rules
strict-labels: boolean                  # true | false
trustedRegistries: string | [string]    # registry or list of registries

supports specifying the ignored rules using a configuration file. The configuration file should be in
format. This is one valid configuration file as an example:
  - DL3000
  - SC1010


can warn you when images from untrusted repositories are being used in Dockerfiles, you can append the
keys to the configuration file, as shown below:
  - DL3000
  - SC1010



If you want to override the severity of specific rules, you can do that too:

- DL3001
- DL3002
- DL3042
- DL3033
- DL3032
- DL3015

Exit with failure code only when rules with a severity above THRESHOLD are violated (Available in v2.6.0+)
failure-threshold: info
- DL3042
- DL3033
- DL3032

The global configuration file should be placed in the folder specified by

, with the name
. In summary, the following locations are valid for the configuration file, in order or preference:
  • $PWD/.hadolint.yaml
  • $XDG_CONFIG_HOME/hadolint.yaml
  • ~/.config/hadolint.yaml

In windows, the

environment variable is used instead of

Additionally, you can pass a custom configuration file in the command line with the

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

To pass a custom configuration file (using relative or absolute path) to a container, use the following command:

docker run --rm -i -v /your/path/to/hadolint.yaml:/.config/hadolint.yaml hadolint/hadolint < Dockerfile
# OR
docker run --rm -i -v /your/path/to/hadolint.yaml:/.config/hadolint.yaml < Dockerfile

Inline ignores

It is also possible to ignore rules by adding a special comment directly above the Dockerfile statement for which you want to make an exception for. Such comments look like

# hadolint ignore=DL3001,SC1081
. For example:
# hadolint ignore=DL3006
FROM ubuntu

hadolint ignore=DL3003,SC1035

RUN cd /tmp && echo "hello!"

The comment "inline ignores" applies only to the statement following it.

Linting Labels

Hadolint is able to check if specific labels are present and conform to a predefined label schema. First, a label schema must be defined either via the command line:

hadolint --require-label author:text --require-label version:semver Dockerfile

or via the config file:

  author: text
  contact: email
  created: rfc3339
  version: semver
  documentation: url
  git-revision: hash
  license: spdx

The value of a label can be either of

: | Schema | Description | |:--------|:---------------------------------------------------| | text | Anything | | rfc3339 | A time, formatted according to RFC 3339 | | semver | A semantic version | | url | A URI as described in RFC 3986 | | hash | Either a short or a long Git hash | | spdx | An SPDX license identifier | | email | An email address conforming to RFC 5322 |

By default, Hadolint ignores any label that is not specified in the label schema. To warn against such additional labels, turn on strict labels, using the command line:

hadolint --strict-labels --require-label version:semver Dockerfile

or the config file:

strict-labels: true

When strict labels is enabled, but no label schema is specified,

will warn if any label is present.

Note on dealing with variables in labels

It is a common pattern to fill the value of a label not statically, but rather dynamically at build time by using a variable:

FROM debian:buster
ARG VERSION="du-jour"
LABEL version="${VERSION}"

To allow this, the label schema must specify

as value for that label:
  version: text


To get most of

, it is useful to integrate it as a check in your CI or into your editor, or as a pre-commit hook, to lint your
as you write it. See our Integration docs.


An incomplete list of implemented rules. Click on the error code to get more detailed information.

  • Rules with the prefix

    are from
    . Have a look at
    to find the implementation of the rules.
  • Rules with the

    prefix are from ShellCheck (only the most common rules are listed, there are dozens more).

Please create an issue if you have an idea for a good rule.

| Rule | Default Severity | Description | | :----------------------------------------------------------- | :--------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------- | | DL3000 | Error | Use absolute WORKDIR. | | DL3001 | Info | For some bash commands it makes no sense running them in a Docker container like ssh, vim, shutdown, service, ps, free, top, kill, mount, ifconfig. | | DL3002 | Warning | Last user should not be root. | | DL3003 | Warning | Use WORKDIR to switch to a directory. | | DL3004 | Error | Do not use sudo as it leads to unpredictable behavior. Use a tool like gosu to enforce root. | | DL3005 | Error | Do not use apt-get dist-upgrade. | | DL3006 | Warning | Always tag the version of an image explicitly. | | DL3007 | Warning | Using latest is prone to errors if the image will ever update. Pin the version explicitly to a release tag. | | DL3008 | Warning | Pin versions in apt-get install. | | DL3009 | Info | Delete the apt-get lists after installing something. | | DL3010 | Info | Use ADD for extracting archives into an image. | | DL3011 | Error | Valid UNIX ports range from 0 to 65535. | | DL3012 | Error | Multiple

instructions. | | DL3013 | Warning | Pin versions in pip. | | DL3014 | Warning | Use the
switch. | | DL3015 | Info | Avoid additional packages by specifying --no-install-recommends. | | DL3016 | Warning | Pin versions in
. | | DL3018 | Warning | Pin versions in apk add. Instead of
apk add 
apk add =
. | | DL3019 | Info | Use the
switch to avoid the need to use
and remove
when done installing packages. | | DL3020 | Error | Use
instead of
for files and folders. | | DL3021 | Error |
with more than 2 arguments requires the last argument to end with
| | DL3022 | Warning |
COPY --from
should reference a previously defined
alias | | DL3023 | Error |
COPY --from
cannot reference its own
alias | | DL3024 | Error |
aliases (stage names) must be unique | | DL3025 | Warning | Use arguments JSON notation for CMD and ENTRYPOINT arguments | | DL3026 | Error | Use only an allowed registry in the FROM image | | DL3027 | Warning | Do not use
as it is meant to be a end-user tool, use
instead | | DL3028 | Warning | Pin versions in gem install. Instead of
gem install 
gem install :
| | DL3029 | Warning | Do not use --platform flag with FROM. | | DL3030 | Warning | Use the
switch to avoid manual input
yum install -y 
| | DL3032 | Warning |
yum clean all
missing after yum command. | | DL3033 | Warning | Specify version with
yum install -y -
| | DL3034 | Warning | Non-interactive switch missing from
zypper install -y
| | DL3035 | Warning | Do not use
zypper dist-upgrade
. | | DL3036 | Warning |
zypper clean
missing after zypper use. | | DL3037 | Warning | Specify version with
zypper install -y [=]
. | | DL3038 | Warning | Use the
switch to avoid manual input
dnf install -y 
| | DL3040 | Warning |
dnf clean all
missing after dnf command. | | DL3041 | Warning | Specify version with
dnf install -y -
| | DL3042 | Warning | Avoid cache directory with
pip install --no-cache-dir 
. | | DL3043 | Error |
triggered from within
instruction. | | DL3044 | Error | Do not refer to an environment variable within the same
statement where it is defined. | | DL3045 | Warning |
to a relative destination without
set. | | DL3046 | Warning |
without flag
and high UID will result in excessively large Image. | | DL3047 | Info |
without flag
will result in excessively bloated build logs when downloading larger files. | | DL3048 | Style | Invalid Label Key | | DL3049 | Info | Label
is missing. | | DL3050 | Info | Superfluous label(s) present. | | DL3051 | Warning | Label
is empty. | | DL3052 | Warning | Label
is not a valid URL. | | DL3053 | Warning | Label
is not a valid time format - must be conform to RFC3339. | | DL3054 | Warning | Label
is not a valid SPDX license identifier. | | DL3055 | Warning | Label
is not a valid git hash. | | DL3056 | Warning | Label
does not conform to semantic versioning. | | DL3057 | IgnoreC |
instruction missing. | | DL3058 | Warning | Label
is not a valid email format - must be conform to RFC5322. | | DL3059 | Info | Multiple consecutive
instructions. Consider consolidation. | | DL3060 | Info |
yarn cache clean
missing after
yarn install
was run. | | DL4000 | Error | MAINTAINER is deprecated. | | DL4001 | Warning | Either use Wget or Curl but not both. | | DL4003 | Warning | Multiple
instructions found. | | DL4004 | Error | Multiple
instructions found. | | DL4005 | Warning | Use
to change the default shell. | | DL4006 | Warning | Set the
option -o pipefail before
with a pipe in it | | SC1000 | |
is not used specially and should therefore be escaped. | | SC1001 | | This
will be a regular
in this context. | | SC1007 | | Remove space after
if trying to assign a value (or for empty string, use
var='' ...
). | | SC1010 | | Use semicolon or linefeed before
(or quote to make it literal). | | SC1018 | | This is a unicode non-breaking space. Delete it and retype as space. | | SC1035 | | You need a space here | | SC1045 | | It's not
foo &; bar
, just
foo & bar
. | | SC1065 | | Trying to declare parameters? Don't. Use
and refer to params as
etc. | | SC1066 | | Don't use $ on the left side of assignments. | | SC1068 | | Don't put spaces around the
in assignments. | | SC1077 | | For command expansion, the tick should slant left (` vs ´). | | SC1078 | | Did you forget to close this double-quoted string? | | SC1079 | | This is actually an end quote, but due to next char, it looks suspect. | | SC1081 | | Scripts are case sensitive. Use
, not
. | | SC1083 | | This
is literal. Check expression (missing
?) or quote it. | | SC1086 | | Don't use
on the iterator name in for loops. | | SC1087 | | Braces are required when expanding arrays, as in
. | | SC1095 | | You need a space or linefeed between the function name and body. | | SC1097 | | Unexpected
. For assignment, use
. For comparison, use
[ .. ]
[[ .. ]]
. | | SC1098 | | Quote/escape special characters when using
, e.g.
eval "a=(b)"
. | | SC1099 | | You need a space before the
. | | SC2002 | | Useless cat. Consider cmd < file | .. or cmd file | .. instead. | | SC2015 | | Note that A && B || C is not if-then-else. C may run when A is true. | | SC2026 | | This word is outside of quotes. Did you intend to 'nest '"'single quotes'"' instead'? | | SC2028 | |
won't expand escape sequences. Consider
. | | SC2035 | | Use
-- *glob*
so names with dashes won't become options. | | SC2039 | | In POSIX sh, something is undefined. | | SC2046 | | Quote this to prevent word splitting | | SC2086 | | Double quote to prevent globbing and word splitting. | | SC2140 | | Word is in the form
(B indicated). Did you mean
? | | SC2154 | | var is referenced but not assigned. | | SC2155 | | Declare and assign separately to avoid masking return values. | | SC2164 | | Use cd ... || exit in case
fails. |


If you are an experienced Haskeller, we would be very grateful if you would tear our code apart in a review.


  1. Clone repository

    git clone --recursive [email protected]:hadolint/hadolint.git
  2. Install the dependencies

    stack install


The easiest way to try out the parser is using the REPL.

# start the repl
stack repl
# overload strings to be able to use Text
:set -XOverloadedStrings
# import parser library
import Language.Docker
# parse instruction and look at AST representation
parseText "FROM debian:jessie"


Run unit tests:

stack test

Run integration tests:



Dockerfile syntax is fully described in the Dockerfile reference. Just take a look at Syntax.hs in the

project to see the AST definition.


We use cookies. If you continue to browse the site, you agree to the use of cookies. For more information on our use of cookies please see our Privacy Policy.