Linting and Testing Dockerfiles and Compose Files: hadolint, trivy, docker compose validate
AI generated
Docker · hadolint · trivy · Container Testing · CI/CD
Linting and Testing Dockerfiles and Compose Files
hadolint, trivy, and Container Structure Tests

Dockerfiles that are never statically checked accumulate silent problems: outdated base images with critical CVEs, invalid compose configurations, and structural issues that only surface once they hit production. Automated linting and testing closes that gap right inside the CI pipeline.

16 min read hadolint · trivy · compose validate · Container Structure Test · GitLab CI Docker 24+ · Docker Compose v2 · Linux

1. Why Dockerfile linting is more than a style check

Dockerfile linting is often misunderstood as a pure formatting check. In reality, a complete Dockerfile linting process uncovers three categories of problems: structural errors (wrong ordering, invalid syntax), security issues (root user, secrets in ENV, unpinned base images), and performance weaknesses (unnecessary layers, missing --no-install-recommends). A Dockerfile that passes the linter without warnings starts from a substantially higher quality baseline than one that has never been checked.

The argument that quality can be ensured through code review only holds up partially for Dockerfiles. Most developers reviewing PHP, Python, or JavaScript code are not Docker experts. They will not immediately notice that a RUN apt-get install without --no-install-recommends pulls in hundreds of megabytes of unnecessary packages, or that a particular ordering of COPY and RUN instructions invalidates the layer cache on every code change. Linting Dockerfiles automatically gives every reviewer that expertise.

Integrating Dockerfile linting into CI pipelines is not overhead, it is an investment: a hadolint run takes less than a second, and a trivy scan of a finished image takes less than a minute. In return, critical CVEs get caught before they can be exploited in production, and build problems surface for the developer instead of the ops team.

2. hadolint: Dockerfile linting with ShellCheck integration

hadolint (Haskell Dockerfile Linter) is the de facto standard for linting Dockerfiles. It does not just check Docker specific rules, it also integrates ShellCheck for every shell command inside RUN instructions. That means a bash mistake in a long RUN block is caught just as reliably as an incorrectly ordered COPY instruction. hadolint returns numbered warnings (for example DL3008 for unpinned apt packages, SC2086 for unquoted shell variables) that link directly to the official documentation.

hadolint ships as a single binary, a Docker image, and a GitHub Action. Configuration happens through a .hadolint.yaml file in the project root, where individual rules can be ignored or downgraded to "info" level. That lets teams take a pragmatic approach: fix all critical warnings (error level) first, then enable further rules step by step. Linting Dockerfiles with hadolint is the first step in every Docker CI pipeline.


# Install hadolint as a single binary (Linux amd64)
curl -L -o /usr/local/bin/hadolint \
  https://github.com/hadolint/hadolint/releases/latest/download/hadolint-Linux-x86_64
chmod +x /usr/local/bin/hadolint

# Basic lint, exit code 1 if errors are found
hadolint Dockerfile

# JSON output for CI artifact processing
hadolint --format json Dockerfile > hadolint-report.json

# Configure rule overrides in .hadolint.yaml
cat > .hadolint.yaml << 'EOF'
failure-threshold: error      # only fail on errors, not warnings
ignore:
  - DL3008                    # allow unpinned apt packages (team decision)
  - DL3018                    # allow unpinned apk packages
trustedRegistries:
  - registry.mironsoft.de     # suppress DL3002 for internal registry
EOF

# Run with config, warns but does not fail for ignored rules
hadolint --config .hadolint.yaml Dockerfile

# Scan all Dockerfiles in the project
find . -name "Dockerfile*" -not -path "./.git/*" \
  -exec hadolint --config .hadolint.yaml {} \;

3. trivy: vulnerability scanning for images and Dockerfiles

trivy from Aqua Security is the most widely used open source tool for container vulnerability scans. It checks finished images against known CVE databases (NVD, Alpine SecDB, Debian Security, RedHat, GitHub Advisories) and finds vulnerabilities in OS packages, Python packages, npm modules, Composer packages, and Java dependencies. A trivy scan returns the CVE ID, severity (CRITICAL, HIGH, MEDIUM, LOW), the affected version, and the available fix version, everything needed for a well informed risk assessment.

Beyond finished images, trivy can also check Dockerfiles directly for misconfigurations (trivy config Dockerfile), and it surfaces problems similar to hadolint, but with an added focus on security policies. The --exit-code 1 flag makes the scan fail with an error exit code when CRITICAL or HIGH vulnerabilities are found, ideal for CI pipelines that should stop a build on critical security issues. With --ignore-unfixed, vulnerabilities without an available fix are filtered out of the report, which sharpens the focus on what can actually be remediated.

4. docker compose config: validating Compose files

Docker Compose files in complex projects quickly grow to several hundred lines of YAML, with overrides, fragments, and environment substitutions. Invalid YAML syntax, incorrectly referenced services, or missing required fields only surface as errors once docker compose up is run. docker compose config validates and normalizes the compose configuration, resolves all overrides and variables, and prints the fully merged result. If the configuration is invalid, the command fails with a clear error message.

Linting Compose files with docker compose config --quiet is the fastest way to find syntactic and structural errors in Compose files without starting a single container. In CI pipelines, docker compose config --quiet && echo "Config valid" can be used as a preliminary step before the actual build. For teams working with multiple Compose files (base, override, dev, prod), this step is especially valuable, it confirms that the combined configuration is genuinely valid before it gets deployed.


# Validate and print merged compose config (resolves overrides and env vars)
docker compose -f compose.yaml -f compose.prod.yaml config

# Silent validation, exit code only (useful in CI)
docker compose config --quiet && echo "Compose config is valid"

# trivy: scan a built image for vulnerabilities
trivy image --exit-code 1 --severity CRITICAL,HIGH \
  --ignore-unfixed registry.mironsoft.de/myapp:latest

# trivy: scan Dockerfile for misconfigurations
trivy config --exit-code 1 Dockerfile

# trivy: generate SARIF report for GitHub/GitLab security dashboards
trivy image --format sarif --output trivy-results.sarif \
  registry.mironsoft.de/myapp:latest

# Container Structure Tests, verify image contents
# Install: go install github.com/GoogleContainerTools/container-structure-test@latest
container-structure-test test \
  --image registry.mironsoft.de/myapp:latest \
  --config structure-test.yaml

# Example structure-test.yaml
cat > structure-test.yaml << 'EOF'
schemaVersion: "2.0.0"
commandTests:
  - name: "PHP version check"
    command: "php"
    args: ["--version"]
    expectedOutput: ["PHP 8.4"]
fileExistenceTests:
  - name: "Entrypoint script exists"
    path: "/usr/local/bin/docker-entrypoint.sh"
    shouldExist: true
    permissions: "-rwxr-xr-x"
metadataTests:
  - user: "www-data"
    exposedPorts: ["9000"]
EOF

5. Container Structure Tests: checking content and behavior

Container Structure Tests (Google) go one step beyond static linting: they start the finished image and check its content and behavior. That covers four test types: command tests (run commands and check the output), file existence tests (files present with correct permissions), file content tests (check file contents with regex), and metadata tests (USER, EXPOSE, ENV, LABEL). The result is an automated quality contract for every image: once the image builds and all structure tests pass, it is known to meet the defined requirements.

Container Structure Tests are especially valuable for images that get handed off to other teams or external users. The test YAML doubles as a machine readable and human readable contract: "This image contains PHP 8.4, the www-data user, the entrypoint under /usr/local/bin, and it listens on port 9000." Changes to the Dockerfile that break this contract, for example an accidental upgrade to PHP 8.5 or a changed file structure, get caught immediately on the next CI run, without anyone having to inspect the image by hand.

6. dockle: CIS Benchmark and security best practices

dockle from Goodwith Technology complements trivy and hadolint with a focus on security best practices based on the CIS Docker Benchmark. It checks finished images against criteria that none of the other tools fully cover: whether the container runs as root, whether setuid binaries are present, whether healthcheck configurations are missing, whether content trust is enabled, and whether secrets are hiding in labels or environment variables. Warnings follow a category system ranging from FATAL to INFO.

In practice, dockle rounds out the linting pipeline: hadolint checks the Dockerfile source, trivy checks CVEs in packages, dockle checks the security configuration of the finished image. No single tool covers all three categories. Combining the three gives a holistic view of the quality and security of a Docker setup. dockle is available as a binary and as a Docker image, and it produces JSON output that integrates cleanly into CI dashboards.

7. A complete CI pipeline: all tools combined

A complete Docker linting and testing pipeline in GitLab CI runs in three phases: lint (hadolint plus compose config), build (docker build with BuildKit), and test (trivy, dockle, and Container Structure Tests). The lint phase runs fast without needing any images and rejects broken commits early. The test phase runs after the build and checks the finished artifact. Critical CVEs and security issues can set the pipeline status to "failed," while lower severities only produce warnings.

Caching is critical for this pipeline structure: Docker layer caching via DOCKER_BUILDKIT=1 and registry caching (--cache-from) keep build times acceptable even with full scans in place. trivy databases can be cached as a CI artifact to avoid repeated downloads. A Dockerfile linting pipeline like this gives teams confidence that every merge request produces a checked, tested image.


# .gitlab-ci.yml, complete Docker lint and test pipeline
stages:
  - lint
  - build
  - test

variables:
  DOCKER_BUILDKIT: "1"
  IMAGE_TAG: "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA"

dockerfile-lint:
  stage: lint
  image: hadolint/hadolint:latest-alpine
  script:
    - hadolint --config .hadolint.yaml Dockerfile
    - hadolint --config .hadolint.yaml docker/php/Dockerfile
  rules:
    - changes: ["Dockerfile", "docker/**/*"]

compose-validate:
  stage: lint
  image: docker:24-cli
  script:
    - docker compose -f compose.yaml -f compose.prod.yaml config --quiet
  rules:
    - changes: ["compose*.yaml", "docker-compose*.yml"]

build-image:
  stage: build
  image: docker:24
  services: [docker:24-dind]
  script:
    - docker build --cache-from "$CI_REGISTRY_IMAGE:latest"
        --tag "$IMAGE_TAG" .
    - docker push "$IMAGE_TAG"

trivy-scan:
  stage: test
  image: aquasec/trivy:latest
  script:
    - trivy image --exit-code 1 --severity CRITICAL,HIGH
        --ignore-unfixed --format sarif
        --output trivy.sarif "$IMAGE_TAG"
  artifacts:
    reports:
      sast: trivy.sarif
    expire_in: 7 days

structure-test:
  stage: test
  image: gcr.io/gcp-runtimes/container-structure-test:latest
  script:
    - container-structure-test test
        --image "$IMAGE_TAG"
        --config tests/structure-test.yaml

8. Existing images: baseline scans and exceptions

When introducing Dockerfile linting into an existing project, the first trivy scan result is often sobering: dozens of CVEs, many of them in transitive dependencies with no immediately available fix. The "fix everything or enable nothing" dilemma can be resolved with trivy's .trivyignore file: known, accepted vulnerabilities can be excluded with a CVE ID, an expiry date, and a justification. That prevents legitimate exceptions from keeping the CI pipeline permanently red, while new critical vulnerabilities still stand out immediately.

For hadolint, the same approach works through .hadolint.yaml and its ignore key. Teams starting out on an existing project with a hundred warnings can proceed step by step: enable only FATAL/ERROR level first, then add further rules weekly until the full baseline is reached. This iterative approach is more pragmatic than trying to fix every problem at once, and in practice it leads to much better buy in from the team.

9. Linting tools compared side by side

The four tools, hadolint, trivy, dockle, and Container Structure Tests, each have their own strengths and areas of application. They do not replace each other, they complement each other. A complete Docker linting stack uses all four in the right order.

Tool What it checks Input Notable feature
hadolint Dockerfile structure, shell syntax Dockerfile (source) ShellCheck built in, very fast (<1s)
trivy CVEs in OS and app packages Finished image or filesystem SARIF output, broadest CVE database
dockle CIS Benchmark, security config Finished image User, setuid, health, secrets in labels
Container Structure Test Files, commands, metadata Running image YAML test contracts, flexibly extensible
docker compose config Compose syntax and completeness compose.yaml Resolves overrides and variables

The sensible order in a CI pipeline: hadolint and compose validation first (fast, no image required), then the build, then trivy and dockle (need the finished image), and finally Container Structure Tests (need a running image). Teams that implement all five checks end up with a robust Dockerfile linting pipeline that safeguards structural, security related, and functional quality all at once.

Mironsoft

Docker CI/CD, container security, and DevOps automation

Want Dockerfile linting integrated into your CI pipeline?

We set up hadolint, trivy, dockle, and Container Structure Tests in your GitLab or GitHub pipeline and build a complete Docker quality assurance process that checks automatically on every commit.

Pipeline setup

Set up hadolint, trivy, and structure tests in GitLab CI or GitHub Actions

Baseline analysis

Scan existing images and Dockerfiles, build a baseline, and set priorities

Training

Team workshop on Docker best practices and how to interpret scan results

10. Summary

Automatically linting and testing Dockerfiles is not an extra, it is a fundamental part of a production ready container infrastructure. hadolint statically checks Dockerfiles for structural and shell errors. trivy finds CVEs in finished images and infrastructure code. dockle validates the security configuration against CIS benchmarks. Container Structure Tests verify the content and behavior of images as automated contracts. docker compose config validates compose configurations before deployment.

Introducing these tools does not have to happen all at once. A pragmatic starting point: enable hadolint with failure-threshold: error in CI, build a trivy baseline for existing images in parallel, and fix the most critical CVEs. Within two sprints, a team will have a working Dockerfile linting pipeline that checks automatically on every merge request and continuously improves the quality level of the container infrastructure.

Linting and testing Dockerfiles: the essentials at a glance

Static linting

hadolint for Dockerfiles (including ShellCheck). docker compose config --quiet for Compose files. Both run without a built image, in seconds.

Vulnerability scanning

trivy image with --exit-code 1 --severity CRITICAL,HIGH. SARIF output for security dashboards. .trivyignore for known exceptions.

Structure & security

Container Structure Tests as a machine readable image contract. dockle for CIS Benchmark checks. Both run after the build, against the finished image.

Rollout strategy

Step by step: error level first, then all rules gradually. Baseline scan for existing images. Document exceptions with an expiry date and justification.

11. FAQ: Linting and testing Dockerfiles and Compose files

1What is hadolint and why is it the standard?
A Haskell based Dockerfile linter with ShellCheck built in. Runs in under a second, has an extensive rule set, and offers configurable exceptions via .hadolint.yaml.
2Difference between trivy and dockle?
trivy: CVE database scans for packages and app dependencies. dockle: CIS Benchmark checks (user, setuid, healthcheck, secrets in env). The two complement each other, neither replaces the other.
3What are Container Structure Tests?
A Google tool that checks finished images for file contents, command output, and metadata. The YAML test file serves as an automatable image contract for teams and CI pipelines.
4How do I automatically validate docker-compose.yaml?
docker compose config --quiet checks syntax and completeness, resolves overrides, and returns exit code 1 on errors. Runs in seconds without a built image, ideal as a first CI step.
5Handling lots of trivy warnings?
.trivyignore for known CVEs with a justification and an expiry date. --ignore-unfixed filters out vulnerabilities that cannot be fixed. Step by step: CRITICAL first, then HIGH.
6hadolint in GitHub Actions / GitLab CI?
GitHub: hadolint/hadolint-action. GitLab: image: hadolint/hadolint:latest-alpine, script: hadolint Dockerfile. JSON output for dashboards. Both work out of the box.
7What is SARIF?
Static Analysis Results Interchange Format, a standardized JSON format for scan results. GitHub and GitLab visualize SARIF reports directly in their security dashboards. trivy --format sarif produces compatible output.
8When to use Structure Tests instead of manual inspection?
Whenever images are built regularly and defined quality requirements need to be met. The YAML test serves as documentation and an automatic test at the same time. Team work and CI make automation essential.
9Overhead of the linting pipeline?
hadolint: <1s. compose config: <1s. trivy: 30 to 90s (faster with cache). dockle: 10 to 30s. Structure Tests: <60s. Under 3 minutes total for a full check.
10Which hadolint rules matter most?
DL3007 (latest tag), DL3008/DL3018 (unpinned packages), DL3009 (apt-get update alone), DL3025 (shell form for CMD/ENTRYPOINT), DL4006 (missing pipefail), SC2xxx (ShellCheck rules).