1. 为什么我们决定弃用 GitLab:项目背景与痛点
1.1 项目背景与资源困境
先交代一下背景。我们是一个十几个人的研发小团队,主要做企业级 Web 应用,代码仓库一直在用自建的 GitLab。当初选 GitLab 的理由很简单:功能全、生态成熟、社区版也够用,网上教程一抓一大把。但用着用着,资源账单开始变得不好看了。
最直观的感受是部署一台 GitLab 服务器,内存 8GB 起步才算舒服。我们自己搭的这台机器配了 16GB 内存,其中 GitLab 全家桶(包括 PostgreSQL、Redis、Sidekiq、Gitaly、Prometheus 等一堆组件)轻松吃掉 8~10GB,高峰期甚至能摸到 12GB。加上我们还有一台 GitLab Runner 跑 CI,整个研发基础设施的硬件成本翻了好几倍。对于一家几十人规模的创业公司来说,每月多出来的服务器费用虽然不算致命,但总觉得这笔钱花得冤枉——毕竟我们的核心诉求只是托管代码、管好分支、跑通 CI,而不是开一个 GitLab 全家桶自助餐厅。
另一个痛点是启动速度和日常维护。GitLab 升级是出了名的折磨人,尤其是跨大版本升级的时候,需要按顺序逐版本升级,跳版本很容易出事。某次我们从小版本 15.3 升到 15.4,遇到一个迁移脚本卡住的问题,折腾了一个下午才恢复,期间整个代码托管服务不可用,团队全部卡在推送和拉代码上。那一刻我就在想:我们真的需要这么重的平台吗?
1.2 10GB vs 600MB 的差距是怎么来的
后来我们注意到行业里讨论轻量级 Git 服务的帖子越来越多,其中 Gitea 的名字出现频率最高。当时心里有个预期:它肯定比 GitLab 省资源,但具体能省多少,没实测过不敢下结论。于是我在另一台闲置的 2GB 小机器上装了 Gitea 做压测,结果有点震撼:Gitea 官方要求的起步配置极低,实际运行时我们的仓库(大约 240 个仓库、2 万多次提交)整体占用内存稳定在 400~600MB,CPU 使用率日常不到 5%。
注意这个对比,不是 10GB 降到 8GB,而是直接从 10GB 级别降到 600MB 级别,差距差不多 15~20 倍。这个数字不是 Gitea 官网吹出来的,是我在同一台 16GB 机器上,先后跑 GitLab 和 Gitea,用 htop 和 docker stats 记录下来的实测数据。更夸张的是,Gitea 的完整安装包体积也很小,一个二进制文件搞定全部功能,而 GitLab 的安装包动辄几个 GB。那种感觉就像你之前开着一台满载货柜车去便利店买瓶水,现在换了一辆自行车,依然能把水带回来。
1.3 为什么不是 Gitea 之外的其他方案
做选型对比的时候,我们认真考虑过几个方向:一个是 Gogs,一个是 Gitea,还有一个是 Gitalb 官方出的 GitLab Runner 优化方案,以及直接托管到 GitHub/Gitee 的云方案。
Gogs 和 Gitea 其实师出同门,Gitea 是 Gogs 的一个社区分支发展起来的,但现在功能演进已经明显分叉。Gitea 的社区活跃度高得多,版本更新频繁,插件和 Webhook 生态也更成熟,所以我们没太纠结,直接排除了 Gogs。云托管方案虽然省心,但代码放在第三方平台上涉及合规评估,我们团队内部讨论后觉得还是自建更稳妥,于是把目光收回到 Gitea 上——它既能解决资源问题,又能保留自建控制权,功能上覆盖我们的日常需求绰绰有余。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 轻量级替代方案的选型对比与核心优势
2.1 常见轻量级 Git 服务横向对比
如果你们团队也在纠结要不要换掉 GitLab,我觉得可以先把市面上主流的轻量级 Git 托管服务看一遍。我整理了一张对比表,基本涵盖了我们当时评估的核心维度:
| 维度 | Gitea | Gogs | GitLab CE |
|---|---|---|---|
| 内存占用(典型) | 400~600MB | 300~500MB | 8~10GB |
| 安装包体积 | 约 100MB | 约 60MB | 3GB+ |
| 安装方式 | 单二进制/ Docker | 单二进制/ Docker | 整套组件部署 |
| 社区活跃度 | 高,更新频繁 | 较低,维护节奏慢 | 高,但主要是商业版在推动 |
| 内置 CI/CD | Gitea Actions(实验性) | 弱 | 完整内置 |
| Webhook 支持 | 完善 | 基础 | 完善 |
| 迁移成本 | 低 | 低 | — |
我们实测时发现 Gogs 虽然省资源,但有些插件和 API 接口跟 GitLab 兼容性不太好,比如有个用 Jenkins 的老项目要做 Webhook 接入,Gogs 的请求格式和 GitLab 差异较大,需要额外写适配层。Gitea 的 Webhook 支持基本对齐 GitLab 的格式,迁移成本小很多。
2.2 为什么最终选择了 Gitea
选型理由归纳起来有三点,这也是我后来给团队分享时反复强调的判断逻辑。
第一点是资源效率。这一点没有争议,Gitea 用 Go 语言实现,编译成一个独立的二进制文件,没有一堆常驻服务,内存占用自然低。跑起来之后,docker stats 显示的容器内存稳定在 500MB 左右,这还是在包含 PostgreSQL 内置数据库的情况下。对比 GitLab 频繁因为内存不足而触发 OOM 甚至服务崩溃的体验,完全是两个世界。
第二点是操作习惯的平滑过渡。Gitea 的界面和操作逻辑跟 GitLab 非常像,开发者日常用到的 fork、pull request、issue、milestone、label 这些功能都有,而且概念基本一一对应。我们的开发同学第一次用 Gitea 时,几乎没有问过“这个功能在哪”之类的问题。UI 风格也是简洁挂历式排版,不用像 GitLab 那样载入一堆仪表盘组件后才显示主要页面。
第三点是可维护性。Gitea 的升级很简单,官方提供了多种升级方式,包括 Docker 镜像替换、二进制文件替换、以及内置的迁移工具。我们后来实测,从 1.18 升到 1.21,整个过程十分钟以内,而且没有遇到任何数据迁移问题。这种维护体验对于小团队来说太重要了——我们没有专门的运维工程师,能把工具链维护的复杂度降到最低,就是最大的效率提升。
2.3 资源占用背后的设计差异
为什么同样的工作,两个工具占用差距这么大?我稍微研究了一下原理,这里可以简单分享一下。
GitLab 是一个典型的 Rails 单体应用架构,集成了 Web 服务、后台任务队列、Git 远程存储、数据库、缓存、监控等多个组件。它内置了 Prometheus、Grafana 做监控,内置了 Nginx 做反向代理,内置了 PostgreSQL 和 Redis,启动之后就像一个微型的 PaaS 平台。功能丰富是优势,但代价是每个组件都要常驻内存,随便一个组件升级就可能连带出问题。
Gitea 则是用 Go 写的单体二进制应用,可以选择 SQLite 或 PostgreSQL 作为后端存储。默认情况下 SQLite 模式不依赖额外服务,一个进程搞定 HTTP 服务、Git 操作和数据库读写。我在生产环境部署时选择了 SQLite,因为我们的并发量不高(几十人同时操作),SQLite 完全能扛住,而且省掉了数据库运维负担。如果你是在更大规模的团队里使用,也可以挂外部 PostgreSQL,不过即便这样,整体资源占用依然远低于 GitLab。
3. 从 GitLab 到 Gitea 的迁移实操全记录
3.1 部署 Gitea:Docker Compose 一键拉起
我们的生产环境用的是 Docker Compose 方式部署,这也是官方推荐的靠谱路线之一。先贴一个最简的 docker-compose.yml 配置:
yaml复制version: "3"
services:
gitea:
image: gitea/gitea:latest
container_name: gitea
environment:
- USER_UID=1000
- USER_GID=1000
- GITEA__database__DB_TYPE=sqlite3
- GITEA__server__DOMAIN=git.example.com
- GITEA__server__SSH_DOMAIN=git.example.com
- GITEA__server__ROOT_URL=https://git.example.com/
volumes:
- ./gitea:/data
- /etc/timezone:/etc/timezone:ro
- /etc/localtime:/etc/localtime:ro
ports:
- "3000:3000"
- "2222:22"
restart: unless-stopped
几个值得注意的点:
USER_UID和USER_GID需要设置成宿主机上有权限操作挂载目录的用户 ID,否则容器内创建的文件属主不对,后面会带来权限问题。- SSH 端口做了映射,从容器内的 22 映射到宿主机的 2222,这样宿主机上的 22 端口不会冲突。用户拉代码时用的地址是
ssh://git@git.example.com:2222/owner/repo.git。 GITEA__server__ROOT_URL必须配置成外部访问地址,否则生成的克隆链接和 OAuth 回调地址都是错的,这个坑我见过很多人在论坛里问。
启动命令很简单:
bash复制docker compose up -d
首次启动后,浏览器打开 http://服务器IP:3000 进行初始化,设置管理员账号。Gitea 的初始化页面很直接,就几个字段:站点名称、仓库根路径、数据库类型。我用 SQLite 不需要额外配置,直接下一步就行。
3.2 数据迁移:从 GitLab 把仓库完整搬过来
迁移操作没有想象中那么复杂。Gitea 支持通过 URL 方式直接从 GitLab 导入仓库,包括 issues、PR、里程碑等元数据。我实测的流程是这样的:
- 在 Gitea 右上角点“+”号,选择“迁移外部仓库”。
- 迁移源选择 GitLab。
- 填上 GitLab 的 API 地址、私人访问令牌(Access Token)和仓库 URL。
- 选择是否镜像仓库,还是只做一次性迁移。
- 点击迁移,等待完成。
我迁移了最大的一个仓库(有 8000 多次提交),几分钟就完成了,代码、标签、合并请求记录都迁移过来了。不过有个小坑:迁移过来后的 Webhook 是不会自动迁移的,需要在 Gitea 里重新配置一遍。还有如果原来的 GitLab 仓库启用了 LFS 大文件存储,需要注意确认 Gitea 也开启了 LFS 支持,否则拉取时文件会缺失。
如果不想用 UI 操作,也能用脚本批量迁移。Gitea 提供了一套完整的 REST API,可以通过 /api/v1/repos/migrate 接口实现迁移。我写了一个简单的 Python 脚本批量处理了剩下的仓库,这里贴一段关键逻辑:
python复制import requests
GITEA_URL = "https://git.example.com/api/v1"
GITEA_TOKEN = "your_token_here"
headers = {"Authorization": f"token {GITEA_TOKEN}"}
payload = {
"clone_addr": "https://gitlab.example.com/owner/repo.git",
"repo_name": "repo-name",
"repo_owner": "gitea-owner",
"service": "gitlab",
"auth_token": "gitlab_token_here",
"mirror": False,
"private": True,
}
r = requests.post(f"{GITEA_URL}/repos/migrate", json=payload, headers=headers)
if r.status_code == 201:
print("迁移成功:", r.json()["clone_url"])
else:
print("迁移失败:", r.text)
跑批之前建议先迁一两个小仓库验证流程,确认 Gitea 的仓库权限、默认分支这些设置符合预期,再全量执行。我们当时就是先人工迁了两个项目,团队确认没大问题后,才把所有仓库都迁过去,整个过程半天搞定。
3.3 迁移过程中的常见问题:上传大小、初始密码、登录失败
实际操作中我们踩了不少跟 GitLab 相关的老坑,这些在搜索引擎里也被问烂了,但确实都是高频问题,值得展开说说。
第一个是上传文件大小限制。GitLab 默认限制上传文件为 10MB,超过这个大小的二进制文件会被拒。Gitea 的默认限制是 3MB,反而更严格。对于存放安装包、设计稿、数据集的仓库,这个限制必须改。Gitea 有两种改法:一种是在配置文件 app.ini 里改:
ini复制[repository]
MAX_UPLOAD_SIZE = 1024
这里的单位是 MB,MAX_UPLOAD_SIZE = 1024 表示 1GB 上限。改完需要重启容器。
另一种是直接改数据库里的 system_setting 表,不过用配置文件更直观,团队后续维护也方便。顺带提一下,GitLab 修改上传大小限制的话,要同时调整 Nginx 配置里的 client_max_body_size,很多人只改 GitLab 配置不改 Nginx,结果一样传不了大文件。Gitea 没这个额外步骤,改完 MAX_UPLOAD_SIZE 重启就生效了。
第二个是初始密码问题。Gitea 安装完成后,通过 Web 页面注册的第一个用户会成为管理员。如果你是用环境变量 GITEA__admin__USERNAME、GITEA__admin__PASSWORD 预置的管理员账号,那不用管初始密码;但如果你是用 Docker 首次启动时通过 UI 初始化的,密码是自己设的,自然不会存在“初始密码”问题。倒是从 GitLab 迁移过来的用户,他们的密码在 Gitea 里不能直接复用,需要重新走一遍邮箱验证或管理员重置密码流程。这里有一个注意点:团队内要提前通知大家新平台的登录方式,不然陆续有人来问“密码不对怎么办”。
第三个是登录失败提示。有同事迁移后遇到一个报错:login failed. check api token or gitlab version. log in via git if the version is lower than 14.0。这个错误其实不是 Gitea 的问题,而是我们在配置 Gitea 的 GitLab 集成时,用的 API Token 权限不足,或者 GitLab 版本低于 R14 导致 API v4 路径变化。解决方向很简单:确认 GitLab API 地址是否包含 /api/v4,再确认 Token 有 api 权限、没有过期。我们在迁移到 Gitea 之前,需要先到旧 GitLab 后台的 Access Token 页面生成一个新 Token,确保它有 api、read_repository、read_user 权限,再在 Gitea 的迁移配置里填这个 Token,问题就解决了。
4. 持续集成改造:Gitea 的 CI/CD 与 Webhooks 配置
4.1 Gitea Actions 还是 Webhooks?
迁移完代码仓库之后,下一步就是 CI/CD 链路。原本我们用 GitLab CI 跑的流水线包括:代码检查、单元测试、打包、部署到测试服务器,大约十几个 pipeline。换到 Gitea 后,CI 方案需要重新考虑。
Gitea 自身有一个内置的 CI/CD 系统叫 Gitea Actions,语法跟 GitHub Actions 基本一致,通过 .gitea/workflows 目录下的 YAML 文件定义流水线。我们试用了一段时间,发现跑常规测试和构建没问题,但部署阶段需要跟内部的发布系统(基于 Jenkins 脚本改的)对接,Gitea Actions 的插件生态还没那么丰富,部分自定义动作写起来比较费劲。
所以我们的最终方案是混合模式:简单项目的 CI 用 Gitea Actions 跑,复杂项目的发布流程走 Webhook 触发 Jenkins。这样既不影响开发体验,又不需要把老系统彻底重写。下面重点说说 Webhook 的配置过程,因为这是很多团队迁移后第一个踩坑的地方。
4.2 Webhooks 配置实战:推送到自己的发布平台
Gitea 的 Webhook 配置在仓库设置里,路径是 Settings -> Webhooks -> Add Webhook。支持的事件类型很多:push、pull request、issues、release、分支创建/删除等等。我们的发布系统需要监听 push 事件,所以选 push,然后填上 Jenkins 的触发地址。
以 Jenkins 为例,我们在 Jenkins 里装了一个 Generic Webhook Trigger 插件,然后在 pipeline 里配置参数:
groovy复制pipeline {
agent any
triggers {
GenericTrigger(
genericVariables: [
[key: 'PUSH_BRANCH', value: '$.ref']
],
token: 'my-secret-token'
)
}
stages {
stage('Build') {
steps {
echo "Branch: ${PUSH_BRANCH}"
sh 'make build'
}
}
}
}
Gitea 侧填的 Webhook URL 格式是:
text复制http://jenkins.example.com/generic-webhook-trigger/invoke?token=my-secret-token
保存后建议先点一下“测试交付”,Gitea 会发送一个测试事件,查看 Jenkins 是否收到。这里有个坑:Gitea 默认的 Webhook 请求 Content-Type 是 application/json,但某些老版本的 Generic Webhook Trigger 插件对 JSON 解析不完整,导致变量值取不到。解决办法是改成 application/x-www-form-urlencoded,或者在 Gitea 的 Webhook 配置里选“发送 JSON payload”并确认格式。
另外,配置 Webhook 时建议开启“允许自签名证书”(如果你的 Jenkins 是 HTTPS 且证书是自签的),否则 Gitea 连不上 Jenkins,而且不会给你很明显的报错,只会显示交付失败。
4.3 分支保护与权限管理:别把权限搞成摆设
Gitea 的分支保护规则在仓库设置的 Branches 页面配置,用法跟 GitLab 的 Protected Branches 差不多。我们配置了以下规则:
- 受保护分支:
main、release/*。 - 允许推送:仅限仓库管理员。
- 允许合并:需要有 1 个审查通过。
- 禁止强制推送。
配置完之后,开发者直接 push 到 main 分支会被拒绝,必须走 PR 流程。刚开始团队会有点不适应,因为 GitLab 的推送规则如果有问题,报错信息是英文的,Gitea 的报错信息更直观,但也是在 push 的时候才会报。建议在团队里发一个简短说明,让每个人都清楚“以后主分支不能直接推了,要走 merge request”。
权限管理方面,Gitea 设置了 Owner、Write、Read 三个层级,组织(Organization)级别的权限可以单独管理每个仓库。我们把团队按项目分成多个 Organization,每个项目一组,成员权限默认 Read,核心开发者给 Write。这一步看起来简单,但建议在迁移前后就规划好,不然后面逐个仓库调权限会很痛苦。
Gitea 还支持关闭 Guest 访问,就是让未登录用户能看到部分公开仓库内容。企业内网部署的话,我建议直接把全局注册关闭,用邀请注册或管理员创建账号,避免匿名用户注册后获得一个默认的 Read 权限。我们在配置里关闭了“允许用户自行注册”,新同事入职需要管理员帮忙创建账号,虽然多了一步操作,但安全性提升明显。
5. 落地后的性能实测与维护心得
5.1 性能对比实测:从 10GB 到 600MB 的真实场景
迁移完成之后,我在同一台服务器上做了连续两周的观察记录。硬件配置是 4 核 8GB 的云主机,部署了 Gitea 容器 + SQLite 数据库。两周的 docker stats 记录显示内存占用在 450MB ~ 700MB 之间波动,稳定期基本维持在 500MB 上下。CPU 使用率日常低于 5%,只有很多人同时 push 或跑 CI 时偶尔跳到 30%~40%。
对比之前跑 GitLab 的 16GB 内存机器每天内存报警的状态,Gitea 确实把我们的基础设施开销降了一个数量级。而且我们顺便把原来跑 GitLab 的那台 16GB 服务器退掉了,换成了 4GB 的小机器跑 Gitea 和若干内部服务,每月账单省了一块不小的支出。
为了确保 Gitea 不是“跑得慢但省资源”,我还做了功能响应测试。300 个仓库列表页、搜索代码、创建 issue、合并 PR,这些常用操作基本是毫秒级响应。对比 GitLab 在低配机器上页面加载都要转圈好几秒的体验,Gitea 的性能表现是实打实的提升。
5.2 维护经验:备份、升级与监控
Gitea 的备份非常容易。因为我们用的是 SQLite 数据库,备份实际就是两件事:拷贝数据库文件、拷贝仓库文件目录。我用一个简单的 shell 脚本做每日备份,生产环境直接扔到 crontab:
bash复制#!/bin/bash
# 每日凌晨 2 点执行完整备份
BACKUP_DIR="/backup/gitea/$(date +%F)"
mkdir -p "$BACKUP_DIR"
# 停止容器避免写入不一致,也可以用 gitea dump 命令
docker exec gitea gitea dump -c /data/gitea/conf/app.ini --file /tmp/gitea-dump.zip
docker cp gitea:/tmp/gitea-dump.zip "$BACKUP_DIR/gitea-dump.zip"
# 保留最近 7 天的备份
find /backup/gitea/ -mindepth 1 -maxdepth 1 -type d -mtime +7 -exec rm -rf {} \;
gitea dump 命令会把配置、数据库和仓库文件打成一个 zip,恢复的时候直接解压到新环境,再启动容器即可。注意 dump 命令会临时锁定数据库,所以最好在低峰期执行。
升级方面,Docker 方式升级只需要拉新镜像、重建容器:
bash复制docker pull gitea/gitea:latest
docker compose up -d
Gitea 的迁移脚本会自动处理数据库升级。我们经历了两个大版本升级,都没有出过问题。如果你们用的是二进制直接部署的方式,升级就更简单了:下载新版本二进制,替换掉旧的,重启进程。
监控方面,Gitea 本身提供了 /api/v1/health 健康检查接口。我们用 Uptime Kuma 每 5 分钟检测一次,如果返回 200 就正常,失败就告警到企业微信。这个配置不复杂,但能第一时间知道服务是否挂了,比让同事发现“拉不了代码”再去排查要高效得多。
5.3 团队实际用得上的若干细节
这里把迁移后团队最常用的一些操作细节整理一下,都是我们实际踩过或者被问过的。
删除分支。Gitea 在网页仓库主页面和分支页面都有删除按钮。如果本地有缓存分支,需要 git fetch --prune 清理。有一点跟 GitLab 相似:Gitea 删除分支后,相关 PR 的关联信息会保留但标记为已合并或已关闭,不影响仓库历史审计。
拉取代码到本地。Gitea 支持 SSH 和 HTTPS 两种方式。SSH 地址在我们部署环境里是 ssh://git@git.example.com:2222/owner/repo.git,端口号不能漏。HTTPS 方式适合内网没有外网端口的场景。我建议团队统一用 SSH,把公钥配置到 Gitea 账户的 SSH Keys 里,省得每次输密码。
所有 GitLab 项目设置成 internal/private。老 GitLab 默认新建项目可能是 public 的,迁移到 Gitea 后,老项目会保留原来的可见性设置。如果你们也想把所有仓库默认设为私有,可以在 Gitea 管理后台的 仓库 -> 默认可见性 里改成“私有”。
关闭 guest 访问。前面提到过,在企业内网环境建议在管理后台的 服务 -> 允许用户自行注册 处关闭注册。我们还顺手把“未登录用户是否可浏览仓库”的选项关了,彻底避免匿名访问。
检查 API Token 或 GitLab 版本的报错。如果迁移过程报 login failed. check api token or gitlab version. log in via git if the version is lower than 14.0,基本都是 Token 权限不足或 API 版本不匹配。建议在源 GitLab 上生成一个有 api、read_repository、read_user 权限的 Token,并且确认 API 地址带上了 /api/v4。如果是老版本 GitLab(低于 14.0),可能需要用 /api/v3,Gitea 侧也支持通过 service 参数指定版本,但最好还是先把 GitLab 侧升级到能兼容 v4 的版本,否则部分迁移接口不可用。
6. 写在最后的几个建议
整理一下这次迁移的经验,如果你们也在考虑从 GitLab 切到轻量级方案,我个人的体会是这样的。
先做小范围验证再全量迁移。别一上来就动生产,找个闲置机器跑 Gitea,把最常用的两三个仓库迁过去,让主力开发用上一周,看有没有功能落差、痛不痛苦,然后再决定要不要全面推广。
提前想好 Webhook 和 CI 方案。CI/CD 是迁移中最容易卡住的环节,因为旧流水线里绑定了很多 GitLab 特有变量和 API 调用。如果你还依赖 Jenkins、飞书/钉钉通知这些外部系统,尽量提前列一个 Webhook 映射表,Gitea 的 payload 字段基本对齐 GitHub,但和 GitLab 有些细节不一样,比如 object_kind 的取值有差异,收到 webhook 的服务端要做好兼容。
关注权限和审计需求。企业内网对代码安全要求高的场景,记得关闭公开浏览和自助注册,所有仓库默认私有,成员按项目分组管理。Gitea 的权限系统足够用,但要主动配置,默认设置的宽松程度可能超过你的预期。
资源省下来的这部分,我后来给团队添置了一台内网文件服务器,专门放 CI 产物和数据库备份。同样的预算,原来只够养 GitLab 一个应用,现在能养一套完整的基础设施——这大概就是轻量级方案最直观的价值。如果你们团队也是几十人规模、核心诉求是代码托管和常规 CI,那么一个低配小机器 + Gitea 的组合,大概率比搭一套 GitLab 全家桶更合适。当然,如果你的组织确实需要复杂的代码审计、大规模权限矩阵、高级 CI 编排和官方商业支持,GitLab 依然有它不可替代的位置,只是它不再是我们这种小团队的最优解了。
