Gitea Actions 自定义 Runner 镜像与自动化部署实战

Gitea Actions 自定义 Runner 镜像与自动化部署实战

本文记录了从零开始搭建一套 Gitea Actions 自动化部署流程的完整过程:自定义 act_runner 镜像、配置国内加速源、生成 SSH 密钥、编写 CI/CD workflow,最终实现「push 代码自动构建 + 上传 + 灰度切换」的部署链路。

一、Gitea Runner 使用自定义镜像

1.1 方式一:通过 labels 指定镜像

注册 runner 时指定 label:schema:image 格式:

bash 复制代码
./act_runner register \
  --instance https://your-gitea-instance \
  --token <your-token> \
  --labels "ubuntu-latest:docker://node:20-bullseye,my-custom:docker://myregistry.com/my-image:latest"

也可以直接编辑 .runner 文件:

json 复制代码
{
  "labels": [
    "ubuntu-latest:docker://node:20-bullseye",
    "custom-runner:docker://myregistry.com/my-custom-image:v1.0"
  ]
}

1.2 方式二:修改 config.yaml 全局配置

yaml 复制代码
container:
  network: ""
  privileged: false
  options:
  workdir_parent:
  valid_volumes: []
  docker_host: ""
  force_pull: false   # 使用本地已构建的镜像时必须设为 false,否则会去远程拉取导致失败

runner:
  labels:
    - "my-custom:docker://mynode/runner"

关键点 :如果镜像只存在于本机(没有推送到任何仓库),一定要把 force_pull 设为 false;如果 runner 是多机部署,则必须把镜像推送到私有仓库,不能依赖"本地"这个概念。

workflow 中通过 runs-on 引用 label(不是镜像名):

yaml 复制代码
jobs:
  build:
    runs-on: my-custom

二、自定义镜像:Node + pnpm + rsync + ssh

2.1 Dockerfile

dockerfile 复制代码
# 基础镜像:Node 20,Debian bullseye-slim 体积较小
FROM docker.1ms.run/library/node:20-bookworm-slim

# 将 apt 源替换为阿里云镜像
# 兼容两种格式:
#   1. 传统格式 /etc/apt/sources.list(Debian 11 及更早)
#   2. 新版 deb822 格式 /etc/apt/sources.list.d/debian.sources(Debian 12+ 部分镜像已切换)
# 文件不存在时跳过,避免 sed 因找不到文件而报错中断构建
RUN set -eux; \
    if [ -f /etc/apt/sources.list ]; then \
        sed -i \
            -e 's|deb.debian.org|mirrors.aliyun.com|g' \
            -e 's|security.debian.org|mirrors.aliyun.com/debian-security|g' \
            /etc/apt/sources.list; \
    fi; \
    if [ -f /etc/apt/sources.list.d/debian.sources ]; then \
        sed -i \
            -e 's|deb.debian.org|mirrors.aliyun.com|g' \
            -e 's|security.debian.org|mirrors.aliyun.com/debian-security|g' \
            /etc/apt/sources.list.d/debian.sources; \
    fi

# 安装 rsync、openssh-client(rsync 走 ssh 传输需要)、ca-certificates(避免 https 证书报错)
RUN apt-get update \
    && apt-get install -y --no-install-recommends \
        rsync \
        openssh-client \
        ca-certificates \
        git \
    && rm -rf /var/lib/apt/lists/*

# 安装指定版本 pnpm(通过 corepack,避免全局 npm install 带来的版本漂移)
RUN corepack enable \
    && corepack prepare pnpm@10 --activate

# 配置 pnpm 使用国内镜像源(淘宝 npmmirror)
RUN pnpm config set registry https://registry.npmmirror.com

# 二进制依赖(node-sass / sharp 等)一并配置镜像源,避免下载失败
RUN pnpm config set disturl https://npmmirror.com/mirrors/node \
    && pnpm config set sass_binary_site https://npmmirror.com/mirrors/node-sass \
    && pnpm config set sharp_binary_host https://npmmirror.com/mirrors/sharp \
    && pnpm config set sharp_libvips_binary_host https://npmmirror.com/mirrors/sharp-libvips

WORKDIR /workspace

# 验证工具是否安装成功
RUN node -v && pnpm -v && rsync --version | head -n 1 && ssh -V

2.2 构建与验证

bash 复制代码
docker build -t mynode/runner .

# BuildKit 默认不打印 RUN 命令的标准输出,验证工具版本要单独跑一次容器
docker run --rm mynode/runner sh -c "node -v && pnpm -v && rsync --version | head -n 1 && ssh -V"

2.3 踩坑记录:sed: can't read /etc/apt/sources.list

原因:较新的基础镜像标签(如 lts-slim)实际拉到的是 Debian 12(bookworm),apt 源配置已迁移到 deb822 格式的 /etc/apt/sources.list.d/debian.sources,直接 sed 一个不存在的旧路径文件会导致构建失败。解决方式是判断文件是否存在,两种路径都做兼容处理(见上方 Dockerfile)。

三、Docker 镜像加速配置

3.1 宿主机全局配置(daemon.json)

bash 复制代码
sudo tee /etc/docker/daemon.json <<'EOF'
{
  "registry-mirrors": [
    "https://docker.1ms.run",
    "https://docker.m.daocloud.io"
  ]
}
EOF

sudo systemctl daemon-reload
sudo systemctl restart docker

3.2 挂载 docker.sock 场景(DooD)会自动继承

如果 act_runner 是通过挂载宿主机 /var/run/docker.sock 的方式使用 Docker(最常见的部署方式),容器内的 docker 命令只是客户端,实际执行者是宿主机的 dockerd,因此会自动使用宿主机配置的加速源,无需额外配置。

只有当容器内运行的是独立的 dockerd (真正的 DinD,如 docker:dind 镜像)时,才需要单独给这个容器配置加速源。

判断方法:

bash 复制代码
docker info | grep "Server Version"

若容器内外版本号一致,说明是 DooD,直接吃宿主机配置。

3.3 docker build 单独指定镜像源

不依赖宿主机全局配置,直接在 Dockerfile 里写死镜像源地址最稳妥:

dockerfile 复制代码
FROM docker.1ms.run/library/node:20-bookworm-slim

规则:docker.io/库名/镜像名镜像源地址/库名/镜像名,官方镜像(无用户名前缀)要带上 library 命名空间。

四、生成 SSH 密钥并配置服务器

4.1 生成密钥对

bash 复制代码
ssh-keygen -t ed25519 -C "gitea-deploy" -f ./gitea_deploy_key -N ""

4.2 用 ssh-copy-id 推送公钥到服务器

bash 复制代码
ssh-copy-id -i gitea_deploy_key.pub zoutao@192.168.1.2

比手动 cat | ssh ... >> authorized_keys 更省心,会自动处理目录权限、去重等细节。

4.3 踩坑记录:家目录不存在

复制代码
Could not chdir to home directory /home/zoutao: No such file or directory
mkdir: cannot create directory '.ssh': Permission denied

原因:账号存在但对应的家目录没有被创建(多半是 useradd 时漏加 -m 参数)。解决:

bash 复制代码
# 方式一:手动补建
sudo mkdir -p /home/zoutao
sudo chown zoutao:zoutao /home/zoutao
sudo chmod 755 /home/zoutao

# 方式二:重新指定并自动创建
sudo usermod -d /home/zoutao -m zoutao

补建后重新执行 ssh-copy-id 即可成功。

4.4 验证免密登录

bash 复制代码
ssh -i gitea_deploy_key zoutao@192.168.1.2 "echo ok"

4.5 配置 Gitea Secrets

在仓库 Settings → Actions → Secrets 中添加:

Secret 名称 说明
REMOTE_HOST 服务器地址
REMOTE_USER 登录用户名
REMOTE_SSH_KEY 私钥完整内容(cat gitea_deploy_key 的输出)
REMOTE_UPLOAD 远程部署根目录,如 /data/www/myapp

五、完整部署 Workflow

采用「按 commit sha 建版本目录 + current 软链接切换 + 自动清理旧版本」的发布策略,方便回滚。

yaml 复制代码
name: Deploy Vue Project

on:
  push:
    paths-ignore:
      - .github/**
      - .commitlintrc.cjs
      - .drone.yml
      - .editorconfig
      - .gitignore
      - .markdownlint.json
      - .prettierignore
      - .prettierrc.cjs
  workflow_dispatch:

jobs:
  build:
    runs-on: my-custom   # 对应 act_runner labels 中定义的 label 名称,不是镜像名
    steps:
      - name: Checkout
        uses: https://gitee.com/actions-mirror/checkout@v4
        with:
          fetch-depth: 0
          # submodules: true   # 如需 Git 子模块,取消注释

      - name: Install dependencies
        run: pnpm install

      - name: Build artifact
        id: build
        env:
          NODE_OPTIONS: --max_old_space_size=2048
        run: |-
          pnpm run build

      - name: 配置 SSH 密钥
        run: |
          mkdir -p ~/.ssh
          echo "${{ secrets.REMOTE_SSH_KEY }}" > ~/.ssh/id_ed25519
          chmod 600 ~/.ssh/id_ed25519
          ssh-keyscan -H "${{ secrets.REMOTE_HOST }}" >> ~/.ssh/known_hosts

      - name: 创建远程发布目录
        run: |
          ssh -i ~/.ssh/id_ed25519 \
            "${{ secrets.REMOTE_USER }}@${{ secrets.REMOTE_HOST }}" \
            "mkdir -p '${{ secrets.REMOTE_UPLOAD }}/releases/${{ gitea.sha }}'"

      - name: 上传 dist 到远程发布目录
        run: |
          rsync -avz --delete \
            -e "ssh -i ~/.ssh/id_ed25519" \
            dist/ \
            "${{ secrets.REMOTE_USER }}@${{ secrets.REMOTE_HOST }}:${{ secrets.REMOTE_UPLOAD }}/releases/${{ gitea.sha }}/"

      - name: 切换软链接并清理旧版本
        run: |
          ssh -i ~/.ssh/id_ed25519 \
            "${{ secrets.REMOTE_USER }}@${{ secrets.REMOTE_HOST }}" bash -s <<'EOF'
          set -euo pipefail

          BASE="${{ secrets.REMOTE_UPLOAD }}"
          TARGET_LINK="$BASE/current"
          NEW_RELEASE_REL="releases/${{ gitea.sha }}"
          NEW_RELEASE_ABS="$BASE/$NEW_RELEASE_REL"
          KEEP=5

          # 1. 校验新版本目录存在,避免创建断链
          if [ ! -d "$NEW_RELEASE_ABS" ]; then
              echo "ERROR: release dir not found: $NEW_RELEASE_ABS" >&2
              exit 1
          fi

          # 2. 若 current 是真目录(历史遗留),先删除
          if [ -d "$TARGET_LINK" ] && [ ! -L "$TARGET_LINK" ]; then
              echo "Removing existing physical directory: $TARGET_LINK"
              rm -rf -- "$TARGET_LINK"
          fi

          # 3. 原子切换软链接(相对路径,Docker 挂载后依然可解析,避免 nginx 500)
          cd "$BASE"
          ln -snf "$NEW_RELEASE_REL" "current.tmp"
          mv -T "current.tmp" "current"
          echo "Switched: $TARGET_LINK -> $(readlink "$TARGET_LINK")"

          # 4. 清理旧版本:保留最新 KEEP 个,且保护 current 指向的目录
          cd "$BASE/releases"
          CURRENT_DIR="$(basename "$(readlink -f "$TARGET_LINK")")"

          find . -maxdepth 1 -mindepth 1 -type d -printf '%T@ %p\n' \
            | sort -rn \
            | tail -n +$((KEEP + 1)) \
            | awk '{print $2}' \
            | while read -r d; do
                name="$(basename "$d")"
                if [ "$name" = "$CURRENT_DIR" ]; then
                    echo "Skip current: $name"
                    continue
                fi
                echo "Removing old release: $name"
                rm -rf -- "$d"
              done

          echo "Done."
          EOF

5.1 踩坑记录:软链接必须使用相对路径

若 nginx 是通过 Docker 挂载宿主机目录提供静态文件服务,current 软链接如果指向绝对路径 (如 /data/www/myapp/releases/xxx),容器内部并不存在这个路径结构,会导致 nginx 返回 500。解决方式是让软链接指向相对路径releases/xxx),只要 currentreleases 目录始终在同一个父目录下、一起被挂载进容器,相对关系在容器内依然成立。

对应的 nginx 挂载配置:

yaml 复制代码
services:
  nginx:
    image: nginx:alpine
    volumes:
      - /data/www/myapp:/usr/share/nginx/html
    ports:
      - "80:80"

六、Gitea Actions 私有仓库 checkout 说明

  • Gitea Actions 会自动生成短期 GITEA_TOKENcheckout action 默认用它通过 HTTPS 拉取当前 workflow 所在的私有仓库,无需额外配置。
  • 如果需要拉取同组织下另一个私有仓库,需要显式传入具备权限的 Personal Access Token:
yaml 复制代码
- uses: https://gitee.com/actions-mirror/checkout@v4
  with:
    repository: your-org/another-private-repo
    token: ${{ secrets.PAT_FOR_OTHER_REPO }}
  • 私有子模块、Git LFS 场景可能存在认证兼容性问题,需要额外处理。
  • 自定义 runner 镜像里必须安装 git,否则 checkout 步骤可能失败。

七、总结

整套流程可以概括为:

  1. 构建自定义 runner 镜像:Node + pnpm(走 npmmirror)+ rsync + ssh 客户端 + git,apt 源换成阿里云加速。
  2. 配置 Docker 拉镜像加速 :宿主机 daemon.json 或 Dockerfile 里直接写镜像源代理地址。
  3. 注册镜像给 act_runner :通过 .runnerconfig.yamllabels 绑定,注意本地镜像需要 force_pull: false
  4. 配置 SSH 免密登录 :生成密钥对、ssh-copy-id 推送公钥、私钥存入 Gitea Secrets。
  5. 编写 CI/CD workflow:checkout → 安装依赖 → 构建 → 建远程目录 → rsync 上传 → 切换软链接并清理旧版本。
  6. 注意 Docker 挂载场景下的相对路径软链接,避免 nginx 500。

至此,一次 git push 即可完成从代码构建到远程灰度发布的完整自动化链路。

\

相关推荐
troy1282 小时前
Codex 安全盲区:代码漏洞生成实测
windows·python·ci/cd·pycharm·django·github·fastapi
小马同学-3 小时前
nerdctl实战:Docker风格的Containerd操作利器
docker·容器·containerd
九皇叔叔4 小时前
Kubernetes 命令式对象配置详解:使用 YAML 管理 Kubernetes 资源
docker·容器·k8s
CVer儿5 小时前
docker内llama.cpp和TRT-LLM win11本机部署qwen3.8
docker
萤火夜5 小时前
Docker(四) Docker介绍
docker·容器
.冰块.6 小时前
Docker 镜像深度学习:分层 Copy‑on‑Write、Dockerfile 语法、Harbor 私有仓库实操
docker·容器·harbor·镜像·dockerfile·images
szephyr6 小时前
Docker 镜像瘦身实战:从 1.2GB 压到 85MB 的完整过程
docker·容器·部署·多阶段构建·镜像优化
溪语流沙8 小时前
Django + Vue电商项目第001讲:开篇|注册登录加增删改查,那不是电商
redis·python·mysql·docker·typescript·django·vue
一直在努力学习的菜鸟10 小时前
Docker Info 详细解析(Rocky Linux 8.10 / Docker 29.8.1)
docker