hadolint — 给 Dockerfile 做体检的小工具
是什么
hadolint 是一个专门读你的 Dockerfile,告诉你哪里写得不规范的命令行工具。日常类比:像写完作文交给老师,老师把红笔圈出来——这里少了句号、那里错别字、这段太啰嗦。
举个最简单的用法:
hadolint Dockerfile它会输出类似:
Dockerfile:5 DL3008 warning: Pin versions in apt-get installDockerfile:7 DL4006 warning: Set the SHELL option -o pipefail before RUN每一条都告诉你第几行、什么规则、严重等级。你拿这个去改,改完再跑一遍直到没警告。
为什么重要
Dockerfile 是构建容器镜像的”配方”,写得不好会出三类问题:
- 构建失败的不稳定性:
apt-get install curl不锁版本,今天装的是 7.81,下个月重建变成 8.0,你的程序行为可能就变了 - 镜像变胖、变慢:每写一个
RUN就多一层,10 个 RUN 就 10 层,镜像 push/pull 都更慢 - 运行时坑:忘了
-o pipefail,shell 命令里curl x | tar y中 curl 失败但 tar 成功,构建照常通过,部署上去才发现问题
hadolint 把这些”老司机才知道的坑”编码成 100+ 条规则,新手装一下就能避掉大半。
核心要点
1. 两套规则系统
DL前缀:hadolint 自己的 Dockerfile 规则(DL3008、DL3018、DL4006…)SC前缀:内嵌 ShellCheck 的 bash 规则——因为RUN后面写的是 shell 脚本,hadolint 直接把那段交给 ShellCheck 再查一遍
2. 三步工作流程
- 读 Dockerfile → 解析成抽象语法树(AST,类比拆成”第 N 行是什么类型的指令”的列表)
- 遍历 AST,每条规则像一个检查员盯着自己关心的指令类型
- 输出违规清单(行号 + 规则号 + 严重等级 + 描述)
3. 用 Haskell 写
为什么是 Haskell?两个原因:
- Haskell 的代数数据类型(ADT)天然适合表达”指令有 FROM / RUN / COPY … 这十几种”
- 它复用了一个叫
language-docker的 Haskell 包,那个包专门做 Dockerfile 解析
新手不必学 Haskell 才能用 hadolint——它发布单文件可执行(连 musl 静态链接都做好),下载就跑。
实践案例
案例 1:DL3008 锁版本
不规范写法:
RUN apt-get update && apt-get install -y curlhadolint 抱怨:DL3008 Pin versions in apt-get install。
规范写法:
RUN apt-get update && apt-get install -y curl=7.81.0-1ubuntu1.15锁版本好处:明年重建镜像,结果完全一样。坏处:版本被下架时构建会挂——所以工程上常配合”定期升级 + lockfile 提交到仓库”的节奏。
案例 2:DL4006 pipefail 救命
RUN curl -L https://example.com/x.tar.gz | tar xz如果 curl 失败(404),管道左边是错的,但 tar 收到空输入也会”成功退出”——构建通过,镜像里没东西,部署上去才崩。
hadolint 提示 DL4006。修复:
SHELL ["/bin/bash", "-o", "pipefail", "-c"]RUN curl -L https://example.com/x.tar.gz | tar xz案例 3:CI 集成
GitHub Actions 里加一步:
- uses: hadolint/hadolint-action@v3.1.0 with: dockerfile: DockerfilePR 里只要 Dockerfile 改了,自动跑一遍,不规范直接 fail PR。
踩过的坑
-
规则太严,全开会被淹没:默认 100+ 条规则,老项目一开全报红。实战做法:建
.hadolint.yaml,先 ignore 一批,逐步降低 noise -
DL3008 锁版本和”自动升级”冲突:锁了版本之后 Renovate / Dependabot 就管不了,需要单独写一个升级机器人盯 Dockerfile(很多团队最后选择只锁基础镜像 tag,不锁 apt 版本)
-
# hadolint ignore=DL3008行内豁免:能在某一行临时关规则,但写多了等于没规则——只在”已知技术债 + 写明原因”时用 -
shell 不是 bash 时漏检:默认按 POSIX sh 检查,写了 bash-only 语法(如
[[ ]])会被误报;用# hadolint shell=bash注释告诉它
适用 vs 不适用场景
适用:
- 任何用 Docker 构建镜像的项目(个人 / 团队 / 开源都值得加)
- CI 流水线门禁——比代码 review 时人肉抓更省心
- 团队约定 Dockerfile 风格的”自动执行版”——靠 lint 而不是 wiki 文档
不适用:
- Windows 容器 / PowerShell 镜像——支持有限,规则覆盖窄
- 镜像安全扫描(CVE / 漏洞)——那是 trivy / grype 的活,hadolint 只看”写法”
- 镜像体积优化建议——只检查指令规范,不分析最终镜像大小(那是 dive / docker-slim)
学到什么
- lint 工具的本质就三步:解析成 AST → 遍历检查 → 输出报告。所有 lint(eslint / clippy / hadolint)都是这个套路
- 复用比自造强:hadolint 没自己写 bash 解析器,直接调 ShellCheck——把”我懂 Dockerfile”和”我懂 bash”两件事拆开各做各的
- 规则编号是契约:
DL3008这种代码不光是给人看,还是配置文件、行内豁免、CI 报错时的稳定 ID。改名等于破坏向后兼容 - lint 必须能定制:默认严格能把人逼疯,所以
.hadolint.yaml让你 ignore / 降级——工具想被用,必须给一个”逐步治理”的滑梯 - 小工具也能赢:核心代码不到 5000 行 Haskell,没炫技、没大架构,就是”读 Dockerfile 报错”这一件事做到位,照样长到 12k 星——专一比全能更稀缺
延伸阅读
- 在线试一下:hadolint.github.io(粘贴 Dockerfile 立刻看检查结果)
- 完整规则列表:Rules wiki
- 配套:ShellCheck 项目本体 shellcheck.net
- shellcheck —— hadolint 内嵌的 bash linter
- docker-engine —— Dockerfile 是给它读的
关联
- shellcheck —— hadolint 复用它检查 RUN 里的 bash
- dockerfile-best-practices —— hadolint 规则的来源就是这份官方最佳实践
- trivy —— 互补:hadolint 看写法,trivy 看安全漏洞
- pre-commit —— 常见集成方式,把 hadolint 挂到 git commit 钩子