Hadolint: The Essential Linter for Dockerfile Perfection

A linter is a tool used in programming to statically analyze code for potential errors, bugs, stylistic issues, and deviations from coding standards. It serves multiple purposes:

  1. Code Quality: Ensures the code adheres to industry standards, improving readability and maintainability.

  2. Error Detection: Identifies syntax errors, undefined variables, and other common mistakes before runtime.

  3. Best Practices: Encourages the use of best practices, which can lead to more efficient and secure code.

  4. Consistency: Helps maintain a consistent coding style within a project or team, which is crucial for collaborative development.

  5. Reduced security vulnerabilities: Some linters can even detect potential security vulnerabilities in your code, helping you write more secure programs.

  6. Increased developer productivity: By catching common errors and suggesting improvements, linters can help you write code faster and more efficiently.

Linters are especially useful in large projects or in projects where multiple developers are contributing, as they help maintain a standard quality across the codebase.

Haskell Dockerfile Linter (hadolint)

Hadolint is a linter specifically designed for Dockerfiles. It helps you build best practice Docker images. Since Dockerfiles define the environment in which applications run, ensuring they are correctly written is crucial for the reliability and security of the application.

Main Features

  1. Dockerfile Best Practices: Enforces best practices for writing Dockerfiles, as recommended by Docker and the broader community.

  2. Style Guide Enforcement: Helps maintain a consistent style in Dockerfile syntax and structure.

  3. Error Detection: Identifies common mistakes and potential bugs in Dockerfile scripts.

  4. Security analysis: Warns about potential security vulnerabilities in your Dockerfile, such as using insecure base images or exposing unnecessary ports.

  5. Customizable Rules: Allows users to enable or disable specific rules based on their project requirements.

  6. Integration: Can be integrated into continuous integration pipelines for automated code review.

  7. Multi-platform Support: Supports OSX, Windows and Linux, making it a versatile tool for different development environments.

How it works?

The linter parses the Dockerfile into an Abstract Syntax Tree (AST) and performs rules on top of the AST. An AST is a high-level, structured representation of the source code, which abstracts away the syntactic details of the code, allowing various tools and compilers to efficiently analyze, interpret, and transform the code. In the context of Hadolint, the AST of a Dockerfile enables the tool to perform accurate and effective linting.

Hadolint additionally utilizes ShellCheck for linting Bash code within RUN directives.

Anyone can improve the linter with suggestions at https://github.com/hadolint/hadolint/issues

Online version of the linter is available at https://hadolint.github.io/hadolint/

Installation methods

Hadolint can be installed by using different methods. Here is the breakdown of the list:

  1. Binary Download: You can download the pre-compiled binary for your operating system from the Hadolint GitHub releases page.

  2. Package Managers: For some operating systems, hadolint can be installed via package managers like Homebrew (macOS):

     brew install hadolint
    

    or scoop (Windows).

     scoop install hadolint
    

    In Linux, specifically Ubuntu system, you can install hadolint by downloading the binary with wget command and making it executable:

     # Replace [VERSION] with the desired version number:
     wget https://github.com/hadolint/hadolint/releases/download/[VERSION]/hadolint-Linux-x86_64 -O hadolint
    
     # Make it Executable
     chmod +x hadolint
    
     # Move to a Directory in Your PATH
     sudo mv hadolint /usr/local/bin/
    

    To check if installation was successful run the following command:

     hadolint --version
    
  3. Docker Image: Hadolint is available as a Docker image, which can be run without installing it directly on your machine.

     docker pull hadolint/hadolint
     # OR
     docker pull ghcr.io/hadolint/hadolint
    

    You can just pipe your Dockerfile to docker run command:

     docker run --rm -i hadolint/hadolint < Dockerfile
     # OR
     docker run --rm -i ghcr.io/hadolint/hadolint < Dockerfile
    
  4. Building from Source: For advanced users, hadolint can be built from source by using Haskell and the cabal build tool to build the binary.

     git clone https://github.com/hadolint/hadolint \
       && cd hadolint \
       && cabal configure \
       && cabal build \
       && cabal install
    
  5. Use VSCode extension: If you want the VS Code Hadolint extension to use Hadolint in a container, you can use the following wrapper script:

     #!/bin/bash
     dockerfile="$1"
     shift
     docker run --rm -i hadolint/hadolint hadolint "$@" - < "$dockerfile"
    

Main commands and options

To demonstrate how to use the main commands and options of Hadolint, use the following example Dockerfile:

FROM ubuntu:latest
RUN apt-get update && apt-get install -y curl
CMD ["echo", "Hello, world!"]

1. Lint a Single Dockerfile

hadolint /path/to/Dockerfile

Replace /path/to/Dockerfile with the actual path to your Dockerfile.

Based on the given suggestions from the hadolint, we can rewrite the Dockerfile into:

# Use a specific version instead of 'latest' to ensure consistency.
FROM ubuntu:20.04

# Update and install curl with a specific version, and clean up the apt cache to reduce image size.
# Also, use --no-install-recommends to avoid installing unnecessary packages.
RUN apt-get update && \
    apt-get install -y --no-install-recommends curl=7.68.0-1ubuntu2.5 && \
    rm -rf /var/lib/apt/lists/*

# The CMD instruction does not need to be modified as it was not part of the lint warnings.
CMD ["echo", "Hello, world!"]

2. Lint Multiple Dockerfiles

hadolint Dockerfile1 Dockerfile2 Dockerfile3

Replace Dockerfile1, Dockerfile2, Dockerfile3 with the paths to the Dockerfiles you want to lint.

3. Ignoring Specific Rules

hadolint --ignore DL3003 --ignore DL3006 /path/to/Dockerfile

This command will ignore the rules DL3003 and DL3006 when linting the specified Dockerfile.

4. Trusting Additional Docker Registries

hadolint --trusted-registry myregistry.com:5000 /path/to/Dockerfile

This tells Hadolint to trust the registry myregistry.com:5000.

5. Output Formats

hadolint -f json /path/to/Dockerfile

This will output the linting results in JSON format.

6. Reading from STDIN

cat /path/to/Dockerfile | hadolint -

This pipes the content of a Dockerfile into Hadolint.

7. Using a Config File

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

Specify a custom config file with --config.

8. 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

# Example usage
hadolint --require-label maintainer:text --require-label org.website:url Dockerfile

The value of a label can be either of text, url, semver, hash or rfc3339. You can see all acceptable format values here. By default, Hadolint ignores any label that is not specified in the label schema.

The --strict-labels option verifies there are no extra labels outside of the ones defined in your schema.

hadolint --require-label maintainer:text --strict-labels Dockerfile

Rule codes

In Hadolint, a rule code is a unique identifier assigned to each specific rule or guideline that the linter checks against in Dockerfiles. These rule codes are central to understanding and effectively using Hadolint, as they allow users to quickly identify and respond to particular issues raised during the linting process. Here's a breakdown of what these rule codes mean and how they are used:

Structure of Rule Codes

  1. Prefix: Each rule code starts with a prefix that indicates the nature or category of the rule. Common prefixes include:

    • DL: Dockerfile Linting. Rules prefixed with DL are specific to Dockerfile best practices and syntax and are from hadolint.

    • SC: ShellCheck. Rules prefixed with SC are inherited from ShellCheck, a tool that Hadolint integrates with for linting shell scripts within Dockerfiles.

  2. Number: Following the prefix is a unique number that identifies the specific rule. For example, DL3006 is a rule about always tagging the version of an image explicitly.

Purpose of Rule Codes

  • Identification: Rule codes make it easy to identify exactly what issue Hadolint has detected in your Dockerfile.

  • Documentation: Each rule code corresponds to a specific guideline or best practice. You can look up these codes in the Hadolint documentation to understand the rationale behind each rule and how to address it.

  • Configuration: When customizing Hadolint using a configuration file (.hadolint.yaml), rule codes are used to specify which rules to ignore, treat as warnings, or enforce.

Examples of Common Rule Codes

  • DL3006: Warns about not using a specific version tag with the FROM instruction in Dockerfiles. Using a specific version helps ensure consistent and repeatable builds.

  • DL3008: Advises pinning versions in apt-get install. This is about specifying exact versions of packages to install, ensuring consistent and predictable builds.

  • SC2086: This is a ShellCheck rule that warns about potential word splitting or globbing issues in shell scripts.

You can ignore one or more rules using the --ignore RULECODE option:

hadolint --ignore DL3006 --ignore DL3008 Dockerfile

Responding to Rule Codes

When you run Hadolint and it outputs warnings or errors with specific rule codes, you should:

  1. Review the Issue: Understand what specific part of your Dockerfile violated the rule.

  2. Consult Documentation: Look up the rule code in Hadolint's documentation for a detailed explanation and recommended practices.

  3. Modify Your Dockerfile: Adjust your Dockerfile according to the suggestions provided by the rule.

Understanding rule codes in Hadolint is essential for effectively using the tool to improve the quality and reliability of your Dockerfiles. These codes not only help in identifying and resolving specific issues but also aid in learning and adhering to best practices in Dockerfile development.

Configuration file

The configuration file in Hadolint is a tool for customizing the behavior of the Hadolint linter, enabling users to define rules and settings that align with their specific needs or the standards of a particular project. It allows you to store all of your Hadolint options in one place. This configuration file plays a crucial role in tailoring the linting process to suit diverse coding practices and requirements and can live in a variety of locations.

This is a full YAML config file schema of the configuration file:

failure-threshold: string               # name of threshold level (error | warning | info | style | ignore | none)
format: string                          # Output format (tty | json | checkstyle | codeclimate | gitlab_codeclimate | gnu | 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
override:
  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
disable-ignore-pragma: boolean          # true | false
trustedRegistries: string | [string]    # registry or list of registries

Purpose and Benefits

  1. Customization: It allows the customization of linting rules, enabling users to enable, disable, or modify the severity of specific rules.

  2. Consistency: Ensures consistent application of linting rules across different environments and among team members, particularly useful in collaborative projects.

  3. Flexibility: Offers flexibility to adapt to different project requirements or to adhere to specific coding standards.

Format and Location

  • Filename: The configuration file is typically named .hadolint.yaml or .hadolint.yml.

  • Format: It's written in YAML format, which is both human-readable and easy to write.

  • Location: Configuration files can be used globally or per project. Hadolint looks for configuration files in the following locations or their platform specific equivalents in this order and uses the first one exclusively:

    • $PWD/.hadolint.yaml

    • $XDG_CONFIG_HOME/hadolint.yaml

    • $HOME/.config/hadolint.yaml

    • $HOME/.hadolint/hadolint.yaml or $HOME/hadolint/config.yaml

    • $HOME/.hadolint.yaml

The configuration file can include several components:

  1. Ignored Rules: Specifies the rules that Hadolint should ignore.

     ignored:
       - DL3000
       - SC1010
    
  2. Rule Severity: Allows setting the severity level (info, warning, or error) for specific rules.

     override:
       error:
         - DL3001
         - DL3002
       warning:
         - DL3042
         - DL3033
       info:
         - DL3032
       style:
         - DL3015
    
  3. Trusted Registries: Defines Docker registries that are considered trusted, useful for rules that check for image sources.

     trustedRegistries:
       - docker.io
       - my-company.com:5000
       - "*.gcr.io"
    

Example Configuration

Here's an example of what a .hadolint.yaml configuration file might look like:

ignored:
  - DL3006 # Ignore the rule about always tagging the version of an image explicitly
override:
    warning:
      - DL3008 # Treat 'Pin versions in apt-get install' as a warning

    error:
      - DL3013 # Treat 'Pin versions in pip install' as an error

trustedRegistries:
  - myregistry.com:5000

Using the Configuration File

When running Hadolint, if a .hadolint.yaml file is present in the project directory, Hadolint automatically applies the configurations specified in the file. This allows for consistent linting rules to be applied across different instances where Hadolint is run, be it locally by developers or in a CI/CD pipeline.

Severity level

Hadolint classifies the issues it detects in Dockerfiles into different severity levels. These levels help users understand the importance or impact of each issue and decide how to prioritize their response. There are four primary severity levels in Hadolint:

Severity Levels

  1. Error: These are critical issues that should be addressed as soon as possible. They often represent significant problems that could lead to build failures, security vulnerabilities, or major deviations from best practices.

  2. Warning: Warnings indicate issues that are less critical but still important. They often relate to practices that could lead to suboptimal builds, potential future errors, or minor deviations from best practices.

  3. Info: These are informational messages about possible improvements or suggestions. They are not critical and often relate to stylistic choices or optimizations.

  4. style: These suggest stylistic improvements that may not affect functionality but aim for best practices and readability. You can choose to fail build based on style violations too.

Using Severity Levels in Hadolint CLI

When running Hadolint from the command line, you can control how these severity levels are handled. Here are some ways to use them:

  1. Changing severity level

    You can configure Hadolint to change severity level of the specific rule ID:

     hadolint --error DL3006 --info DL3045 Dockerfile
    
  2. Fail only on Errors

    The hadolint includes a --failure-threshold (abbreviated as -t) to exclude certain severity levels from causing a failure. For example, if you only want Hadolint to fail on error violations:

     hadolint -t error Dockerfile
    
  3. Ignoring Specific Severities

    If you want to ignore issues of a specific severity, you can use a configuration file (.hadolint.yaml) to do so. For example, to ignore all info-level issues, you could have the following in your config file:

     ignored:
         - info
    
  4. Configuring Severities in the Configuration File

    In the .hadolint.yaml file, you can specify how to treat different rules based on their severity. For example, you can configure certain rules to be treated as warnings or errors:

     override:
         warning:
           - DL3008 # Treat as a warning
    
         error:
           - DL3006 # Treat as an error
    
  5. Checking Severity Levels in Lint Output

    When Hadolint lints a Dockerfile, it outputs the severity level alongside each issue. This helps you quickly assess the importance of each issue.

    Example output:

     Dockerfile:2 DL3006 error: Always tag the version of an image explicitly
     Dockerfile:4 DL3008 warning: Pin versions in apt-get install
    

Understanding and effectively using severity levels in Hadolint allows you to tailor the linting process to your project's needs. You can enforce strict standards by treating warnings as errors or focus only on critical issues by ignoring lower severity levels. The configuration file offers a flexible way to set these preferences, ensuring that your Dockerfile linting aligns with your development practices and goals.

Integrations

Hadolint, being a popular tool for linting Dockerfiles, offers several types of integrations that enhance its utility in various development environments and workflows. These integrations help in automating the linting process, ensuring consistency and adherence to best practices across different stages of development and deployment. Here are some of the key integrations available with Hadolint:

1. Continuous Integration (CI) Services

Hadolint can be integrated into various Continuous Integration (CI) services to automatically lint Dockerfiles during the build process. This ensures that any issues are caught early in the development cycle. Common CI services where Hadolint can be integrated include:

  • GitHub Actions: Hadolint can be set up as a step in GitHub workflows to lint Dockerfiles on each commit or pull request.

  • GitLab CI: Similar to GitHub Actions, Hadolint can be configured as part of the GitLab CI pipeline.

  • CircleCI, Jenkins, Drone CI, Travis CI, and BitBucket Pipelines: Hadolint can be integrated into these services through respective configuration files or scripts.

2. Code Editors and IDEs

For developers, integrating Hadolint directly into their code editors or Integrated Development Environments (IDEs) provides immediate feedback on their Dockerfiles. Some popular integrations include:

  • Visual Studio Code: Extensions are available that integrate Hadolint into VS Code, allowing Dockerfile linting directly in the editor.

  • Sublime Text, Atom, and other editors: Similar integrations can be set up in various text editors, often through plugins or external tools.

3. Pre-commit Hooks

Hadolint can be used as a pre-commit hook in version control systems like Git. This ensures Dockerfiles are linted before each commit, helping to maintain code quality from the very start of the development process.

4. Docker

Hadolint itself is available as a Docker image, which means it can be run in a containerized environment. This is particularly useful for ensuring consistency across different machines or in environments where installing additional software is not preferred. Running Hadolint as a Docker container is a common approach in various automated pipelines and development setups.

5. Code Review Platforms

Hadolint can be integrated with various code review platforms such as Codacy, Codeship Pro and others. These platforms automates hadolint code reviews on every commit and pull request.

These integrations make Hadolint a versatile tool that can be adapted to various parts of the software development lifecycle. Whether it's during coding in an IDE, as part of a CI/CD pipeline, or during the pre-commit phase in version control, Hadolint can be integrated to maintain high-quality and standard-compliant Dockerfiles.

Tutorial: Set up Hadolint in GitHub Actions CI pipeline

Step 1: Create a GitHub Repository

If you don't already have a GitHub repository, create one. You can do this on the GitHub website by clicking on the "New repository" button.

Example repository is available at https://github.com/Brain2life/demo-hadolint-github-actions

Step 2: Add a Dockerfile to Your Repository

Create a simple Dockerfile in your repository. Here's an example you can use:

# Example Dockerfile
FROM ubuntu:latest
RUN apt-get update && apt-get install -y curl
CMD ["echo", "Hello, world!"]

Commit this Dockerfile to your repository.

Step 3: Set Up GitHub Actions

  1. In your GitHub repository, go to the "Actions" tab.

  2. Choose "set up a workflow yourself" or start from a template provided by GitHub.

Step 4: Create the Workflow File

GitHub will create a new .github/workflows directory in your repository with a starter YAML file for your workflow (usually named main.yml or something similar). Replace its contents with the following:

name: Lint Dockerfile

on:
  push:
    branches:
      - main
  pull_request:
    branches:
      - main

jobs:
  hadolint:
    name: Run Hadolint
    runs-on: ubuntu-latest

    steps:
      - name: Check out code
        uses: actions/checkout@v2

      - name: Run Hadolint
        uses: hadolint/hadolint-action@v3.1.0
        with:
          dockerfile: "Dockerfile"

This YAML file defines a workflow named "Lint Dockerfile" that triggers on push and pull requests to the main branch. It has a single job called hadolint, which checks out your code and then runs Hadolint on the Dockerfile.

Step 5: Commit Your Workflow File

Commit the workflow file to your repository. You can do this directly through the GitHub interface or by pushing it from your local machine.

Step 6: Trigger the Workflow

The workflow will trigger automatically on your next push to the main branch or when you create a pull request against main.

Step 7: View the Results

  1. After triggering the workflow, go back to the "Actions" tab in your GitHub repository.

  2. You'll see a list of workflow runs. Click on the latest one to view the details.

  3. Under "Jobs", you can see the result of the Hadolint action. If there are any linting issues, they will be displayed here.

    In our case you will see the failed pipeline, as GitHub Actions reacts to any severity level of the Hadolint output.

Step 8: Use custom configuration file
Now let's configure Hadolint in GitHub Actions workflow to react only to errors and ignore warnings and info-level messages. To achieve this we can use a custom Hadolint configuration file. This file allows to specify which rules to ignore or change their severity levels. Create a .hadolint.yaml configuration file in the root of your repository with the following content:

ignored:
  - DL3007 # Example of ignoring a specific rule
  - DL3008
  - DL3009
  - DL3015

override:  
  error:
    - DL3006 # Treat this rule as an error

  warning: [] # Empty array means treat no rules as warnings

  info: [] # Empty array means treat no rules as info

Modify your GitHub Actions workflow file to use the custom Hadolint configuration. The workflow file should look something like this:

name: Lint Dockerfile

on:
  push:
    branches:
      - main
  pull_request:
    branches:
      - main

jobs:
  hadolint:
    name: Run Hadolint
    runs-on: ubuntu-latest

    steps:
      - name: Check out code
        uses: actions/checkout@v2

      - name: Run Hadolint
        uses: hadolint/hadolint-action@v3.1.0
        with:
          dockerfile: "Dockerfile"
          # Add this line to specify the configuration file
          config: .hadolint.yaml

Step 9: Fail only on "error" severity level
Now let's make our hadolint fail only on "error" severity level messages. To do this we need to modify existing GitHub Actions workflow file (usually located in .github/workflows/ directory)

Here's what your workflow file might look like with the custom Hadolint step:

name: Lint Dockerfile

on:
  push:
    branches:
      - main
  pull_request:
    branches:
      - main

jobs:
  hadolint:
    name: Run Hadolint
    runs-on: ubuntu-latest

    steps:
      - name: Check out code
        uses: actions/checkout@v2

      - name: Run Hadolint with Config
        run: |
          docker run --rm -i -v $(pwd):/root hadolint/hadolint hadolint -c /root/.hadolint.yaml /root/Dockerfile

The -v $(pwd):/root option mounts the current working directory (your repository) to the /root directory inside the container. This makes your Dockerfile and the .hadolint.yaml file available inside the container.

The Hadolint command is then executed with the -c /root/.hadolint.yaml option to explicitly specify the configuration file path.

Modify your configuration file to:

ignored:
  - DL3007 # Example of ignoring a specific rule

override:  
  error:
    - DL3008 # Treat this rule as an error

After committing and pushing the changes we can see that CI pipeline failed on Hadolint step with rule ID DL3008:

Step 10: Verify best practice Dockerfile

To satisfy Hadolint's requirements and follow best practices, we'll update the Dockerfile. The updated Dockerfile will address the common issues typically flagged by Hadolint, such as using a specific version tag instead of latest, pinning versions of packages, and cleaning up the apt cache:

# Use a specific version of Ubuntu to ensure consistency.
# Replace '20.04' with the desired version as per your requirements.
FROM ubuntu:20.04

# Install curl with a specific version and clean up the apt cache to reduce the image size.
# Use `--no-install-recommends` to avoid installing unnecessary packages.
# Replace '7.68.0-1ubuntu2.5' with the version of curl available and compatible with your chosen Ubuntu version.
RUN apt-get update && \
    apt-get install -y --no-install-recommends curl=7.68.0-1ubuntu2.5 && \
    rm -rf /var/lib/apt/lists/*

# The CMD instruction does not need modification.
CMD ["echo", "Hello, world!"]

This Dockerfile should now meet the standards enforced by Hadolint, ensuring better practices in terms of security, stability, and maintainability.

Conclusion

You've successfully set up Hadolint in your GitHub Actions workflow. This setup will help you maintain high-quality Dockerfiles by automatically linting them on each push or pull request, ensuring that any potential issues are caught and resolved early in the development process.

References:

  1. https://github.com/hadolint/hadolint

  2. Using Hadolint, a dockerfile linter, to enforce best practices

  3. https://github.com/hadolint/hadolint/releases

  4. https://github.com/michaellzc/vscode-hadolint

  5. https://github.com/koalaman/shellcheck

  6. https://hadolint.github.io/hadolint/