GitHub Actions + Docker + ACR + GHCR 自动化构建与发布

GitHub Actions + Docker + ACR + GHCR 自动化构建与发布

GitHub Actions + Docker + 阿里云 ACR + GitHub Container Registry(GHCR)自动化构建与发布工作流。

一、先理解 CI/CD 到底是什么

  • CI(持续集成):代码提交后,自动检查代码是否有问题、能否构建、基本功能能否运行。
  • CD(持续交付 / 持续部署):把验证通过的程序交付出去,例如发布 Docker 镜像;如果进一步自动更新生产服务器,才是完整的自动部署流程。

你的这个工作流已经包含了自动测试和镜像发布,但从这份 YAML 来看,它没有直接登录你的生产服务器,也没有执行服务器上的 docker compose pull 或 docker compose up -d。所以它完成的是"构建、测试、发布镜像",不是自动更新运行中的服务。

GitHub Actions 的核心机制是:事件触发工作流,工作流运行一个或多个 Job,每个 Job 再依次执行 Steps。

二、你的工作流具体做了什么?

查看这份文件。它只有一个 Job,但包含多个按顺序执行的步骤。

阶段 你的工作流做什么 目的
1. 触发 监听 main 分支的 push,也支持手动运行 自动启动流水线
2. 获取代码 Checkout 仓库 把代码下载到临时机器
3. 前置检查 检查文件和 ACR 配置 提前发现配置错误
4. 代码检查 设置 Python 3.12,检查 Python 语法 避免低级错误
5. 构建应用镜像 构建 Flask/Python 应用镜像 验证应用能否打包
6. 冒烟测试 启动应用容器,访问 /health 验证应用基本可用
7. 构建 Nginx 镜像 构建 Web 入口镜像 验证 Nginx 能否打包
8. 配置检查 执行 nginx -t 检查 Nginx 配置
9. 镜像发布 登录两个仓库,推送镜像 发布可供部署的镜像

这里有一个特别值得学习的设计:测试通过之前,不会执行最后的镜像发布步骤。 如果前面的步骤失败,后续普通步骤默认会跳过。

1. 为什么是两个镜像?

应用镜像(app)

包含 Python 应用及依赖,负责文件服务器的后端逻辑。

示例:file-server-app-ci:版本号

Web 镜像(nginx)

包含 Nginx 和相关配置,负责 Web 入口、静态页面或反向代理。

示例:file-server-nginx-ci:版本号

这两个镜像会分别推送到两个仓库,因此按这份脚本的逻辑,最终有 4 个镜像仓库路径,每个路径 3 个标签,共 12 次镜像标签推送操作。这不代表构建了 12 个不同的镜像内容:同一个镜像的不同标签通常指向相同的镜像内容。

三、理解最重要的几个概念

1. on:什么情况下执行?

yaml 复制代码
on:
  push:
    branches:
      - main
  workflow_dispatch:
  • push:有人向仓库推送代码时触发。
  • branches: - main:只响应 main 分支的 push。
  • workflow_dispatch:允许你在 GitHub 的 Actions 页面手动点击运行。

注意:这并不意味着所有分支的代码都会自动发布。比如你在 dev 分支提交代码,默认不会触发这个工作流;合并到 main 后才会触发。

2. jobs 和 steps:流水线与具体任务

yaml 复制代码
jobs:
  build-test-publish:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout source
        uses: actions/checkout@v4

可以这样理解:

  • jobs:定义需要执行的任务。
  • build-test-publish:Job 的内部标识符。
  • runs-on:指定运行环境,这里是 GitHub 托管的 Ubuntu Linux 机器。
  • steps:任务中的具体步骤。
  • uses:调用别人已经编写好的 Action。
  • run:直接执行 Shell 命令。

例如,actions/checkout@v4 就是一个可复用的 Action,负责将仓库代码检出到工作目录。

3. ${{ ... }} 和 $VARIABLE 有什么区别?

这是阅读 GitHub Actions 时很容易混淆的地方。

写法 由谁处理 示例
${{ github.sha }} GitHub Actions 表达式引擎 获取本次提交的完整 SHA
${{ vars.ACR_IMAGE }} GitHub Actions 表达式引擎 读取仓库变量
${{ secrets.ACR_PASSWORD }} GitHub Actions 表达式引擎 读取密码 Secret
$ACR_IMAGE Bash Shell 读取已注入环境的变量
${GITHUB_SHA} Bash Shell 读取当前提交 SHA 对应的环境变量
"$TAG" Bash Shell 读取标签变量,并防止普通空格导致参数拆分

比如:

yaml 复制代码
env:
  ACR_IMAGE: ${{ vars.ACR_IMAGE }}
run: |
  docker push "${ACR_IMAGE}:latest"

GitHub Actions 先把仓库变量注入环境,Bash 执行命令时再读取 $ACR_IMAGE。

记住:${{ ... }} 是工作流表达式,$VARIABLE 是 Shell 变量。 两者处于不同的处理阶段。

4. vars、secrets 和 GITHUB_TOKEN

你的工作流使用了这几种配置:

名称 类型 用途
vars.ACR_REGISTRY 仓库变量 阿里云 ACR 的 Registry 地址
vars.ACR_IMAGE 仓库变量 应用镜像的完整仓库路径
secrets.ACR_USERNAME Secret 阿里云仓库用户名
secrets.ACR_PASSWORD Secret 阿里云仓库密码或访问凭证
secrets.GITHUB_TOKEN GitHub 提供的令牌 工作流登录 GHCR 时使用

变量与 Secret 的关键区别是:普通配置通常放在 vars,敏感凭证放在 secrets。不要把真实密码直接写进 YAML。

你的配置还包含:

yaml 复制代码
permissions:
  contents: read
  packages: write

contents: read 允许工作流读取仓库内容;packages: write 允许对应的 GITHUB_TOKEN 写入 GitHub Packages,包括 GHCR 镜像发布所需的权限。阿里云 ACR 的认证则另外使用 ACR_USERNAME 和 ACR_PASSWORD。<Cite refs={"turn540972search2","turn540972search0"}/>

四、 docker-ci.yml 添加详细中文注释

下面是根据你当前文件整理的完整中文注释版。我保留了原有的触发条件、构建参数、测试逻辑、镜像名称和推送规则,主要增加注释,便于你逐行学习。

有一点要注意:这是一份学习用的注释版,不是已经写回 GitHub 仓库的修改。

yaml 复制代码
# ============================================================
# 工作流名称
# 在 GitHub 仓库的 Actions 页面中显示这个名称
# ============================================================
name: File Server CI and Publish

# ============================================================
# 触发条件:什么情况下启动 CI/CD
# ============================================================
on:
  # 当代码被 push 到指定分支时触发
  push:
    branches:
      - main  # 只监听 main 分支

  # 允许在 GitHub Actions 页面手动启动工作流
  workflow_dispatch:

# ============================================================
# 工作流权限
# 这里设置的是 GitHub 自动提供的 GITHUB_TOKEN 权限
# ============================================================
permissions:
  contents: read   # 允许读取仓库代码
  packages: write  # 允许向 GitHub Packages / GHCR 发布镜像

# ============================================================
# 并发控制
# 防止同一组工作流同时执行
# ============================================================
concurrency:
  group: file-server-publish-main  # 同组工作流使用相同的并发标识
  cancel-in-progress: false        # 新任务不会主动取消正在运行的旧任务

# ============================================================
# Jobs:定义工作流需要执行的任务
# 当前只有一个 Job,内部包含多个按顺序执行的 Step
# ============================================================
jobs:
  build-test-publish:
    name: Build, test and publish  # 在 Actions 页面显示的任务名称

    # 使用 GitHub 托管的 Ubuntu Linux 环境
    # 每次运行会分配干净的临时运行环境
    runs-on: ubuntu-latest

    # 整个 Job 最多运行 30 分钟,超时会被终止
    timeout-minutes: 30

    # ========================================================
    # Steps:具体执行步骤
    # 同一个 Job 内的步骤按顺序运行
    # 普通步骤失败后,后续步骤默认不会继续执行
    # ========================================================
    steps:

      # ------------------------------------------------------
      # 第 1 步:下载仓库代码
      # ------------------------------------------------------
      - name: Checkout source
        # 使用 GitHub 官方维护的 checkout Action
        uses: actions/checkout@v4

      # ------------------------------------------------------
      # 第 2 步:检查项目文件和镜像仓库配置
      # ------------------------------------------------------
      - name: Validate repository and registry configuration
        shell: bash

        # 将 GitHub 仓库变量传入当前 Shell 步骤
        env:
          ACR_REGISTRY: ${{ vars.ACR_REGISTRY }}
          ACR_IMAGE: ${{ vars.ACR_IMAGE }}

        # 多行 Shell 脚本
        run: |
          # -e:命令失败时退出
          # -u:使用未定义变量时退出
          # -o pipefail:管道中任意命令失败,管道结果也视为失败
          set -euo pipefail

          # 检查构建所需的文件是否存在
          # 任意文件不存在,test 就会返回非零状态
          test -f app/Dockerfile
          test -f app/app.py
          test -f app/requirements.txt
          test -f nginx/Dockerfile
          test -f nginx/nginx.conf
          test -f html/index.html
          test -f docker-compose.yml

          # 检查 ACR Registry 配置是否为空
          # 为空时打印 GitHub Actions 错误信息并退出
          test -n "$ACR_REGISTRY" || {
            echo "::error::Set Actions variable ACR_REGISTRY first"
            exit 1
          }

          # 检查应用镜像完整路径是否为空
          test -n "$ACR_IMAGE" || {
            echo "::error::Set Actions variable ACR_IMAGE first"
            exit 1
          }

          # 检查应用镜像路径是否以 Registry 地址开头
          # 例如:
          # ACR_REGISTRY=registry.example.com
          # ACR_IMAGE=registry.example.com/my-project/file-server
          case "$ACR_IMAGE" in
            "$ACR_REGISTRY"/*) ;;
            *)
              echo "::error::ACR_IMAGE must begin with ACR_REGISTRY/"
              exit 1
              ;;
          esac

      # ------------------------------------------------------
      # 第 3 步:安装并配置 Python
      # ------------------------------------------------------
      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          # 指定 CI 检查使用的 Python 版本
          python-version: "3.12"

      # ------------------------------------------------------
      # 第 4 步:检查 Python 代码语法
      # ------------------------------------------------------
      - name: Check Python syntax
        # py_compile 会检查语法,并生成字节码缓存文件
        # 注意:语法检查通过不等于应用业务逻辑没有问题
        run: python -m py_compile app/app.py

      # ------------------------------------------------------
      # 第 5 步:准备 Docker Buildx
      # Buildx 是 Docker 的构建工具,支持 BuildKit 和构建缓存
      # ------------------------------------------------------
      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3
        with:
          # 使用 docker-container 驱动运行 BuildKit 构建器
          driver: docker-container

      # ------------------------------------------------------
      # 第 6 步:构建 Python 应用镜像
      # 这里仅构建镜像,不推送到远程仓库
      # ------------------------------------------------------
      - name: Build application image
        uses: docker/build-push-action@v6
        with:
          # 构建上下文目录
          # Dockerfile 中 COPY 等指令可使用这个目录内的文件
          context: ./app

          # 明确指定应用使用的 Dockerfile
          file: ./app/Dockerfile

          # 临时镜像标签使用完整提交 SHA
          # github.sha 是触发本次工作流的提交 ID
          tags: file-server-app-ci:${{ github.sha }}

          # 将构建结果加载到本机 Docker 镜像库
          # 后面的 docker run 才能使用这个本地镜像
          load: true

          # 这一阶段不推送镜像
          push: false

          # 尝试读取之前保存的 GitHub Actions 构建缓存
          cache-from: type=gha,scope=file-server-app

          # 保存本次构建缓存,供以后构建复用
          # mode=max 尽可能保存中间构建层
          cache-to: type=gha,mode=max,scope=file-server-app

      # ------------------------------------------------------
      # 第 7 步:启动应用容器,执行冒烟测试
      # 冒烟测试用于快速验证应用的基本功能是否可用
      # ------------------------------------------------------
      - name: Smoke test application container
        shell: bash
        run: |
          # 启用严格 Shell 检查
          set -euo pipefail

          # 后台启动刚刚构建的应用镜像
          docker run -d \
            --name file-server-ci-test \
            # 仅将容器的 5000 端口绑定到本机回环地址
            -p 127.0.0.1:5000:5000 \
            # 为测试容器注入临时环境变量
            -e SECRET_KEY=ci-only-test-secret \
            -e FILE_PASSWORD=ci-only-test-password \
            # 使用本次提交对应的本地镜像
            file-server-app-ci:${GITHUB_SHA}

          # 定义清理函数,删除测试容器
          cleanup() {
            docker rm -f file-server-ci-test >/dev/null 2>&1 || true
          }

          # 当前 Shell 退出时执行清理
          # 即使后面的健康检查失败,也会尝试清理容器
          trap cleanup EXIT

          # success=0 表示尚未检测到成功
          success=0

          # 最多尝试 30 次,每次间隔 2 秒
          # 用来等待应用启动完成
          for i in $(seq 1 30); do
            # curl --fail:HTTP 错误状态返回失败
            # --silent:不显示常规进度信息
            if curl --fail --silent http://127.0.0.1:5000/health; then
              echo
              success=1
              break
            fi
            sleep 2
          done

          # 30 次尝试后仍然失败,则输出容器日志并终止任务
          if [ "$success" -ne 1 ]; then
            docker logs file-server-ci-test
            echo "::error::Container health check failed"
            exit 1
          fi

      # ------------------------------------------------------
      # 第 8 步:构建 Nginx Web 镜像
      # 同样只构建、不推送,先进行配置检查
      # ------------------------------------------------------
      - name: Build Nginx web image
        uses: docker/build-push-action@v6
        with:
          # Nginx 构建上下文是项目根目录
          # 这样 Dockerfile 可以访问根目录下允许使用的文件
          context: .

          # Nginx 镜像使用自己的 Dockerfile
          file: ./nginx/Dockerfile

          # 使用提交 SHA 作为临时镜像标签
          tags: file-server-nginx-ci:${{ github.sha }}

          # 加载到本地 Docker,供后续 nginx -t 使用
          load: true

          # 测试阶段不发布镜像
          push: false

          # Nginx 专属构建缓存,避免与应用镜像缓存混用
          cache-from: type=gha,scope=file-server-nginx
          cache-to: type=gha,mode=max,scope=file-server-nginx

      # ------------------------------------------------------
      # 第 9 步:检查 Nginx 配置是否有效
      # ------------------------------------------------------
      - name: Validate Nginx configuration
        shell: bash
        run: |
          set -euo pipefail

          # 在临时目录创建证书存放位置
          mkdir -p "$RUNNER_TEMP/nginx-certs"

          # 生成仅用于 CI 检查的自签名证书
          # -x509:生成自签名证书
          # -nodes:不使用密码加密私钥
          # -newkey rsa:2048:创建 2048 位 RSA 密钥
          # -days 1:证书有效期为 1 天
          # -subj:指定证书主题,避免交互式提问
          openssl req -x509 -nodes -newkey rsa:2048 \
            -keyout "$RUNNER_TEMP/nginx-certs/server.key" \
            -out "$RUNNER_TEMP/nginx-certs/server.crt" \
            -days 1 -subj "/CN=localhost"

          # 启动一个临时容器,只执行 Nginx 配置检查
          docker run --rm \
            # 让容器内的 app 主机名解析到 127.0.0.1
            --add-host app:127.0.0.1 \
            # 将临时证书目录以只读方式挂载到容器
            -v "$RUNNER_TEMP/nginx-certs:/etc/nginx/certs:ro" \
            # 使用刚刚构建的镜像执行 nginx -t
            file-server-nginx-ci:${GITHUB_SHA} nginx -t

      # ------------------------------------------------------
      # 第 10 步:生成镜像标签
      # 生成北京时间时间戳标签和短提交 SHA 标签
      # ------------------------------------------------------
      - name: Generate readable China-time tags
        id: image-tags
        shell: bash
        run: |
          set -euo pipefail

          # 使用上海时区生成时间戳
          # 例如:20261009-184500
          TAG="$(TZ=Asia/Shanghai date +'%Y%m%d-%H%M%S')"

          # 截取完整提交 SHA 的前 7 个字符
          SHORT_SHA="${GITHUB_SHA:0:7}"

          # 写入 GitHub Actions 的步骤输出文件
          # 后续步骤可以通过 steps.image-tags.outputs 读取
          echo "timestamp=$TAG" >> "$GITHUB_OUTPUT"
          echo "short_sha=$SHORT_SHA" >> "$GITHUB_OUTPUT"

          # 在日志中打印生成的时间标签
          echo "Image timestamp tag: $TAG"

      # ------------------------------------------------------
      # 第 11 步:登录阿里云 ACR
      # 使用 GitHub 仓库变量和 Secret 提供认证信息
      # ------------------------------------------------------
      - name: Login to Alibaba Cloud ACR
        uses: docker/login-action@v3
        with:
          # ACR 的 Registry 地址
          registry: ${{ vars.ACR_REGISTRY }}

          # 从 GitHub Secrets 读取用户名和密码
          username: ${{ secrets.ACR_USERNAME }}
          password: ${{ secrets.ACR_PASSWORD }}

      # ------------------------------------------------------
      # 第 12 步:登录 GitHub Container Registry
      # GHCR 的域名是 ghcr.io
      # ------------------------------------------------------
      - name: Login to GitHub Container Registry
        uses: docker/login-action@v3
        with:
          registry: ghcr.io

          # 当前触发工作流的 GitHub 用户或应用身份
          username: ${{ github.actor }}

          # 使用 GitHub 自动提供的令牌认证
          password: ${{ secrets.GITHUB_TOKEN }}

      # ------------------------------------------------------
      # 第 13 步:给两个镜像打标签,并发布到两个仓库
      # 到这里,前面的构建和测试都已成功完成
      # ------------------------------------------------------
      - name: Tag and publish both images to both registries
        shell: bash

        # 将仓库地址和之前生成的标签输出传给 Shell
        env:
          # ACR 应用镜像完整路径
          ACR_IMAGE: ${{ vars.ACR_IMAGE }}

          # 使用当前 GitHub 仓库自动生成 GHCR 镜像路径
          # 例如 ghcr.io/owner/repository
          GHCR_IMAGE: ghcr.io/${{ github.repository }}

          # 引用第 10 步生成的时间戳和短 SHA
          TIMESTAMP_TAG: ${{ steps.image-tags.outputs.timestamp }}
          SHORT_SHA: ${{ steps.image-tags.outputs.short_sha }}

        run: |
          set -euo pipefail

          # 定义可重复使用的镜像发布函数
          # 参数 1:本地已有的源镜像
          # 参数 2:需要推送到的目标镜像路径
          publish_image() {
            local SOURCE="$1"
            local IMAGE="$2"

            # 同一份镜像生成三个标签
            for TAG in "$TIMESTAMP_TAG" "$SHORT_SHA" latest; do

              # 将源镜像标记为目标仓库中的指定版本
              docker tag "$SOURCE" "${IMAGE}:$TAG"

              # 将带标签的镜像推送到远程仓库
              docker push "${IMAGE}:$TAG"
            done

            # 打印发布结果
            echo "Published image: $IMAGE (tags: $TIMESTAMP_TAG, $SHORT_SHA, latest)"
          }

          # 发布应用镜像到阿里云 ACR
          publish_image "file-server-app-ci:${GITHUB_SHA}" "$ACR_IMAGE"

          # 发布 Nginx 镜像到 ACR
          # 通过 -nginx 区分应用镜像与 Web 镜像
          publish_image "file-server-nginx-ci:${GITHUB_SHA}" "${ACR_IMAGE}-nginx"

          # 发布应用镜像到 GHCR
          publish_image "file-server-app-ci:${GITHUB_SHA}" "$GHCR_IMAGE"

          # 发布 Nginx 镜像到 GHCR
          publish_image "file-server-nginx-ci:${GITHUB_SHA}" "${GHCR_IMAGE}-nginx"

          # --------------------------------------------------
          # 将发布摘要写入 GitHub Actions 的运行摘要
          # GitHub 会在本次工作流的 Summary 页面显示这些内容
          # --------------------------------------------------
          {
            echo "### Published Docker images"
            echo
            echo "Tags: $TIMESTAMP_TAG, $SHORT_SHA, latest"
            echo
            echo "ACR app: $ACR_IMAGE"
            echo "ACR web: ${ACR_IMAGE}-nginx"
            echo "GHCR app: $GHCR_IMAGE"
            echo "GHCR web: ${GHCR_IMAGE}-nginx"
          } >> "$GITHUB_STEP_SUMMARY"

五、这份工作流中最值得理解的 5 个细节

1. 为什么先构建,再推送?

你的两个构建步骤都设置了:

yaml 复制代码
load: true
push: false

这表示先将构建结果加载到本地 Docker 镜像库,不立即发布到远程仓库。接下来启动容器或检查 Nginx 配置,验证成功后才登录仓库并推送。

这是一个很实用的原则:先验证,再发布。

2. 为什么使用三个标签?

你的发布函数会给每个镜像创建三个标签。

标签 示例 用途
时间戳 20261009-184500 方便按发布时间查找版本
短 SHA a1b2c3d 方便追溯对应的代码提交
latest latest 表示当前发布的最新版本

实际时间戳和 SHA 会根据每次运行动态生成。

需要注意,latest 会被新版本覆盖,所以生产环境如果需要稳定回滚,最好使用时间戳、提交 SHA 或镜像摘要,而不是只依赖 latest。

3. GITHUB_OUTPUT 是什么?

你的代码中有:

bash 复制代码
echo "timestamp=$TAG" >> "$GITHUB_OUTPUT"
echo "short_sha=$SHORT_SHA" >> "$GITHUB_OUTPUT"

它将当前步骤生成的数据传递给后面的步骤。后面通过:

yaml 复制代码
TIMESTAMP_TAG: ${{ steps.image-tags.outputs.timestamp }}
SHORT_SHA: ${{ steps.image-tags.outputs.short_sha }}

读取这些值。

其中 image-tags 是步骤的 id,timestamp 和 short_sha 是步骤输出的名称。

这是一种很常见的工作流数据传递方式。

4. set -euo pipefail 为什么常见?

这条命令让 Shell 脚本更加严格:

  • -e:大多数未被处理的命令失败会导致脚本退出。
  • -u:使用未定义变量时退出。
  • pipefail:管道中的命令失败不会轻易被后续成功命令掩盖。

它可以避免脚本在某个关键操作失败后,仍然继续推送镜像。

不过,Shell 的错误处理有一些例外情况,例如 if 条件中执行的命令、使用 || 处理的失败等,因此它不能替代明确的错误判断。

5. 冒烟测试不等于完整测试

你当前的 Python 检查:

bash 复制代码
python -m py_compile app/app.py

主要验证语法是否正确。

而 /health 检查主要验证容器是否能够启动并返回成功响应。它不能证明登录、文件上传、分片合并、下载、权限控制等业务功能都正确。

关于

www.oiox.cn/

www.oiox.cn/index.php/s...

CSDN、GitHub、知乎、开源中国、思否、掘金、简书、华为云、阿里云、腾讯云、哔哩哔哩、今日头条、新浪微博、个人博客

全网可搜《小陈运维》

文章主要发布于微信公众号:《Linux运维交流社区》

相关推荐
miofly2 小时前
AI 智能体用 83% 字节精确匹配反编译完整游戏
开源·github
TunerT_TQ4 小时前
开源评测】openai/math 深度拆解:722 份 AI 数学手稿与 Lean 形式化的工程真相
github
W.A委员会4 小时前
PID自动整定与LLM调优项目使用说明(源码附文末)
自动化·github
m4Rk_5 小时前
【论文阅读】Agent 记忆机制(96):M3-Agent——让 Agent 从视听经历中持续形成情景与语义长期记忆
论文阅读·人工智能·学习·开源·github
粥里有勺糖6 小时前
安利一下最近用的桌面“Agent” | T3 Code
前端·github·ai编程
粥里有勺糖7 小时前
视野修炼第136期 | 前端小恐龙"Deno"被收购
前端·github·ai编程
怕浪猫8 小时前
Agent 工程化面试:从开发到部署的 5 个关键问题
面试·架构·github
高频因子挖掘机8 小时前
历史 K 线突然少一天?用交易日历、停牌信息和数据校验逐步排查
后端·github·api
miofly8 小时前
claude-sonnet-5 降价 90% 并支持推理调节
开源·github