作为独立开发者,我们使用 Gitea 搭建了私有代码仓库,并通过 Gitea Runner 实现 .NET 多服务的 CI/CD 自动构建。核心需求很明确:构建 amd64 + arm64 双架构镜像,同时解决 基础镜像重复下载、耗流量、构建速度慢 的痛点——毕竟 .NET SDK 镜像体积不小,每次构建都重新下载,不仅浪费时间,也增加了外网流量成本。
这篇日志记录了我们从初始方案、踩坑试错,到最终找到最优解的完整过程,希望能给有同样需求的开发者提供参考。
一、初始需求与痛点
我们有两个 .NET 微服务,需要通过 Gitea Actions 自动构建双架构镜像(amd64 用于服务器部署,arm64 用于边缘设备),并推送到私有镜像仓库。初始面临的核心问题:
每次构建都会重新下载 .NET SDK、Runtime 基础镜像,耗时久、耗流量;
尝试过本地文件缓存,不仅配置复杂,还存在权限问题和磁盘膨胀风险;
多架构构建(buildx)默认不保留基础镜像,缓存方案难以落地。
二、第一版方案:本地文件缓存(踩坑)
最初想到的解决方案是使用 buildx 的本地文件缓存(type=local),通过映射宿主机目录,将构建缓存持久化,试图避免重复下载基础镜像。
第一版 workflow 核心配置:
name: Build and Push Multiple .NET Services
on:
push:
tags:
- 'v*'
workflow_dispatch:
jobs:
build-multiple-services:
runs-on: runner-host
strategy:
fail-fast: false
max-parallel: 1
matrix:
project:
- image_name: service-account
dockerfile: ./AccountServer/Dockerfile
- image_name: service-ai
dockerfile: ./AIServer/Dockerfile
steps:
- name: Checkout Code
uses: actions/checkout@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Docker Registry
uses: docker/login-action@v3
with:
registry: registry.example.com
username: ${{ secrets.REGISTRY_USERNAME }}
password: ${{ secrets.REGISTRY_PASSWORD }}
- name: Extract Version
id: vars
run: |
REF_NAME="${GITHUB_REF_NAME:-${GITEA_REF_NAME:-}}"
if [ -z "$REF_NAME" ]; then REF_NAME="${GITHUB_REF##*/}"; fi
if [[ "$REF_NAME" == v* ]]; then
VERSION="$REF_NAME"
else
VERSION="latest"
fi
echo "version=$VERSION" >> $GITHUB_OUTPUT
# 尝试本地文件缓存,解决基础镜像重复下载
- name: Prepare Cache Directories
run: |
mkdir -p /data/build-cache/${{ matrix.project.image_name }}
mkdir -p /data/build-cache/${{ matrix.project.image_name }}-new
- name: Build and Push ${{ matrix.project.image_name }}
uses: docker/build-push-action@v5
with:
context: .
file: ${{ matrix.project.dockerfile }}
platforms: linux/amd64,linux/arm64
push: true
tags: |
registry.example.com/group/${{ matrix.project.image_name }}:${{ steps.vars.outputs.version }}
registry.example.com/group/${{ matrix.project.image_name }}:latest
# 本地文件缓存配置
cache-from: type=local,src=/data/build-cache/${{ matrix.project.image_name }}
cache-to: type=local,dest=/data/build-cache/${{ matrix.project.image_name }}-new,mode=max
# 清理旧缓存,防止磁盘膨胀
- name: Move cache to prevent disk bloat
run: |
rm -rf /data/build-cache/${{ matrix.project.image_name }}
mv /data/build-cache/${{ matrix.project.image_name }}-new /data/build-cache/${{ matrix.project.image_name }} || true
踩坑总结
这个方案看似能解决缓存问题,但实际使用中发现多个致命缺陷:
并行构建冲突:为了避免缓存目录冲突,不得不设置
max-parallel: 1,强制串行构建,降低了构建效率。部署迁移麻烦:缓存目录交替删除重命名,防止缓存复写出现问题。runner部署是容器化的,它需要额外挂载一个卷来映射/data,需要配置,清理和迁移都麻烦。
显然,本地文件缓存方案不适合我们的场景,必须寻找更优解。
三、第二版方案:镜像仓库缓存(不适用)
参考 Docker 官方文档,尝试使用 type=registry 缓存,将构建缓存推送到私有镜像仓库,试图实现缓存共享和持久化。
核心修改点:
# 替换原有的本地缓存配置
cache-from: type=registry,ref=registry.example.com/group/${{ matrix.project.image_name }}:buildcache
cache-to: type=registry,ref=registry.example.com/group/${{ matrix.project.image_name }}:buildcache,mode=max
不适用原因
这个方案虽然解决了缓存共享和磁盘膨胀问题,但完全不符合我们的核心需求:
我们的痛点是「避免重复下载基础镜像、节省外网流量」,而将缓存推送到私有镜像仓库,本质上还是需要从网络拉取缓存——无论是拉取基础镜像,还是拉取仓库中的缓存,依然会消耗外网流量,和直接下载基础镜像没有本质区别。
如果私有镜像仓库是内网部署,这个方案可行,但我们的镜像仓库需要外网访问,因此这个方案被放弃。
四、第三版方案:原生 Docker Build(牺牲多架构)
既然 buildx 多架构构建难以缓存基础镜像,我们尝试放弃 buildx,使用原生 docker build,因为原生 Docker 会将基础镜像永久缓存到宿主机,不会重复下载。
核心修改点:
# 替换 buildx 构建步骤
- name: Build & Push ${{ matrix.project.image_name }}
run: |
IMAGE="registry.example.com/group/${{ matrix.project.image_name }}"
VERSION="${{ steps.vars.outputs.version }}"
# 原生 docker build,自动缓存基础镜像
docker build \
-t $IMAGE:$VERSION \
-t $IMAGE:latest \
-f ${{ matrix.project.dockerfile }} .
# 推送镜像
docker push $IMAGE:$VERSION
docker push $IMAGE:latest
优缺点分析
优点:完美解决基础镜像重复下载问题,第一次下载后,后续构建直接复用宿主机缓存,不耗流量、速度极快;配置简单,无权限和磁盘膨胀问题。
缺点:无法构建 arm64 架构——原生 docker build 不支持多架构构建,而我们需要同时构建 amd64 和 arm64 双架构,因此这个方案只能作为过渡,无法满足最终需求。
五、终极方案:持久化 Buildx 构建器(完美解决)
经过多次试错,我们终于找到了解决方案:创建一个长期驻留在宿主机上的 Buildx 构建器,让构建器容器常驻宿主机,其内部缓存会永久保留,既支持多架构构建,又能避免基础镜像重复下载。
这个方案的核心逻辑是:Buildx 构建器本身是一个 Docker 容器,只要这个容器不被删除,其内部的基础镜像、构建中间层缓存就会永久保留,后续构建直接复用,无需重新下载。
最终定稿 workflow
name: Build and Push Multiple .NET Services
on:
push:
tags:
- 'v*'
workflow_dispatch:
jobs:
build-multiple-services:
runs-on: runner-host
strategy:
fail-fast: false # 单个服务构建失败,不影响其他服务
matrix:
# 多服务构建矩阵,可按需扩展
project:
- image_name: service-account
dockerfile: ./AccountServer/Dockerfile
- image_name: service-ai
dockerfile: ./AIServer/Dockerfile
steps:
- name: Checkout Code
uses: actions/checkout@v3
# 核心:创建/复用持久化 Buildx 构建器,缓存永久保留
- name: Set up Persistent Docker Buildx Builder
run: |
# 尝试使用已有的构建器,不存在则创建
docker buildx use custom-builder 2>/dev/null || \
docker buildx create --name custom-builder --driver docker-container --use --bootstrap
- name: Login to Docker Registry
uses: docker/login-action@v3
with:
registry: registry.example.com
username: ${{ secrets.REGISTRY_USERNAME }}
password: ${{ secrets.REGISTRY_PASSWORD }}
- name: Extract Version
id: vars
run: |
REF_NAME="${GITHUB_REF_NAME:-${GITEA_REF_NAME:-}}"
if [ -z "$REF_NAME" ]; then REF_NAME="${GITHUB_REF##*/}"; fi
if [[ "$REF_NAME" == v* ]]; then
VERSION="$REF_NAME"
else
VERSION="latest"
fi
echo "version=$VERSION" >> $GITHUB_OUTPUT
# 多架构构建 + 自动复用缓存
- name: Build and Push ${{ matrix.project.image_name }}
uses: docker/build-push-action@v5
with:
context: . # .NET 构建必须使用仓库根目录作为上下文
file: ${{ matrix.project.dockerfile }}
platforms: linux/amd64,linux/arm64 # 双架构构建
push: true
tags: |
registry.example.com/group/${{ matrix.project.image_name }}:${{ steps.vars.outputs.version }}
registry.example.com/group/${{ matrix.project.image_name }}:latest
关键优化点解析
持久化 Buildx 构建器:
docker buildx create --name custom-builder --driver docker-container创建一个命名构建器,使用docker-container驱动,构建器容器会常驻宿主机,不会每次构建都销毁;缓存自动保留:构建器容器内部会自动缓存 .NET SDK、Runtime 等基础镜像,以及构建中间层,后续构建直接复用,无需重新下载;
无需额外缓存配置:不需要
cache-from、cache-to配置,Buildx 构建器会自动管理缓存,简洁高效;支持多架构 + 并行构建:去掉了
max-parallel: 1,可以并行构建多个服务(根据服务器性能调整),同时完美支持 amd64 + arm64 双架构。
六、可选优化:禁止缓存自动清理(更稳)
为了确保基础镜像缓存永久保留,避免 Buildx 自动清理缓存,可在创建构建器时添加 --buildkitd-flags '--oci-worker-gc=false',禁止自动垃圾回收:
- name: Set up Persistent Docker Buildx Builder
run: |
docker buildx use custom-builder 2>/dev/null || \
docker buildx create --name custom-builder --driver docker-container \
--use --bootstrap \
--buildkitd-flags '--oci-worker-gc=false'
七、最终效果与总结
优化后,我们的 CI/CD 构建实现了以下目标:
✅ 双架构构建(amd64 + arm64)正常运行,满足多设备部署需求;
✅ .NET SDK、Runtime 基础镜像只下载一次,后续构建零流量消耗;
✅ 构建速度提升 80% 以上,从每次构建 10+ 分钟缩短到 2 分钟以内;
✅ 无权限问题、无磁盘膨胀风险,无需手动维护缓存;
✅ 配置简洁,可直接复用,支持多服务扩展。
经验总结
不要盲目使用本地文件缓存或镜像仓库缓存,先明确核心痛点:我们的痛点是「基础镜像重复下载」,而非「项目构建层缓存」;
Buildx 多架构构建的缓存关键的是「持久化构建器」,而非额外的缓存配置;
原生 Docker Build 和 Buildx 各有优劣,根据是否需要多架构选择合适的方案;
对于 Gitea Runner + .NET 多架构构建场景,「持久化 Buildx 构建器」是最优解,兼顾简洁性、稳定性和效率。
希望这篇日志能帮助到有同样需求的开发者,少走弯路,快速实现高效、省流量的 CI/CD 构建流程。