第四章:Dockerfile 编写规范归纳总结
好,聊完了前面的环境搭建和版本控制,咱们今天得把焦点落在最核心的生产资料上——Dockerfile。
说实话,Dockerfile 写得好不好,直接决定了镜像“能不能复现”。我见过太多团队,环境一换,构建就炸,翻来覆去查 Dockerfile,最后发现是几个基本原则没守住。今天我就把这些年踩过的坑和总结出的规范,一次性梳理出来。
可重复性的核心挑战
你想想看,一份 Dockerfile 怎么就能确保“在任何时间、任何机器上都能构建出完全一样的镜像”?
核心问题其实就三件:
- 基础镜像变了——今天拉的和上周拉的 `latest` 可能不是同一个版本。
- 软件源变了——`apt-get update` 的结果每次都不一样。
- 构建上下文脏了——本地开发目录里乱七八糟的文件被打进了镜像。
嗯,这三个问题不解决,什么 CI/CD 都是空谈。
Dockerfile 编写规范“三大纪律”
我个人习惯把这套规范总结成三条硬规矩。只要遵守了,可重复性基本就有保证了。
第一纪律:锁定一切源
我最开始写 Dockerfile 的时候,也爱用 `:latest`。直到有一次生产构建失败,发现是因为 `python:3.9-slim` 悄悄更新了底层 glibc 版本。没别的,直接用具体 SHA256 摘要来拉基础镜像。这招最保险。
为什么不只用 tag?因为 tag 能被人为覆盖,而 SHA256 是唯一的。
# 错误的写法
FROM ubuntu:22.04
# 正确的写法(推荐)
FROM ubuntu@sha256:aabed3296a3d45cede1dc866a24476c4d7e093aa80626351ec67e8c4e2563b10
如果你觉得每次查 SHA256 太麻烦,我建议你通过项目里的一个 `Makefile` 来自动化这件事,或者用锁文件工具来校验。总而言之,不要相信 tag。
第二纪律:最小化与有序化
每一行 `RUN` 都会产生一个新的镜像层。层数多了不仅构建慢,还会增大镜像体积。我自己写 Dockerfile 时一般要求 BASE 层数加 RUN 层总和不超过 6 层。这是个经验值,大家可以参考。
还有个常见错误:把 `apt-get update` 和 `apt-get install` 分开写。这样做会让 `update` 层被缓存,导致第二天构建时安装的软件包可能是旧的。你应该将它们合并成一个 RUN 指令。
# 错误:分开写,update 层缓存失效风险高
RUN apt-get update
RUN apt-get install -y curl vim
# 正确:合并写,并在中间执行清除
RUN apt-get update && \
apt-get install -y curl vim && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
注意,最后一定要清理缓存。我见过很多人的镜像一层比一层大,就是因为忘了清 `/var/lib/apt/lists/*`。
第三纪律:构建上下文只放需要的
`.dockerignore` 文件是很多人容易忽略的。如果没有它,你可能会把本地的 `.git` 目录、`node_modules`、甚至是密码文件一起打包进镜像。不仅泄露风险大,还导致镜像构建缓慢。
举个例子,你的项目结构可能是这样的:
.dockerignore
Dockerfile
src/
tests/
.git/
.idea/
venv/
*.pyc
*.log
__pycache__
我在项目中遇到过最离谱的案例:有人把整个 `venv` 目录打进了镜像,镜像体积直接飙到 2.3 GB。嗯,从那以后,我对 `COPY . /app` 这种写法就格外警惕。你得明确告诉 Docker:只复制必须的文件。
规范落地工具与检查清单
光说不练假把式。我建议大家在 CI 流水线里加一个静态检查步骤。当然,也可以用一些轻量级工具来帮忙。
下面是我自己项目里常用的一个检查表,每次合并 MR 前都要过一遍:
| 检查项 | 说明 |
|---|---|
| 基础镜像锁定 | 是否使用了 SHA256 摘要而非 `:latest` 等可变标签 |
| 层数控制 | RUN 指令是否合理合并,最终层数是否超过 6 层 |
| APT/YUM 缓存清理 | `apt-get clean` 或 `yum clean all` 是否出现在同一层里 |
| .dockerignore 存在 | 必须排除不必要的文件和目录 |
| 多阶段构建 | 是否使用多阶段构建分离构建环境和运行环境 |
| 版本号显式声明 | `ARG` 或 `ENV` 里的版本号是否在 Dockerfile 头声明 |
知识图谱总览
为了让你更直观地理解各规范之间的关系,我画了张图。你看,一个规范的 Dockerfile 从左侧的锁定源开始,逐步走向右侧的可重复构建。
一个典型的规范 Dockerfile 示例
说了这么多,不如直接看一个完整例子。注意看它是怎么把前面那些规则串起来的。
# 1. 锁定基础镜像
FROM python:3.11-slim@sha256:abc123def456... AS builder
# 2. 显式声明版本号
ARG POETRY_VERSION=1.8.0
# 3. 层数合并与缓存清理
RUN apt-get update && \
apt-get install -y --no-install-recommends build-essential libssl-dev && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
# 4. 安装依赖
WORKDIR /build
COPY pyproject.toml poetry.lock ./
RUN pip install --no-cache-dir poetry==${POETRY_VERSION} && \
poetry config virtualenvs.create false && \
poetry install --only main
# 5. 多阶段构建:清掉构建依赖
FROM python:3.11-slim@sha256:abc123def456...
# -- 从 builder 阶段复制编译好的三方库
COPY --from=builder /usr/local/lib/python3.11/site-packages /usr/local/lib/python3.11/site-packages
COPY ./app /app
# 6. 最后用非 root 用户运行
RUN useradd -m appuser
USER appuser
CMD ["python", "/app/main.py"]
总结一下我个人的习惯
其实说白了,Dockerfile 的规范就是“说清楚、重复做、别带垃圾”。
- 说清楚——用 SHA256 和显式版本号,不用模糊的 tag。
- 重复做——确保每一步都在相同条件下执行,包括 APT 源、pip 源、缓存清理。
- 别带垃圾——.dockerignore 要严格,多阶段构建要彻底,构建环境中的不必要的文件千万别带到生产镜像里。
嗯,今天就先聊这么多。希望这份总结能帮你少走些弯路,让“可重复验证”不再停留在口号上。