Files
freedom-admin/docs/CICD-DEPLOY.md
T
2026-08-31 17:31:28 +08:00

17 KiB
Raw Blame History

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(生效版):

log:
  level: info
runner:
  file: /data/.runner
  capacity: 1
container:
  privileged: false
  valid_volumes:
    - /opt/freedom
    - /var/run/docker.sock
  force_pull: false

runner 容器完整创建命令(可复现):

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:

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/<owner>/<repo> 是空目录(实测)。必须自行 git clone:

- 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

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(关键段)

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 末段)

[[proxies]]
name = "freedom"
type = "tcp"
localIP = "192.168.1.158"
localPort = 8880        # freedom-web 宿主端口
remotePort = 8888       # 公网端口

改后 docker restart 1Panel-frpc-hn0g 生效。


5. 验证与排查命令速查(全部在本机执行)

# 查 runner 日志(容器创建/挂载剥离等关键信息都在这里)
docker logs --tail 100 gitea-actions-runner

# 查 run 列表 / job 日志(TOKEN 为管理员 API token)
curl -s -H "Authorization: token <TOKEN>" \
  "http://127.0.0.1:3000/api/v1/repos/yiciheng/freedom-admin/actions/runs?limit=5"
curl -s -H "Authorization: token <TOKEN>" \
  "http://127.0.0.1:3000/api/v1/repos/yiciheng/freedom-admin/actions/jobs/<jobId>/logs"

# 不改代码重跑(需 workflow 声明 workflow_dispatch)
curl -s -X POST -H "Authorization: token <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 初始任务提示词(完整背景移交)

你是我的 Gitea Actions CI/CD 排查与修复助手。以下任务背景与进度基于真实环境排查得出,请据此继续,不要重复已完成事项。所有命令都在当前这台机器(Gitea 主机,<内网IP>,Ubuntu)本机执行。

# 任务目标
为 <owner>/<backend>(后端)与 <owner>/<frontend>(前端)实现 CI/CD:push master 后自动构建 Docker 镜像并 docker compose up -d 部署到 <部署目录>,公网经 frp(<公网IP>:7000)提供统一入口。

# 环境信息(已核实)
- Gitea <版本>:本机 <内网IP>:3000,公网 http://<公网IP>:3000
- act_runner:容器 <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 <TOKEN>

# 已办 / 已确认的事实(二分定位结论)
<列出已验证的事实与失败现象,例如:所有 run 失败、runner 日志止于何处、
最小复现仓库的各次实验结果、报错原文>

# 未办 / 下一步(按顺序执行)
<编号列出待办>

# 常用命令速查(本机)
<贴出查 runner 日志 / 查 runs / 拉 job 日志 / 重跑 / 查 payload 的具体命令>

要点:把「已确认事实 / 二分结论」单独列出并明确「不要重复已完成事项」,能让 AI 直接从断点继续,避免绕圈重试已被排除的方案。

7.2 过程中有效的短提示词(按场景)

# 定位配置是否生效(最高性价比的一步):
检查 /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 实测环境。