Appearance
Hadolint — Dockerfile Linting
Hadolint is a Dockerfile linter that enforces best practices, catches common mistakes, and ensures Dockerfiles follow security-hardening guidelines. It runs as an early pipeline stage (Stage 2B) immediately after the source checkout and before the Docker build.
Positioning in the Pipeline
Pipeline Stages
──────────────────────────────────────────────────────────────
Stage 1: Gitleaks — Secret scanning
Stage 2: Build Prep — Restore, lint
Stage 2A: dotnet restore / npm install
Stage 2B: Hadolint — Dockerfile lint ◄──
Stage 3: SonarCloud — SAST
...
Stage 8: Docker Build — Multi-stage Dockerfile
──────────────────────────────────────────────────────────────Linting before building catches Dockerfile issues without spending 5–10 minutes on the build, giving developers faster feedback.
What Hadolint Checks
Hadolint checks fall into two categories: Hadolint rules (DL*) and ShellCheck rules (SC*) for RUN instructions.
Key Rules Enforced
| Rule | Severity | Description |
|---|---|---|
DL3002 | Warning | USER root — do not switch to root user |
DL3006 | Warning | Missing tag in FROM — always pin image versions |
DL3007 | Warning | latest tag in FROM — use specific version |
DL3008 | Warning | apt-get packages without fixed version |
DL3009 | Info | apt-get lists should be cleaned up after install |
DL3025 | Warning | Use COPY instead of ADD for local files |
DL3033 | Warning | yum packages without fixed version |
DL3045 | Info | COPY to a relative path without WORKDIR set |
DL3047 | Info | wget without --progress=dot:giga flag |
DL4001 | Warning | wget and curl both present in the same image |
DL4006 | Warning | set -o pipefail missing in multi-pipe shell commands |
SC2086 | Style | Double-quote shell variables to prevent word splitting |
Pipeline Integration
Stage 2B — Hadolint task:
yaml
- stage: Lint
displayName: 'Stage 2B - Dockerfile Lint (Hadolint)'
dependsOn: []
jobs:
- job: Hadolint
pool:
vmImage: 'ubuntu-latest'
steps:
- checkout: self
- task: Bash@3
displayName: 'Run Hadolint on all Dockerfiles'
inputs:
script: |
# Find all Dockerfiles in the repo
DOCKERFILES=$(find . \
-name 'Dockerfile' \
-o -name 'Dockerfile.*' \
| grep -v node_modules \
| grep -v '.git')
echo "Found Dockerfiles:"
echo "$DOCKERFILES"
FAILED=0
for dockerfile in $DOCKERFILES; do
echo "──── Linting: $dockerfile ────"
docker run --rm \
-v "$(Build.SourcesDirectory):/work" \
hadolint/hadolint:latest-debian \
hadolint \
--config /work/.hadolint.yaml \
--format sarif \
"/work/${dockerfile#./}" \
| tee "$(Build.ArtifactStagingDirectory)/hadolint-$(basename $(dirname $dockerfile)).sarif"
if [ "${PIPESTATUS[0]}" -ne 0 ]; then
FAILED=1
fi
done
exit $FAILED
- task: PublishBuildArtifacts@1
condition: always()
inputs:
pathToPublish: '$(Build.ArtifactStagingDirectory)'
artifactName: 'security-hadolint'Configuration File (.hadolint.yaml)
The Hadolint configuration file is placed at the repository root and customizes which rules are enforced and which are ignored:
yaml
# .hadolint.yaml — repository root
failure-threshold: warning # Fail on warning or higher (not info/style)
# ── Rules to ignore (with justification) ────────────────────────
ignore:
# DL3008: apt-get packages without version — acceptable in build stage only
# We pin the base image version; apt package versions are transitive
- DL3008
# DL3009: Delete apt-get lists — handled by multi-stage build
# The runtime stage never includes apt caches
- DL3009
# SC2016: Expressions in single quotes — used intentionally in heredocs
- SC2016
# ── Trusted base image registries ────────────────────────────────
trustedRegistries:
- mcr.microsoft.com # Microsoft Container Registry (dotnet images)
- quay.io # Red Hat Quay (Keycloak)
# ── Per-rule overrides ────────────────────────────────────────────
override:
error:
- DL3002 # USER root must always be an error (not just warning)
warning:
- DL3007 # latest tag in FROM
- DL4006 # Missing pipefailSeverity Levels
| Level | failure-threshold | Pipeline Effect |
|---|---|---|
error | error | Blocks regardless |
warning | warning | Blocks (our current threshold) |
info | info | Blocks only at info threshold |
style | style | Blocks only at style threshold |
| ignored | — | Never shown |
Setting failure-threshold: warning means info and style findings are reported but do not block the pipeline. All warning and error findings block the build.
Per-File Inline Ignores
For specific Dockerfile lines where a rule genuinely does not apply, use inline ignore comments:
dockerfile
# Use --no-install-recommends to reduce image size (DL3015 — ignore for this project)
# hadolint ignore=DL3015
RUN apt-get install -y --no-install-recommends curl
# We intentionally use ADD here for URL fetch (DL3020)
# hadolint ignore=DL3020
ADD https://example.com/config.tar.gz /config/Use Inline Ignores Sparingly
Inline ignores bypass the rule for that specific line. Prefer fixing the Dockerfile to comply with the rule. Add a comment explaining why the rule is being ignored. Do not use broad hadolint ignore=all annotations.
Common Violations and Fixes
DL3002 — Running as Root
dockerfile
# BAD
FROM mcr.microsoft.com/dotnet/aspnet:8.0
# Runs as root by default
# GOOD
FROM mcr.microsoft.com/dotnet/aspnet:8.0
RUN groupadd -r appgroup && useradd -r -g appgroup appuser
USER appuserDL3006 — Untagged FROM
dockerfile
# BAD
FROM mcr.microsoft.com/dotnet/aspnet
# GOOD
FROM mcr.microsoft.com/dotnet/aspnet:8.0DL3007 — latest Tag
dockerfile
# BAD
FROM mcr.microsoft.com/dotnet/aspnet:latest
# GOOD — pin to specific minor version
FROM mcr.microsoft.com/dotnet/aspnet:8.0.5DL4006 — Missing pipefail
dockerfile
# BAD
RUN wget https://example.com/file.tar.gz | tar xz
# GOOD
RUN set -o pipefail && \
wget https://example.com/file.tar.gz | tar xzDL3025 — ADD Instead of COPY
dockerfile
# BAD — ADD has implicit tar extraction and URL support (unexpected behavior)
ADD ./configs /app/configs
# GOOD — COPY is explicit and predictable
COPY ./configs /app/configsDL3045 — Relative COPY Without WORKDIR
dockerfile
# BAD — where does the file go?
COPY config.json .
# GOOD
WORKDIR /app
COPY config.json .Running Hadolint Locally
bash
# Install (macOS)
brew install hadolint
# Lint a single Dockerfile
hadolint Platforms/Src/Gateway/Dockerfile
# Lint all Dockerfiles in the repo
find . -name Dockerfile | xargs hadolint --config .hadolint.yaml
# JSON output (for IDE integration)
hadolint --format json Dockerfile
# Using Docker (no local install required)
docker run --rm -i hadolint/hadolint < DockerfileIDE Integration
| IDE | Plugin |
|---|---|
| VS Code | exiasr.hadolint extension |
| JetBrains Rider | Built-in Dockerfile inspection |
| Vim | ale with hadolint linter |
The VS Code extension reads .hadolint.yaml automatically and shows inline warnings.
Reporting
Hadolint SARIF output is published as a pipeline artifact (security-hadolint) and can be imported into Azure DevOps Advanced Security:
bash
# Upload SARIF to Azure DevOps Advanced Security
az devops security sarif upload \
--project erp \
--sarif-file hadolint-Gateway.sarif \
--build-id $(Build.BuildId)Related Documentation
- Pipeline Stages — Full 16-stage DevSecOps pipeline
- Build Docker — How Dockerfiles are built after linting
- Trivy — Container image scanning after build
- Gitleaks — Stage 1 secret scanning