Request change

How to Find and Fix Dockerfile Security Issues with Hadolint

How to use Hadolint to lint Dockerfiles, catch security issues and anti-patterns before build, fix common mistakes (version pinning, non-root users, exec-form CMD), and integrate checks into CI/CD—so you ship smaller, safer container images.

How to Find and Fix Dockerfile Security Issues with Hadolint

Docker Security & Container Best Practices with Hadolint

A comprehensive guide for learning Hadolint, a powerful Dockerfile linter that enforces security and best practices for container images.

Overview

This guide provides practical examples and exercises to help you:

  • Understand Docker security best practices
  • Learn how to use Hadolint to identify security vulnerabilities
  • Fix common Dockerfile issues
  • Build secure, optimized container images
  • Integrate Hadolint into CI/CD pipelines

What You’ll Learn

  • Security: Non-root users, version pinning, vulnerability prevention
  • Performance: Layer caching optimization, multi-stage builds
  • Best Practices: Proper Dockerfile structure and patterns
  • Tooling: Hadolint configuration and integration

Prerequisites

  • Basic understanding of Docker and Dockerfiles
  • Docker installed (or access to a system with Docker)
  • Command line experience

What is Hadolint?

Hadolint is a powerful Dockerfile linter written in Haskell that helps you write better, more secure, and more efficient Docker images. It analyzes your Dockerfile and provides recommendations based on best practices.

Key Features

  • Static Analysis: Analyzes Dockerfile syntax and structure without building the image
  • Bash Validation: Validates inline bash scripts using ShellCheck integration
  • Best Practices: Enforces Docker best practices and security guidelines
  • Rule-Based: Provides specific error codes (DLxxxx) for each issue found
  • CI/CD Integration: Easy to integrate into CI/CD pipelines
  • Multiple Output Formats: Supports JSON, TAP, and other output formats

Why Use Hadolint?

  1. Security: Identifies security vulnerabilities before deployment
  2. Performance: Suggests optimizations to reduce image size and build time
  3. Maintainability: Ensures consistent Dockerfile patterns across projects
  4. Quality: Catches common mistakes and anti-patterns early
  5. Documentation: Provides clear explanations for each issue found

Installation

docker pull hadolint/hadolint

To use hadolint with Docker:

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

Or create an alias for convenience:

echo 'alias hadolint="docker run --rm -i hadolint/hadolint"' >> ~/.bashrc
source ~/.bashrc

Linux/Ubuntu:

sudo wget -O /usr/local/bin/hadolint https://github.com/hadolint/hadolint/releases/latest/download/hadolint-Linux-x86_64
sudo chmod +x /usr/local/bin/hadolint

macOS:

wget -O /usr/local/bin/hadolint https://github.com/hadolint/hadolint/releases/latest/download/hadolint-Darwin-x86_64
chmod +x /usr/local/bin/hadolint
Verify Installation
hadolint --version

Basic Usage

hadolint [OPTIONS] Dockerfile

Common Options:

  • --ignore RULE: Ignore specific rules (e.g., --ignore DL3008)
  • --format FORMAT: Output format (tty, json, checkstyle, codeclimate, gitlab_codeclimate, sarif)
  • --failure-threshold LEVEL: Exit with failure code if issues >= LEVEL
  • --no-color: Disable colored output
  • --verbose: Enable verbose output
  • --config FILE: Path to configuration file

Basic Example:

hadolint Dockerfile
hadolint --format json Dockerfile
hadolint --ignore DL3008 --ignore DL3009 Dockerfile

Exercise 1: Dockerfile Without Issues (Dockerfile.good)

FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
RUN npm run build

FROM node:18-alpine
RUN addgroup -g 1001 -S nodejs && adduser -S nodejs -u 1001
WORKDIR /app
COPY --from=builder --chown=nodejs:nodejs /app/dist ./dist
COPY --from=builder --chown=nodejs:nodejs /app/node_modules ./node_modules
COPY --from=builder --chown=nodejs:nodejs /app/package*.json ./
USER nodejs
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 CMD node healthcheck.js
CMD ["node", "dist/index.js"]

Why This Dockerfile is Good:

  1. Specific base image (node:18-alpine instead of latest)
  2. Multi-stage build
  3. Layer caching (dependencies before code)
  4. Non-root user
  5. Health check
  6. Exec form for CMD
  7. Proper ownership with --chown

Exercise 2: Dockerfile With Common Issues (Dockerfile.bad)

FROM ubuntu:latest
RUN apt-get update
RUN apt-get install -y python3 python3-pip
RUN pip3 install flask requests
WORKDIR /app
COPY . .
RUN chmod +x start.sh
RUN ./start.sh
EXPOSE 8080
CMD python3 app.py

Common Hadolint Issues:

Rule Issue
DL3007 Using latest tag – pin version
DL3008 Pin versions in apt-get install
DL3009 Delete apt-get lists after installing
DL3013 Pin versions in pip install
DL3015 Use --no-install-recommends
DL3025 Use JSON notation for CMD
DL3042 Use pip install --no-cache-dir
DL3059 Consolidate consecutive RUN instructions

Issue 1: DL3006 – Avoid latest tag

Problem: FROM ubuntu:latest
Solution: FROM ubuntu:22.04

Issue 2: DL3008 – Pin versions in apt-get

Better:

RUN apt-get update && \
    apt-get install -y --no-install-recommends python3 python3-pip && \
    rm -rf /var/lib/apt/lists/*

Issue 3: DL3009 – Delete apt lists after install

Combine with apt-get in one RUN and add rm -rf /var/lib/apt/lists/*.

Issue 4: DL3013 – Pin versions in pip

Better: Use requirements.txt and:

COPY requirements.txt .
RUN pip3 install --no-cache-dir -r requirements.txt

Issue 5: DL3002 – Run as non-root

RUN groupadd -r appuser && useradd -r -g appuser appuser
# ... other instructions ...
USER appuser

Issue 6: DL3025 – Use exec form for CMD

Problem: CMD python3 app.py
Solution: CMD ["python3", "app.py"]

Fixed Dockerfile Example

FROM ubuntu:22.04
RUN apt-get update && \
    apt-get install -y --no-install-recommends python3 python3-pip && \
    rm -rf /var/lib/apt/lists/*
COPY requirements.txt .
RUN pip3 install --no-cache-dir -r requirements.txt
RUN groupadd -r appuser && useradd -r -g appuser appuser
WORKDIR /app
COPY --chown=appuser:appuser . .
USER appuser
EXPOSE 8080
CMD ["python3", "app.py"]

Multi-stage Build – Fixed Example

FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM node:18-alpine
WORKDIR /app
RUN addgroup -g 1001 -S nodejs && adduser -S nodejs -u 1001
COPY package*.json ./
RUN npm ci --only=production && npm cache clean --force
COPY --from=builder --chown=nodejs:nodejs /app/dist ./dist
USER nodejs
EXPOSE 3000
CMD ["node", "dist/index.js"]

Improvements: Specific versions, better caching, production deps only, non-root user, exec form CMD.

Layer Caching

Bad: COPY . . then RUN npm install – every code change invalidates cache.
Good: COPY package*.json ./RUN npm installCOPY . .

Combining RUN Commands

Bad: Multiple RUN (more layers).
Good: Single RUN apt-get update && apt-get install ... && rm -rf /var/lib/apt/lists/*

Using .dockerignore

Exclude node_modules, .git, .env, etc. to speed builds and avoid leaking secrets.

Health Checks

HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
  CMD curl -f http://localhost:3000/health || exit 1

Configuration File (.hadolint.yaml)

ignored:
  - DL3008
  - DL3013
override:
  error: DL3002
  warning: DL3006
trustedRegistries:
  - docker.io
  - gcr.io
  - quay.io

Use: hadolint --config .hadolint.yaml Dockerfile

GitHub Actions

name: Lint Dockerfile
on:
  pull_request:
    paths: ['Dockerfile*', '.dockerfilelintrc']
jobs:
  hadolint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Run Hadolint
        uses: hadolint/hadolint-action@v3.1.0
        with:
          dockerfile: Dockerfile
          failure-threshold: warning
          ignore: DL3008,DL3013

Pre-commit Hook

repos:
  - repo: https://github.com/hadolint/hadolint
    rev: v2.12.0
    hooks:
      - id: hadolint-docker
        args: ['--ignore', 'DL3008']

GitLab CI

hadolint:
  image: hadolint/hadolint:latest
  script: hadolint --ignore DL3008 --ignore DL3013 Dockerfile
  only: [merge_requests]

Output Formats & Failure Thresholds

hadolint --format json Dockerfile
hadolint --format checkstyle Dockerfile
hadolint --failure-threshold warning Dockerfile

Common Rules Reference

Rule Description Solution
DL3002 Last USER should not be root Create and use non-root user
DL3006 Tag image version explicitly Use e.g. ubuntu:22.04
DL3008 Pin apt-get versions apt-get install package=version
DL3013 Pin pip versions pip install package==version
DL3015 Avoid extra packages Use --no-install-recommends
DL3009 Delete apt lists after install rm -rf /var/lib/apt/lists/*
DL3025 Use JSON for CMD CMD ["executable", "arg"]
DL3027 Do not COPY as root Use COPY --chown=user:group

Summary

  • Security: Non-root users, version pinning, clean apt/pip usage
  • Performance: Layer order, multi-stage builds, consolidated RUN
  • Reliability: Reproducible builds with pinned versions
  • Maintainability: Consistent patterns and Hadolint in CI/CD

Resources: Hadolint GitHub · Docker Best Practices

Share
Helpful?

Request a change or update

Suggest a correction or content update. The post author or an admin will be notified and can resolve or respond.

Comments (1)

V
vishnusai.vks

Helpful nice and clear

Leave a comment