Gitea Actions 全攻略:把 GitHub Actions 私有化

一、为什么是 Gitea Actions
GitHub Actions 改变了大家写 CI 的方式,但它有两个绕不开的痛点:代码要放到 GitHub 上,免费额度有限。对于公司内部代码、或者不想把构建过程暴露到公网的场景,需要一个能完全离线、完全自控的替代品。
Gitea Actions 就是答案。它直接复用 GitHub Actions 的生态——同样的 YAML 语法、同样的 uses: 复用机制、甚至能直接拉 GitHub 上的官方 Action(actions/checkout、docker/build-push-action 等都能用),但跑在你自己的机器上,没有任何外部依赖。
这篇文章我会从零搭一套完整链路:
- 用
docker-compose起一个 Gitea 实例(带 Postgres) - 注册一个
gitea-runner(Gitea 的 CI 执行器) - 写一个最简单的 Go 服务,用 Gitea Actions 把它构建成 amd64 + arm64 多架构镜像,推送到一个私有 Zot registry
所有代码、配置、.runner 文件,全部公开。
二、起 Gitea:docker-compose 一把梭
Gitea 是个单体二进制,但生产里一般配 Postgres。这里贴的 docker-compose.yaml 是我本机实际在跑的:
1 | |
几个要点:
GITEA__database__*这些环境变量:Gitea 支持用GITEA__<section>__<key>的形式直接通过环境变量覆盖app.ini配置。这里把数据库指向了同一个 compose 网络里的db:5432,免得装完进 Web UI 手动配。HTTP_PROXY/HTTPS_PROXY:如果你在内网、需要拉取或者迁移 GitHub Action 的话,需要给 Gitea 容器配代理。我这里是走宿主机的172.29.96.1:7897。如果你的 Gitea 完全离线、所有 Action 都走私有镜像源,这两行可以删掉。- 端口
3000:3000是 Web,10022:22是 SSH。SSH 端口特意避开 22,是为了不和宿主机的 sshd 打架。 - 数据持久化到
/opt/gitea和/opt/postgres。容器删了重建,数据还在。
docker compose up -d 起来之后,访问 http://<你的IP>:3000,第一次会让你创建管理员账号。这里假设创建好后访问地址是 http://172.29.102.221:3000(下文统一用这个 IP)。
前置依赖:Gitea Actions 需要在
app.ini里启用。 Gitea 1.27+ 默认开启,如果你用老版本,需要在/opt/gitea/gitea/conf/app.ini加:
1
2[actions]
ENABLED = true然后重启容器:
docker restart gitea。
三、装 gitea-runner:CI 的执行器
Gitea 本身只负责调度(什么时候触发、执行哪个 workflow),真正干活的是 gitea-runner(也叫 Gitea Runner)。架构上:
1 | |
gitea-runner 是个独立的二进制,跟 Gitea server 是解耦的——你可以装在宿主机上、装在 k8s 里、或者装在另一台机器上。本篇就用最简单的:装在跑 Gitea 的同一台宿主机上。
3.1 下载二进制
1 | |
3.2 注册 runner
注册前,先在 Gitea Web UI 拿 token:站点管理 → Actions → Runners → 创建 Runner,会给你一串 token。
1 | |
这里有个关键概念,值得展开讲:--labels 决定了 workflow 里的 runs-on: 到底匹配到什么。
runs-on 与 labels 的对应关系
在 GitHub Actions 里,你写 runs-on: ubuntu-latest,GitHub 把它调度到它云上的 Ubuntu runner。但在 Gitea,没有云上的 runner,全靠你注册时声明的 labels 来匹配。看一下注册成功后生成的 .runner 文件:
1 | |
label 的格式是 名字:实现,冒号后面是这个 label 实际跑在什么里:
ubuntu-latest:docker://docker.gitea.com/runner-images:ubuntu-latest的意思是——当 workflow 写runs-on: ubuntu-latest时,runner 会docker pull docker.gitea.com/runner-images:ubuntu-latest,然后在容器里执行 job。- 这是 Gitea runner 最常见的模式:每个 job 跑在一个 Docker 容器里,跟你 GitHub Actions 上的体验一致。
⚠️ 大坑:用了
docker://这种 label,宿主机必须有 Docker。因为 runner 要调用 Docker daemon 起容器。所以 runner 二进制要么装在宿主机上(能看到docker命令),要么用 DinD(Docker-in-Docker)。
注册时这个文件会生成在你 gitea-runner 的当前工作目录下。我建议给 runner 单独建个目录,比如 /opt/runner/,把 .runner 放里面,别跟别的混。
3.3 生成 config 并后台运行
1 | |
跑起来后回到 Gitea Web UI 的 Runners 页面,应该能看到 gitea-runner 状态变成 idle(绿色),说明它已经在线等活了。

四、构建项目:一个最小 Go 服务
我们来实战。准备一个最小化的 Go + Fiber 服务,目录结构:
1 | |
main.go:
1 | |
go.mod:
1 | |
Dockerfile(多阶段构建,最终镜像用 distroless):
1 | |
注意两个细节:
- 基础镜像都从
172.29.102.221:5000/拉——这是一个我本地起的私有 registry(就是后文的 Zot),里面缓存了golang:1.26和distroless/static-debian12:nonroot。内网拉镜像不走公网,快且不依赖外网。 CGO_ENABLED=0+ distroless static:得到一个完全静态链接、没有任何 shell 的极小镜像(几 MB 级别),同时方便做多架构(amd64/arm64 都能编出来,不用担心 glibc 版本)。
五、核心:workflow 文件,逐行讲
这是本篇最关键的部分。.gitea/workflows/actions.yaml:
1 | |
里面的几个概念,一个个解读。
5.1 on: push: tags: ["v*"] —— 触发条件
这个 workflow 只在打 tag(tag 名以 v 开头,比如 v1.0.0)时触发。对应的命令是:
1 | |
push 一个 v 开头的 tag,Gitea 收到事件,匹配到这个 workflow,丢给 runner 执行。普通的 commit push 不会触发,避免每次提交都构建镜像。
5.2 runs-on: ubuntu-latest —— 跑在哪个容器里
回顾第三节讲的 labels:runner 注册时声明了 ubuntu-latest:docker://docker.gitea.com/runner-images:ubuntu-latest,所以这里写 runs-on: ubuntu-latest,runner 就会去拉 docker.gitea.com/runner-images:ubuntu-latest 这个镜像,把整个 job 关进这个容器里跑。
这正是 Gitea Actions 和 GitHub Actions 体验一致的关键——你写 runs-on: ubuntu-latest,得到的也是一个干净的 Ubuntu 环境,里面预装了 docker、git、curl 这些常用工具。
5.3 uses: http://172.29.102.221:3000/... —— 私有 Action 源前缀(重点)
这是 Gitea Actions 最不一样、也是最容易踩坑的地方。
在 GitHub Actions,你写:
1 | |
Gitea 默认从是 github.com/actions/checkout 拉的。但是我们已经提前把这些actions全部mirror了一份,所以写全 URL,而且这个 URL 指向的是 Gitea 上你自己存的 mirror:
1 | |
拆开看:
| 片段 | 含义 |
|---|---|
http://172.29.102.221:3000 |
你的 Gitea 实例地址(没有协议前缀 Gitea 不认) |
/actions/checkout |
Gitea 上一个名为 actions 的组织(org)下的 checkout 仓库 |
@v7 |
这个仓库的 tag/分支 |
所以你得先把 GitHub 上的常用 Action mirror 一份到自己的 Gitea:
除非你是自由身
1 | |
Gitea 的「迁移仓库」支持把 GitHub 仓库(含所有 tag)拉过来做镜像。常用的那几个 Action 我都 mirror 了:
actions/checkoutdocker/login-actiondocker/setup-qemu-actiondocker/setup-buildx-actiondocker/build-push-action
💡 为什么 workflow 文件里把
actions/checkout那步注释掉了?
因为本例里我们只构建镜像、不跑单测,源码由 buildx 在构建 Docker 镜像时通过context直接拿到(runner 容器启动时已经把仓库 clone 到${{ github.workspace }}了)。但如果你要在 workflow 里跑go test,就必须显式uses: actions/checkout@v7一下,否则 workspace 是空的。
5.4 三件套:QEMU + Buildx + Build
构建多架构镜像的标准三步:
① QEMU(docker/setup-qemu-action):让 amd64 宿主机能通过模拟跑 arm64 的二进制。没这一步,platforms: linux/arm64 会直接报错。
② Buildx(docker/setup-buildx-action):Docker 官方的多架构构建器,基于 BuildKit。这里有个关键配置:
1 | |
这两行告诉 BuildKit:拉/推 172.29.102.221:5000 这个 registry 时走 http(不是 https)、并且跳过证书校验。因为我的 Zot registry 是个纯 http、自签/无签的内部服务。没这个配置,buildx 会因为 https 握手失败而推不上去——这是私有 registry 最常见的坑。
③ Build and push(docker/build-push-action):
1 | |
${{ github.ref_name }}是触发 tag 的名字(去掉refs/tags/前缀),比如打v1.0.0,最终镜像 tag 就是v1.0.0。provenance: false和sbom: false:Buildx 默认会额外推一个 provenance(构建来源溯源)manifest 和 SBOM(软件物料清单)。这俩东西在某些 registry(尤其 Zot 老版本)里会触发 tag 列表异常,关掉更干净。github.ref_name而不是gitea.ref_name:Gitea Actions 同时暴露了gitea.*和github.*两套上下文,值完全一样,github.*是为了兼容 GitHub Actions 现有生态,方便你直接抄 GitHub 的 workflow 过来。
5.5 配置 Secrets
secrets.REGISTRY_USERNAME / secrets.REGISTRY_PASSWORD 不能明文写,要在 Gitea Web UI 配:
仓库 → Settings → Secrets → Actions → Add Secret
加两个:REGISTRY_USERNAME、REGISTRY_PASSWORD。workflow 里用 ${{ secrets.XXX }} 引用,runner 执行时会注入,日志里会打码。
六、跑起来:打 tag,看执行
代码 push 到 Gitea 之后,触发构建:
1 | |
回到 Gitea Web UI:仓库 → Actions,能看到 release 这个 workflow 正在跑。点进去能看到每一步的实时日志。
执行流程按 step 顺序展开:
- Set up Job:runner 拉起
ubuntu-latest容器,把仓库源码挂进去。 - Login to Docker Hub:
docker/login-action拿 secrets 里的账密,docker login到172.29.102.221:5000。 - Set up QEMU:注册
qemu-aarch64-static等二进制,让内核能 exec arm64 ELF。 - Set up Docker Buildx:起 buildkitd 容器,把上面那段 inline config 灌进去。
- Build and push:buildx 串起
golang:1.26(builder 阶段,amd64+arm64 各跑一次)→distroless(runner 阶段)→ 推到 Zot。
如果某一步红了,常见原因:
| 现象 | 原因 |
|---|---|
uses: http://...actions/checkout@v7 拉不到 |
这个 Action 没 mirror 到你的 Gitea,或者 mirror 了但没 v7 这个 tag |
docker login 报 401 |
secrets 名字拼错,或账密不对 |
buildx 推送时报 http: server gave HTTP response to HTTPS client |
buildkitd-config-inline 没配 http = true |
arm64 步骤 exec format error |
QEMU 没装上,或宿主机的 binfmt 没注册 |
七、结果:Zot registry 里的多架构镜像
构建成功后,镜像会出现在 172.29.102.221:5000/actions/demo:v1.0.0。我用 Zot(一个轻量、CNCF 兼容的 registry 实现)作为私有 registry,它的 Web UI 里能看到这次 push 出来的 manifest:

一个 tag 下面挂着两个 platform(linux/amd64、linux/arm64),每个 platform 各有一份 layer。在 amd64 机器上 docker pull 自动拿 amd64 那份,在 arm64 机器上自动拿 arm64 那份——对使用者完全透明,这是 OCI image manifest 的标准能力。
验证一下:
1 | |
八、总结:私有化 GitHub Actions 的完整拼图
把整条链路串起来,Gitea Actions 私有化一共四块拼图:
- Gitea server(
docker-compose起)——托管代码 + 调度 CI。 - gitea-runner(注册到 server,靠 labels 决定 job 跑在哪)——执行 CI。
- Action mirror(把 GitHub 的
actions/*、docker/*mirror 到自己 Gitea)——解决uses:前缀。 - 私有 registry(Docker registry / Zot / Harbor 都行,记得配
insecure+http)——接收构建产物。
记住这几个最容易卡的地方,能省你大半天:
runs-on: ubuntu-latest不是魔法,它对应 runner 注册时的某个 label;label 后面的docker://...才决定实际执行容器。uses:后面的 URL 必须带http(s)://前缀 + 你的 Gitea 域名,并且 Action 仓库要预先 mirror 到本地。- 推私有 http registry,Buildx 的
buildkitd-config-inline里一定要写http = true; insecure = true。 github.*和gitea.*上下文等价,抄 GitHub workflow 过来基本不用改。
到此,你就拥有了一套完全私有、零外部依赖的 CI/CD——语法和 GitHub Actions 一模一样,但代码、构建、产物全部不出内网。对内部项目、离线环境、或者单纯想自己掌控 CI 的同学,这套方案基本能带你上道。