Dockerfile 与 CI/CD 脚本阅读指南
配套课程:W03-D04|持续集成与 Docker:商城系统容器化交付
这份指南解决两个第一次接触时最常见的问题:Dockerfile 每一行到底在什么时候执行?.github/workflows/*.yml 为什么能让 GitHub 自动测试和构建?
今天不要求背完所有指令。目标是拿到一份 AI 生成或同事提交的配置后,能逐行说明它做了什么、依赖什么、哪里可能泄露密钥,以及它到底完成了 CI、交付还是部署。
1. 先把 CI 和 CD 分开
| 名词 | 英文 | 本周能做什么 | 不能自动宣称什么 |
|---|---|---|---|
| CI | Continuous Integration,持续集成 | 在干净 runner 上取指定 commit、安装依赖、运行测试 | 已部署、已上线、真实用户可访问 |
| Continuous Delivery | 持续交付 | CI 通过后构建可部署制品,例如带 commit SHA 的镜像或镜像归档 | 制品已经进入某台服务器 |
| Continuous Deployment | 持续部署 | 通过审批和环境保护后,把制品自动部署到明确目标环境 | 生产安全、零故障、无需人工治理 |
W03 的核心验收是 CI + Docker 本人实跑 + 可交接候选包。仓库没有冻结镜像仓库、云服务器、域名和生产密钥,因此不能把当前 GitHub Actions 绿色结果写成“已完成自动化生产部署”。
2. Dockerfile 的执行顺序
Docker 构建时从上到下执行 Dockerfile。先读本周后端的最小版本:
FROM python:3.12-slim
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1
WORKDIR /app
COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt
COPY app ./app
EXPOSE 8000
HEALTHCHECK --interval=10s --timeout=3s --retries=6 CMD python -c "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/health', timeout=2)"
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]FROM:从什么基础开始
FROM python:3.12-slim 选择带 Python 3.12 的精简 Linux 基础镜像。它不是“在你的 Windows 上安装 Python”,而是建立镜像内部的运行基础。
检查点:
- 版本是否和课程 Python 基线一致;
- 是否使用了含糊的
latest; - 镜像来源是否可信;
- 变更 tag 后是否重新跑完整测试。
ENV:设置镜像内的非敏感默认环境变量
反斜杠 \ 表示这一条指令换行继续。这里关闭 .pyc 写入并让 Python 日志及时输出。
数据库密码、API Key、真实连接串不能写进 ENV。Dockerfile 会进入 Git,构建历史和镜像元数据也可能暴露这些值。敏感值应在运行时通过受控环境注入。
WORKDIR:后续命令在哪个目录执行
WORKDIR /app 会为后面的 COPY、RUN 和默认启动命令设置工作目录。没有它时,相对路径容易落到错误位置。
第一次 COPY:先复制依赖清单
COPY requirements.txt ./这里只复制变化频率较低的依赖文件。只要 requirements.txt 没变,后面的依赖安装层就有机会复用构建缓存。
RUN:构建镜像时执行
RUN pip install --no-cache-dir -r requirements.txtRUN 发生在 docker build 阶段,结果进入镜像层。它不是容器每次启动时重新安装依赖。
第二次 COPY:再复制经常变化的业务代码
COPY app ./app业务代码改变时,只需从这一层之后重建。若先 COPY . . 再安装依赖,改一行 Python 也可能让依赖层缓存失效。
EXPOSE:声明端口,不负责发布端口
EXPOSE 8000 是镜像作者对运行者的说明。它不会自动完成主机端口映射。
真正让主机访问容器需要:
docker run -p 8000:8000 ...或在 Compose 中写 ports。
HEALTHCHECK:周期性判断进程是否健康
健康命令退出码为 0 时表示本轮检查成功,非 0 表示失败。interval 是检查间隔,timeout 是单次超时,retries 是连续失败次数阈值。
/health 只能证明最小服务可响应,不能证明下单事务、409 回滚和浏览器联调正确。
CMD:容器启动时的默认命令
JSON 数组写法称为 exec form。每一项分别是可执行文件或参数,不会先经过 shell 拼接:
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]这里必须监听 0.0.0.0,否则进程可能只接受容器内部回环地址的请求。
RUN、CMD、ENTRYPOINT 不要混
| 指令 | 发生时间 | 常见用途 |
|---|---|---|
RUN | 构建镜像时 | 安装系统包、安装 Python 依赖、生成构建产物 |
CMD | 容器启动时 | 提供默认启动命令或默认参数 |
ENTRYPOINT | 容器启动时 | 固定主要可执行程序,常与 CMD 默认参数配合 |
本周只需要读懂现有 RUN 和 CMD。不要为了“用全语法”强行加入 ENTRYPOINT。
3. .dockerignore 为什么和 Dockerfile 一起审查
docker build ... <context> 会把构建上下文交给 builder。.dockerignore 应排除:
.git
.venv
__pycache__
.pytest_cache
.env
evidence
本地数据库或大文件检查方法不是只看文件存在,而是观察 build context 大小,并在构建后检查镜像中是否误带密钥或本地垃圾文件。
4. GitHub Actions YAML 怎么读
本周 CI 文件放在 .github/workflows/。YAML 使用缩进表达层级,不能混用 Tab。先看最小结构:
name: w03-contract-unit
on:
push:
pull_request:
jobs:
test:
runs-on: ubuntu-latest
defaults:
run:
working-directory: backend
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
cache: pip
- run: python -m pip install -r requirements.txt
- run: python -m pytest tests/unit tests/contract -q逐层解释:
| 键 | 作用 | 审查问题 |
|---|---|---|
name | Actions 页面显示的工作流名称 | 能否一眼看出它验证什么 |
on | 触发事件 | push、PR 还是手工触发;是否会造成无意义重复运行 |
jobs | 可并行或有依赖关系的任务集合 | 测试、构建、部署是否被错误混成一个黑盒 |
test | 当前 job 的自定义 ID | needs 引用时必须一致 |
runs-on | runner 操作系统 | 和本地环境差异是什么 |
defaults.run.working-directory | 后续 run 命令的默认目录 | 路径是否真实存在 |
steps | job 内按顺序执行的步骤 | 哪一步失败,后面就不会继续 |
uses | 调用可复用 Action | 来源、主版本和权限是否可信 |
with | 给 Action 传参数 | Python 版本是否与项目一致 |
run | 在 runner shell 中执行命令 | 本地能否先复现;是否掩盖退出码 |
${{ github.sha }} 表示当前提交 SHA。${{ secrets.NAME }} 表示从受保护 Secret 读取值。Secret 不应写进 YAML、echo 到日志,也不应发给 AI。
5. 从 CI 到交付:为什么要用 needs
配套的 CI/CD 脚本示例 把流程拆成:
test
└─通过后→ build-image
└─生成带 commit SHA 的镜像归档build-image 使用 needs: test,测试失败时不构建候选制品。镜像 tag 使用 commit SHA,避免只看到 latest 却不知道对应哪份代码。
这仍属于 Continuous Delivery 示例,不是生产部署。真正的 deploy job 至少还需要:
- 明确目标环境和负责人;
- 镜像仓库或受控制品来源;
- 受保护的环境变量和 Secret;
- 审批规则;
- 数据库迁移策略;
- 健康检查、失败停止和回滚;
- 部署后的 smoke test;
- 日志、监控和审计记录。
这些事实没有冻结时,不要让 AI 猜一个 IP、拼一段 SSH 命令就声称“CD 完成”。
deploy job 应该怎样读
下面是结构阅读模板,不是本周可直接运行的生产答案:
deploy-staging:
needs: build-image
if: github.event_name == 'workflow_dispatch'
runs-on: ubuntu-24.04
environment:
name: course-staging
concurrency:
group: course-staging
cancel-in-progress: false
steps:
- uses: actions/download-artifact@v4
with:
name: w03-commerce-${{ github.sha }}
- name: Deploy with the approved environment adapter
env:
DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }}
DEPLOY_USER: ${{ secrets.DEPLOY_USER }}
run: ./ops/deploy.sh w03-commerce-image.tar
- name: Smoke test the deployed service
env:
HEALTH_URL: ${{ vars.HEALTH_URL }}
run: curl --fail --show-error --silent "$HEALTH_URL"逐行检查:
needs:只有候选镜像构建成功才部署;workflow_dispatch:示例要求人工触发,不跟每次 push 自动上线;environment:让平台应用环境审批和 Secret 保护;concurrency:防止同一环境两个部署互相覆盖;download-artifact:取回同一 commit 的制品;secrets:只在运行时注入,不能回显;deploy.sh:是环境适配器,不存在时 job 必须失败,不能让 AI 假装执行;- smoke test:部署后从目标地址做最小真实检查;
- 仍缺回滚:真实项目必须再定义旧版本、失败判定和恢复动作。
课堂只要求学生能读懂这张骨架、指出缺少哪些环境事实。等目标环境、ops/deploy.sh 和 Secret 管理冻结后,才能把它变成可执行作业。
6. 课堂代码审查顺序
拿到 AI 生成的 Dockerfile 或 workflow 后,按这个顺序检查:
- 对照项目版本和目录,不先运行;
- 标出构建期、启动期和运行期变量;
- 检查密钥、
.env、数据库文件是否可能进入镜像或日志; - 检查测试 job 与 build job 的依赖;
- 本地运行测试;
- 构建镜像;
- 运行 health、log、volume 证据;
- 再看 CI 的同一 commit;
- 没有目标环境时把 deploy 标为“未实施”,不要伪造截图。
7. 面试追问
RUN和CMD的执行时机有什么不同?- 为什么先复制
requirements.txt,再复制业务代码? EXPOSE 8000为什么不等于主机能访问 8000?- GitHub Actions 中
uses和run有什么区别? needs: test解决什么问题?- CI 绿色为什么不能证明已经部署?
- Continuous Delivery 与 Continuous Deployment 的差别是什么?