From e8c3394d35afa0936230c48c3a8c8c9268c48795 Mon Sep 17 00:00:00 2001 From: yiciheng Date: Mon, 31 Aug 2026 17:31:28 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20=E6=96=B0=E5=A2=9E=20Gitea=20Actions=20?= =?UTF-8?q?CI/CD=20=E4=BB=8E=200=20=E5=88=B0=201=20=E9=83=A8=E7=BD=B2?= =?UTF-8?q?=E5=AE=9E=E5=BD=95=EF=BC=88=E7=8E=AF=E5=A2=83=E4=BF=A1=E6=81=AF?= =?UTF-8?q?/=E8=B8=A9=E5=9D=91=E6=A0=B9=E5=9B=A0/=E6=9C=80=E7=BB=88?= =?UTF-8?q?=E9=85=8D=E7=BD=AE/=E6=8E=92=E6=9F=A5=E5=91=BD=E4=BB=A4/?= =?UTF-8?q?=E5=8F=AF=E5=A4=8D=E7=94=A8=E6=8F=90=E7=A4=BA=E8=AF=8D=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/CICD-DEPLOY.md | 378 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 378 insertions(+) create mode 100644 docs/CICD-DEPLOY.md diff --git a/docs/CICD-DEPLOY.md b/docs/CICD-DEPLOY.md new file mode 100644 index 0000000..496132d --- /dev/null +++ b/docs/CICD-DEPLOY.md @@ -0,0 +1,378 @@ +# Gitea Actions CI/CD 从 0 到 1 部署实录(freedom 项目) + +> 本文档记录 freedom-admin / freedom-web 两个仓库在自建 Gitea 上实现 CI/CD 的完整过程: +> **push master → 自动构建 Docker 镜像 → docker compose 部署 → 公网统一入口访问**。 +> 包含环境信息、踩坑记录(根因分析)、最终可用配置、验证方法与可复用的 AI 排查提示词。 + +--- + +## 1. 目标与成果 + +| 项 | 结果 | +|---|---| +| 触发方式 | push 到 master(另支持手动 workflow_dispatch 重跑) | +| 后端 freedom-admin | CI 构建 `freedom-admin:latest` → compose 部署 backend 容器(内网 8800) | +| 前端 freedom-web | CI 构建 `freedom-web:latest` → compose 部署 web 容器(宿主 8880 → 容器 80) | +| 统一入口 | `freedom-web`(nginx):`/api/*` 反代到 `backend:8800`(去前缀),其余走前端静态文件 | +| 公网访问 | frp 隧道:`http://124.221.214.237:8888` → `192.168.1.158:8880` | + +**验证通过的完整链路**: +`公网 :8888 → frp → freedom-web(nginx) → freedom-admin(SpringBoot) → MySQL` + +--- + +## 2. 环境信息(实测环境,明确列出) + +### 2.1 主机与基础服务 + +| 组件 | 信息 | +|---|---| +| Gitea 主机 | Ubuntu 25.04,内网 `192.168.1.158`(主机名 jiaxun-EliteMini-Series) | +| 公网入口 | frp 服务端 `124.221.214.237:7000`(frpc 由 1Panel 管理,容器 `1Panel-frpc-hn0g`) | +| Gitea | v26.4.6 EE,内网 `http://192.168.1.158:3000`(容器 `1Panel-gitea-MED4`),公网 `http://124.221.214.237:3000` | +| MySQL | 容器 `1Panel-mysql-uagl`,root 密码见 `/opt/freedom/deploy/.env`(勿写入文档/仓库) | +| Gitea API | `http://127.0.0.1:3000/api/v1`,Token 存放于管理员本地(勿入库) | + +### 2.2 Runner + +| 组件 | 信息 | +|---|---| +| 容器名 | `gitea-actions-runner`,镜像 `gitea/act_runner:latest`(二进制 v0.6.1) | +| labels | `ubuntu-latest`(容量 capacity=1,多个 job 串行) | +| 配置文件 | 宿主 `/opt/freedom/runner-config.yml` → 绑定挂载为容器内 `/data/config.yaml` | +| **关键环境变量** | `CONFIG_FILE=/data/config.yaml`(见 3.1 坑) | +| 容器挂载 | `/var/run/docker.sock`、`/opt/freedom`、`/opt/freedom/.runner:/data/.runner`、配置文件 | +| 其它 | `--add-host host.docker.internal:host-gateway`,`--restart always` | + +**runner-config.yml(生效版)**: + +```yaml +log: + level: info +runner: + file: /data/.runner + capacity: 1 +container: + privileged: false + valid_volumes: + - /opt/freedom + - /var/run/docker.sock + force_pull: false +``` + +**runner 容器完整创建命令**(可复现): + +```bash +docker run -d --name gitea-actions-runner --restart always \ + --add-host host.docker.internal:host-gateway \ + -e GITEA_INSTANCE_URL=http://host.docker.internal:3000 \ + -e GITEA_RUNNER_REGISTRATION_TOKEN=<注册token> \ + -e GITEA_RUNNER_NAME=local-runner -e TZ=Asia/Shanghai \ + -e CONFIG_FILE=/data/config.yaml \ + -v /opt/freedom/runner-config.yml:/data/config.yaml \ + -v /opt/freedom/.runner:/data/.runner \ + -v /var/run/docker.sock:/var/run/docker.sock \ + -v /opt/freedom:/opt/freedom \ + gitea/act_runner:latest +``` + +### 2.3 部署目录 + +| 路径 | 内容 | +|---|---| +| `/opt/freedom/deploy/docker-compose.yml` | backend + web 编排(web 宿主端口 8880) | +| `/opt/freedom/deploy/.env` | DB_URL / DB_USERNAME / DB_PASSWORD(敏感,不入库) | +| `/opt/freedom/workspace-admin`、`workspace-web` | CI 每次运行时重新 `git clone` 的工作目录 | +| `/opt/freedom/.runner` | runner 注册凭据 | + +### 2.4 镜像 / 基础设施版本 + +- job 容器镜像:`docker:24.0-cli`(自带 docker CLI + compose 插件 v2) +- 后端构建:`maven:3.6.3-jdk-8` + 运行 `openjdk:8-jre-slim`(Maven 配阿里云镜像) +- 前端构建:`node:16-alpine` + 运行 `nginx:1.24-alpine`(npm 配 npmmirror) +- dockerd:Ubuntu 源 `docker.io 27.5.1` + `containerd 2.0.5`(**注意版本错配风险,见第 6 节**) + +--- + +## 3. 踩坑记录(按重要性排序,均经二分定位实测确认) + +### 3.1 【根因】act_runner 配置文件根本没有被加载 + +**现象**: +- workflow 里 `container.options: -v /opt/freedom:/opt/freedom`,job 容器创建成功,但容器内 `/opt/freedom` 不存在; +- runner 日志出现 `[/opt/freedom] is not a valid volume, will be ignored`——尽管它明明在 `valid_volumes` 白名单里; +- 使用 workflow 级 `container.volumes` 时则更糟:静默失败,runner 日志止于 `NewParallelExecutor: Creating 1 workers for 0 executors`,Gitea 侧 job 同秒失败、无任何步骤日志。 + +**根因**: +官方镜像的 `/usr/local/bin/run.sh` 只在环境变量 **`CONFIG_FILE`** 有值时才追加 `--config`: + +```bash +CONFIG_ARG="" +if [[ ! -z "${CONFIG_FILE}" ]]; then + CONFIG_ARG="--config ${CONFIG_FILE}" +fi +exec act_runner daemon ${CONFIG_ARG} +``` + +而本机容器历史上设置的是 `CONFIG_ARG=--config=...`(run.sh 完全不读这个变量),导致 `act_runner daemon` **以空配置运行**: +- `valid_volumes` 为空 → workflow 提供的所有 bind 被静默剥离; +- `container.volumes` 解析后全被剥光 → 计划阶段失败(0 executors)。 + +**验证方法**:配置文件里写 `log.level: debug` 后重启 runner,看容器日志是否出现 `log level set to debug`。若无,即配置未加载。 + +**修复**:重创容器时显式加 `-e CONFIG_FILE=/data/config.yaml`(见 2.2 完整命令)。 + +### 3.2 docker.sock 无需显式挂载(显式挂必错) + +act_runner 会**自动**把 `/var/run/docker.sock` 挂进 job 容器(job 日志 `Merged container.HostConfig Binds:[/var/run/docker.sock:/var/run/docker.sock]` 可证)。 +若 workflow 再显式挂一次 → `failed to create container: Duplicate mount point: /var/run/docker.sock`。 + +**结论:workflow 里永远不要声明 docker.sock。** + +### 3.3 act_runner 不会自动检出代码 + +job 容器的 `/workspace//` 是**空目录**(实测)。必须自行 `git clone`: + +```yaml +- name: 拉取 master 代码 + run: | + rm -rf /opt/freedom/workspace-admin + git clone --depth 1 --branch master \ + "http://oauth2:${GITHUB_TOKEN}@host.docker.internal:3000/${GITHUB_REPOSITORY}.git" \ + /opt/freedom/workspace-admin + env: + GITHUB_TOKEN: ${{ github.token }} +``` + +`docker:24.0-cli` 是 alpine 底座,先 `apk add --no-cache git`。 + +### 3.4 job 容器内 host.docker.internal 解析 + +需要 options 里带 `--add-host=host.docker.internal:host-gateway`(解析到 docker 网桥网关 172.17.0.1,即可访问宿主上的 Gitea 3000)。 + +### 3.5 volumes 与 options 的取舍(最终方案 A) + +| 写法 | 结果 | +|---|---| +| `container.volumes:`(YAML 列表) | act_runner v0.6.1 下静默失败(0 executors),弃用 | +| `options: -v ...`(字符串) | 可用,但依赖 valid_volumes 白名单(配置必须真正被加载) | +| **最终**:`options: -v /opt/freedom:/opt/freedom --add-host=host.docker.internal:host-gateway` | ✅ 实测通过 | + +### 3.6 其它经验 + +- **workspace 与 build context**:`docker build` 的 context 由 CLI 打包经 sock 发给宿主 daemon,所以 job 容器内的路径(含 bind 进来的 /opt/freedom)都可以直接作为 build context。 +- **首次构建慢**:基础镜像(maven/openjdk/node/nginx)从 docker.io 拉取慢(~10-20KB/s)。宿主 `/etc/docker/daemon.json` 已配 11 个 registry-mirrors,小镜像快、大镜像慢;首次忍一忍,之后走本地缓存。 +- **Maven/npm 源**:Dockerfile 内分别固定阿里云 Maven 镜像与 npmmirror,避免依赖下载卡死。 +- **compose 里或容器环境有历史遗留**:出现 `Found orphan containers ([wechatpadpro])` 警告属无害噪音。 + +--- + +## 4. 最终可用配置(全文) + +### 4.1 freedom-admin `.gitea/workflows/deploy.yml` + +```yaml +name: CI/CD Deploy (Admin) + +on: + push: + branches: + - master + workflow_dispatch: + +jobs: + build-and-deploy: + name: 构建后端镜像并部署 + runs-on: ubuntu-latest + container: + image: docker:24.0-cli + # docker.sock 由 act_runner 自动挂载,切勿重复声明(会报 Duplicate mount point) + # /opt/freedom 需在 runner-config.yml 的 container.valid_volumes 白名单内 + options: -v /opt/freedom:/opt/freedom --add-host=host.docker.internal:host-gateway + steps: + - name: 安装 git + run: apk add --no-cache git + + - name: 拉取 master 代码 + run: | + set -euo pipefail + rm -rf /opt/freedom/workspace-admin + git clone --depth 1 --branch master \ + "http://oauth2:${GITHUB_TOKEN}@host.docker.internal:3000/${GITHUB_REPOSITORY}.git" \ + /opt/freedom/workspace-admin + cd /opt/freedom/workspace-admin + git log -1 --oneline + env: + GITHUB_TOKEN: ${{ github.token }} + + - name: 构建后端镜像 + run: docker build -t freedom-admin:latest /opt/freedom/workspace-admin + + - name: 部署后端服务 + run: | + set -euo pipefail + cd /opt/freedom/deploy + test -f docker-compose.yml || { echo "缺少 docker-compose.yml"; exit 1; } + test -f .env || { echo "缺少 .env"; exit 1; } + docker compose up -d --force-recreate backend + + - name: 确认部署结果 + run: cd /opt/freedom/deploy && docker compose ps backend +``` + +freedom-web 的 workflow 结构完全相同,仅三处不同:clone 目标 `workspace-web`、镜像名 `freedom-web:latest`、compose 服务 `web`。 + +### 4.2 `/opt/freedom/deploy/docker-compose.yml`(关键段) + +```yaml +services: + backend: + image: freedom-admin:${IMAGE_TAG:-latest} + container_name: freedom-admin + restart: unless-stopped + expose: ["8800"] # 仅内网,供 nginx 反代 + environment: + DB_URL: ${DB_URL} + DB_USERNAME: ${DB_USERNAME} + DB_PASSWORD: ${DB_PASSWORD} + TZ: Asia/Shanghai + networks: [freedom-net] + + web: + image: freedom-web:${IMAGE_TAG:-latest} + container_name: freedom-web + restart: unless-stopped + ports: + - "8880:80" # 宿主 8880 → 容器 80(统一入口) + depends_on: [backend] + networks: [freedom-net] + +networks: + freedom-net: + driver: bridge +``` + +### 4.3 frp 转发(`/opt/1panel/apps/frpc/frpc/data/frpc.toml` 末段) + +```toml +[[proxies]] +name = "freedom" +type = "tcp" +localIP = "192.168.1.158" +localPort = 8880 # freedom-web 宿主端口 +remotePort = 8888 # 公网端口 +``` + +改后 `docker restart 1Panel-frpc-hn0g` 生效。 + +--- + +## 5. 验证与排查命令速查(全部在本机执行) + +```bash +# 查 runner 日志(容器创建/挂载剥离等关键信息都在这里) +docker logs --tail 100 gitea-actions-runner + +# 查 run 列表 / job 日志(TOKEN 为管理员 API token) +curl -s -H "Authorization: token " \ + "http://127.0.0.1:3000/api/v1/repos/yiciheng/freedom-admin/actions/runs?limit=5" +curl -s -H "Authorization: token " \ + "http://127.0.0.1:3000/api/v1/repos/yiciheng/freedom-admin/actions/jobs//logs" + +# 不改代码重跑(需 workflow 声明 workflow_dispatch) +curl -s -X POST -H "Authorization: token " -H "Content-Type: application/json" \ + -d '{"ref":"master"}' \ + "http://127.0.0.1:3000/api/v1/repos/yiciheng/freedom-admin/actions/workflows/deploy.yml/dispatches" + +# 端到端验证 +curl -s -o /dev/null -w "%{http_code}\n" http://192.168.1.158:8880/ # 本机前端 +curl -s -o /dev/null -w "%{http_code}\n" http://124.221.214.237:8888/ # 公网前端 +curl -s -o /dev/null -w "%{http_code}\n" -X POST -d '{"username":"t","password":"t"}' \ + http://124.221.214.237:8888/api/login # 公网 API(经 nginx 去前缀转后端) +docker logs --tail 20 freedom-admin # 确认请求与 SQL 到达后端 + +# 判断 runner 配置是否真正加载(最关键的一步排查) +docker exec gitea-actions-runner ps aux | grep act_runner # 必须看到 --config /data/config.yaml +``` + +**job 日志里找挂载证据**(debug 级别下): +- `Merged container.HostConfig ==> &{Binds:[/var/run/docker.sock:/var/run/docker.sock /opt/freedom:/opt/freedom]` → 两个挂载都在; +- 若随后出现 `[/opt/freedom] is not a valid volume, will be ignored` → 配置没加载,回到 3.1。 + +--- + +## 6. 遗留风险(部署成功后仍需处理) + +**dockerd 反复 SIGSEGV(已实测 4 次/日,均发生在 CI job 容器创建/清理时)**: + +- 现象:`journalctl -u docker` 可见 `Main process exited, code=dumped, status=11/SEGV`,systemd 自动拉起; +- 危害:崩溃瞬间在跑的 CI job 失败、刚部署的容器被 SIGTERM(exit 143),且 `unless-stopped` 容器可能不会自动恢复; +- 根因判断:**版本错配**——Ubuntu 源 `docker.io 27.5.1` 配 `containerd 2.0.5`,而 docker 27.x 官方仅兼容 containerd 1.7.x;本机同时残留 `docker-ce 28.3.3` / `containerd.io 1.7.27` 的卸载残留(dpkg 状态 rc); +- 建议修复:统一安装 `docker-ce 28.3.3 + containerd.io 1.7.27` 并移除 `docker.io`/Ubuntu `containerd`(影响所有容器,操作前做好停机窗口与镜像/卷盘点)。 + +--- + +## 7. 附录:AI 排查提示词(可复用) + +以下为本次从 0 到 1 排查修复过程中实际使用、并被验证有效的提示词。遇到同类「自建 Gitea Actions + act_runner + Docker 部署」问题时可直接复用,替换尖括号内容即可。 + +### 7.1 初始任务提示词(完整背景移交) + +```text +你是我的 Gitea Actions CI/CD 排查与修复助手。以下任务背景与进度基于真实环境排查得出,请据此继续,不要重复已完成事项。所有命令都在当前这台机器(Gitea 主机,<内网IP>,Ubuntu)本机执行。 + +# 任务目标 +为 /(后端)与 /(前端)实现 CI/CD:push master 后自动构建 Docker 镜像并 docker compose up -d 部署到 <部署目录>,公网经 frp(<公网IP>:7000)提供统一入口。 + +# 环境信息(已核实) +- Gitea <版本>:本机 <内网IP>:3000,公网 http://<公网IP>:3000 +- act_runner:容器 ,镜像 gitea/act_runner:latest,labels ubuntu-latest +- runner 容器自身挂载 docker.sock + <部署根目录>;配置 <宿主配置路径> +- 部署目录 <部署目录>(docker-compose.yml + .env 已就绪) +- Gitea API:http://127.0.0.1:3000/api/v1,Authorization: token + +# 已办 / 已确认的事实(二分定位结论) +<列出已验证的事实与失败现象,例如:所有 run 失败、runner 日志止于何处、 +最小复现仓库的各次实验结果、报错原文> + +# 未办 / 下一步(按顺序执行) +<编号列出待办> + +# 常用命令速查(本机) +<贴出查 runner 日志 / 查 runs / 拉 job 日志 / 重跑 / 查 payload 的具体命令> +``` + +> **要点**:把「已确认事实 / 二分结论」单独列出并明确「不要重复已完成事项」,能让 AI 直接从断点继续,避免绕圈重试已被排除的方案。 + +### 7.2 过程中有效的短提示词(按场景) + +```text +# 定位配置是否生效(最高性价比的一步): +检查 /data/config.yaml 是否真的被 act_runner 加载:看容器进程命令行是否带 --config, +再把 log.level 改成 debug 重启对比日志级别。 + +# 决策挂载方案(给出候选让 AI 验证): +据此确定最终挂载方案(候选优先级):A. workflow options 只挂 <目录> + --add-host, +不显式挂 docker.sock;B. runner-config 统一提供挂载;C. 升级 act_runner。 + +# 执行授权(明确授权破坏性操作): +A方案执行。 + +# 修复后验证: +重跑 ci-test,确认 job 容器内 <目录> 可读写、docker.sock 可用、docker compose 可用。 + +# 端口/入口调整: +将 freedom-web 的端口改为 <新端口>(同步更新 compose 与 frpc 转发并验证公网)。 +``` + +### 7.3 本次沉淀的方法论 + +1. **先建最小复现仓库**(如 ci-test),用「最小 workflow → 逐项加 container.image / volumes / options」二分定位,比直接在真实仓库试错快得多; +2. **区分"三类日志"**:runner 容器日志(守护进程与容器创建)、Gitea job 日志(步骤输出 + Merged HostConfig 调试行)、dockerd journalctl(崩溃/信号),交叉对时间戳; +3. **静默失败必看 Merged 配置行**:`Merged container.HostConfig ==> &{Binds:[...]}` 是真相所在,警告行(如 "not a valid volume")出现在 Merged 之后意味着运行时剥离; +4. **改基础设施配置后必须验证"真的加载了"**(本例的 CONFIG_FILE 教训); +5. 端到端验证要**贯通到业务层**(curl 到具体 API + 查后端 SQL 日志),不要只看容器 Up。 + +--- + +*文档生成于 2026-08-31,对应 act_runner v0.6.1 / Gitea 26.4.6 EE 实测环境。*