Gitea Actions CI/CD 实战:从构建到部署

不久前发现我部署的 drone CI/CD 报错了 docker api 版本过低,找到一个相关的 issue,但是没有解决,另外感觉 drone 这一套貌似维护不太活跃。于是寻找一个替代方案,正好发现 Gitea 支持了 actions,于是尝试把本地部署迁移到 gitea actions,因为我本地已经部署了一个 gitea。

上手

首先需要一个 self-hosted 的 Gitea,这部分略过。

接下来,我们需要配置 Gitea Actions 的执行环境,也就是 Runner。Gitea 的 Runner 叫做 act_runner,它负责执行在 workflow 文件中定义的任务。

Runner 配置解析

为了让我们的 CI/CD 流程顺利跑起来,一个配置合理的 Runner 是关键。我的部署环境是基于 Docker 的,所以 Runner 也是通过 Docker Compose 来管理的。

docker-compose.yml

这是用于启动 act_runnerdocker-compose.yml 文件。它定义了 Runner 容器的运行参数。

services:
  runner:
    image: docker.io/gitea/act_runner:latest
    privileged: true
    environment:
      CONFIG_FILE: /config.yaml
      GITEA_INSTANCE_URL: http://127.0.0.1:16004
      GITEA_RUNNER_REGISTRATION_TOKEN: loPCJ0jM7zeArrE6QukefYFxRGUbwF6JFGSz7ips
      GITEA_RUNNER_NAME: local_runner
      GITEA_RUNNER_LABELS: "runner-host,node18:docker://node:18,node20:docker://node:20,node22:docker://node:22"
    volumes:
      - /data/gitea-runner/config.yaml:/config.yaml
      - /data/gitea-runner:/data
      - /var/run/docker.sock:/var/run/docker.sock
      - /data/next-cloud/data/yunyuyuan/files/nuxt3-blog-imgs:/data/next-cloud/data/yunyuyuan/files/nuxt3-blog-imgs
    network_mode: "host"

这里有几个关键配置需要特别说明:

  • privileged: true: 赋予容器特权模式,这对于在容器内操作 Docker 是必要的。
  • GITEA_RUNNER_LABELS: 这是 Runner 的标签,是 Gitea Actions 用来决定哪个 Runner 执行哪个 Job 的依据。这里的配置非常关键:
    • runner-host: 这是一个自定义标签,我用它来指定在 Runner 宿主环境(也就是 act_runner 容器本身)中执行任务。
    • node18:docker://node:18, node20:docker://node:20, node22:docker://node:22: 这些标签定义了当 runs-on 指定为 node18node20node22 时,act_runner 会启动一个对应的 Node.js 官方 Docker 镜像作为 Job 的执行环境。
  • volumes:
    • - /var/run/docker.sock:/var/run/docker.sock: 这是实现 “Docker in Docker” 的核心。通过将宿主机的 Docker socket 文件挂载到 act_runner 容器内部,我们使得在 act_runner 容器中可以调用和控制宿主机的 Docker 服务。这是我们后续能够更新部署在宿主机上的应用容器的关键。
  • network_mode: "host": 让 act_runner 容器直接使用宿主机的网络,简化了网络配置。

config.yaml

这个文件是 act_runner 的配置文件,它为 Job 的执行容器提供了默认配置。

container:
  network: host
  privileged: true
  volumes:
    - /var/run/docker.sock:/var/run/docker.sock
    - /data/next-cloud/data/yunyuyuan/files/nuxt3-blog-imgs:/data/next-cloud/data/yunyuyuan/files/nuxt3-blog-imgs

这里的配置会被应用到由 act_runner 启动的每一个 Job 容器上。例如,当一个 Job runs-on: node22 时,act_runner 会启动一个 node:22 的容器,并且这个 config.yaml 中的配置(如 privileged: truevolumes)会被应用到这个 node:22 容器上。这意味着,即使是 Job 容器,也同样挂载了宿主机的 Docker socket,因此也具备了操作宿主机 Docker 的能力。

Workflow 工作流解析

理解了 Runner 的配置后,我们来看看实际的 CI/CD 工作流文件 .gitea/workflows/build.yaml。我将整个流程拆分成了两个 Job:builddeploy。这么做的原因我会在后面详细解释。

name: build
on:
  push:
    branches:
      - blog
jobs:
  build:
    runs-on: node22
    steps:
      # ... (省略了分支检查、代码检出、Node.js环境设置等步骤)
      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3
      - name: Build and load Docker image
        uses: docker/build-push-action@v5
        with:
          context: .
          push: false
          load: true
          tags: nuxt3-blog:latest

  deploy:
    runs-on: runner-host
    needs: build
    steps:
      - name: Install Docker
        run: |
          apk update
          apk add docker
      - name: Deploy Docker container
        run: |
          # Stop and remove existing container
          docker ps -q --filter "name=nuxt3-blog-app" | xargs -r docker stop || true
          docker ps -aq --filter "name=nuxt3-blog-app" | xargs -r docker rm || true
          # Run new container
          docker run -d \
            --name=nuxt3-blog-app \
            --restart=always \
            -e CLOUDFLARE_D1_TOKEN=${{ secrets.CLOUDFLARE_D1_TOKEN }} \
            -e CLOUDFLARE_D1_ACCOUNT_ID=${{ secrets.CLOUDFLARE_D1_ACCOUNT_ID }} \
            -e CLOUDFLARE_D1_DATABASE_ID=${{ secrets.CLOUDFLARE_D1_DATABASE_ID }} \
            -p 127.0.0.1:8451:3000 \
            nuxt3-blog:latest

为什么拆分为两个 Job?

这是本次实践的核心设计。将构建和部署拆分为两个独立的 Job,主要是为了解决 “在什么环境中执行什么命令” 的问题。

  1. build Job:专业的构建环境

    • runs-on: node22:我的博客项目是基于 Nuxt.js 的,构建过程需要 Node.js 环境和 pnpm 包管理器。因此,我让 build Job 运行在一个标准的 node:22 容器中,这是一个纯净且专业的 Node.js 构建环境。
    • 这个 node:22 容器默认是没有安装 Docker CLI 的。但我们依然可以构建 Docker 镜像,这得益于 docker/setup-buildx-action@v3docker/build-push-action@v5 这两个官方 Action。它们通过我们之前挂载的 /var/run/docker.sock 文件,直接与宿主机的 Docker 引擎通信来完成镜像的构建和加载 (load: true),完全绕过了在 Job 容器中安装 Docker CLI 的需要。
  2. deploy Job:直接的宿主机控制

    • runs-on: runner-host:这是关键所在。这个配置告诉 Gitea,这个 Job 应该由带有 runner-host 标签的 Runner 来执行,并且是在 host 模式下。根据我们之前的 docker-compose.yml 配置,这意味着 deploy Job 会直接在 act_runner 容器本身的环境中执行,而不是在一个新启动的 Job 容器里。
    • apk add dockeract_runner 的基础镜像是 Alpine Linux,它非常轻量,默认不包含 Docker 客户端。因此,第一步我们需要通过 apk 包管理器来安装 docker
    • 执行 docker 命令:一旦 Docker CLI 安装完毕,这个 Job 就获得了直接操作宿主机 Docker 的能力——因为它本身就运行在一个挂载了 docker.sock 的容器里。接下来的 docker stop, docker rm, docker run 等命令,实际上都是在操作物理机上的 Docker 容器,从而完成了应用的停止、移除和重新部署。

总结

通过将 jobs 拆分为两个,我们实现了关注点分离:

Job 运行环境 核心任务 关键技术
build node:22 Docker 容器 使用 Node.js 环境构建前端项目并打包 Docker 镜像 通过 docker.sock 挂载,使用 build-push-action 与宿主机 Docker 通信
deploy act_runner 容器本身 (Alpine Linux) 在宿主机上停止旧容器、启动新容器 使用 runner-host 标签,在 Runner 容器内安装 Docker CLI,通过 docker.sock 操作宿主机

这种方法巧妙地利用了 Gitea Actions Runner 的标签和 docker.sock 挂载机制,为不同的任务阶段提供了最合适、最干净的执行环境,既保证了构建环境的标准化,又实现了对宿主机容器的直接控制,是一种非常灵活和高效的 CI/CD 实践。


AI味太浓了哈哈哈