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:什么情况下执行?
on:
push:
branches:
- main
workflow_dispatch:
push:有人向仓库推送代码时触发。branches: - main:只响应main分支的 push。workflow_dispatch:允许你在 GitHub 的 Actions 页面手动点击运行。
注意:这并不意味着所有分支的代码都会自动发布。比如你在 dev 分支提交代码,默认不会触发这个工作流;合并到 main 后才会触发。
2. jobs 和 steps:流水线与具体任务
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 | 读取标签变量,并防止普通空格导致参数拆分 |
比如:
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。
你的配置还包含:
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 仓库的修改。
# ============================================================
# 工作流名称
# 在 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. 为什么先构建,再推送?
你的两个构建步骤都设置了:
load: true
push: false
这表示先将构建结果加载到本地 Docker 镜像库,不立即发布到远程仓库。接下来启动容器或检查 Nginx 配置,验证成功后才登录仓库并推送。
这是一个很实用的原则:先验证,再发布。
2. 为什么使用三个标签?
你的发布函数会给每个镜像创建三个标签。
| 标签 | 示例 | 用途 |
|---|---|---|
| 时间戳 | 20261009-184500 |
方便按发布时间查找版本 |
| 短 SHA | a1b2c3d |
方便追溯对应的代码提交 |
latest |
latest |
表示当前发布的最新版本 |
实际时间戳和 SHA 会根据每次运行动态生成。
需要注意,latest 会被新版本覆盖,所以生产环境如果需要稳定回滚,最好使用时间戳、提交 SHA 或镜像摘要,而不是只依赖 latest。
3. GITHUB_OUTPUT 是什么?
你的代码中有:
echo "timestamp=$TAG" >> "$GITHUB_OUTPUT"
echo "short_sha=$SHORT_SHA" >> "$GITHUB_OUTPUT"
它将当前步骤生成的数据传递给后面的步骤。后面通过:
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 检查:
python -m py_compile app/app.py
主要验证语法是否正确。
而 /health 检查主要验证容器是否能够启动并返回成功响应。它不能证明登录、文件上传、分片合并、下载、权限控制等业务功能都正确。
- 点赞
- 收藏
- 关注作者
评论(0)