Dockerfile 与 CI/CD 脚本阅读指南

配套课程:W03-D04|持续集成与 Docker:商城系统容器化交付

这份指南解决两个第一次接触时最常见的问题:Dockerfile 每一行到底在什么时候执行?.github/workflows/*.yml 为什么能让 GitHub 自动测试和构建?

今天不要求背完所有指令。目标是拿到一份 AI 生成或同事提交的配置后,能逐行说明它做了什么、依赖什么、哪里可能泄露密钥,以及它到底完成了 CI、交付还是部署。

1. 先把 CI 和 CD 分开

名词英文本周能做什么不能自动宣称什么
CIContinuous 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.txt

RUN 发生在 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

逐层解释:

键作用审查问题
nameActions 页面显示的工作流名称能否一眼看出它验证什么
on触发事件push、PR 还是手工触发;是否会造成无意义重复运行
jobs可并行或有依赖关系的任务集合测试、构建、部署是否被错误混成一个黑盒
test当前 job 的自定义 IDneeds 引用时必须一致
runs-onrunner 操作系统和本地环境差异是什么
defaults.run.working-directory后续 run 命令的默认目录路径是否真实存在
stepsjob 内按顺序执行的步骤哪一步失败,后面就不会继续
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 至少还需要:

  1. 明确目标环境和负责人;
  2. 镜像仓库或受控制品来源;
  3. 受保护的环境变量和 Secret;
  4. 审批规则;
  5. 数据库迁移策略;
  6. 健康检查、失败停止和回滚;
  7. 部署后的 smoke test;
  8. 日志、监控和审计记录。

这些事实没有冻结时,不要让 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 后,按这个顺序检查:

  1. 对照项目版本和目录,不先运行;
  2. 标出构建期、启动期和运行期变量;
  3. 检查密钥、.env、数据库文件是否可能进入镜像或日志;
  4. 检查测试 job 与 build job 的依赖;
  5. 本地运行测试;
  6. 构建镜像;
  7. 运行 health、log、volume 证据;
  8. 再看 CI 的同一 commit;
  9. 没有目标环境时把 deploy 标为“未实施”,不要伪造截图。

7. 面试追问

  1. RUN 和 CMD 的执行时机有什么不同?
  2. 为什么先复制 requirements.txt,再复制业务代码?
  3. EXPOSE 8000 为什么不等于主机能访问 8000?
  4. GitHub Actions 中 uses 和 run 有什么区别?
  5. needs: test 解决什么问题?
  6. CI 绿色为什么不能证明已经部署?
  7. Continuous Delivery 与 Continuous Deployment 的差别是什么?

官方资料