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),只要 current 和 releases 目录始终在同一个父目录下、一起被挂载进容器,相对关系在容器内依然成立。
对应的 nginx 挂载配置:
yaml
services:
nginx:
image: nginx:alpine
volumes:
- /data/www/myapp:/usr/share/nginx/html
ports:
- "80:80"
六、Gitea Actions 私有仓库 checkout 说明
- Gitea Actions 会自动生成短期
GITEA_TOKEN,checkoutaction 默认用它通过 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 步骤可能失败。
七、总结
整套流程可以概括为:
- 构建自定义 runner 镜像:Node + pnpm(走 npmmirror)+ rsync + ssh 客户端 + git,apt 源换成阿里云加速。
- 配置 Docker 拉镜像加速 :宿主机
daemon.json或 Dockerfile 里直接写镜像源代理地址。 - 注册镜像给 act_runner :通过
.runner或config.yaml的labels绑定,注意本地镜像需要force_pull: false。 - 配置 SSH 免密登录 :生成密钥对、
ssh-copy-id推送公钥、私钥存入 Gitea Secrets。 - 编写 CI/CD workflow:checkout → 安装依赖 → 构建 → 建远程目录 → rsync 上传 → 切换软链接并清理旧版本。
- 注意 Docker 挂载场景下的相对路径软链接,避免 nginx 500。
至此,一次 git push 即可完成从代码构建到远程灰度发布的完整自动化链路。
\