没人一开始就想跟GitLab闹分手。我们团队大概十来人,前后端加运维都挤在同一个代码平台里,过去两年一直用GitLab社区版,功能确实全:代码托管、MR审批、CI流水线、内置镜像仓库、Wiki、Issue、看板,一个后台啥都齐了。但随着仓库数量变大、成员变多,我们那台16GB内存的服务器越来越吃力——Docker里跑gitlab-ce,光是内存表就轻轻松松吃掉7GB以上,磁盘空间隔三差五告急,升级一次要等十几分钟,然后又开始新一轮提心吊胆。直到有一天,它连登录都开始抽风,我们终于决定回头看看自己真正需要的东西。
后来我们放弃了GitLab,把整套代码托管切到了一个只用单二进制文件就能跑的轻量级方案上。迁移完我把服务器跑了一遍du和top,GitLab时代连镜像带仓库加日志动不动冲到10GB规模的数据,现在整套系统加一起也就600MB左右。这个数字对比不是什么玄学,是我们在同一台服务器上真实观察到的变化。如果你也在维护一个小型团队或者个人自建的Git服务,正在被GitLab的体量折腾得难受,这篇文章应该能帮你厘清思路:到底值不值得迁,迁移要准备什么,以及最容易被忽略的坑在哪里。
1. 起因:GitLab这个“全家桶”,我们终于推不动了
1.1 我们真实的需求:只是一个好用的代码托管
做技术选型有一个很朴素的原则:能解决当前核心问题的方案,永远比能做一百件事但你把九十九件都用不上的方案更靠谱。回过头看,我们团队对代码平台的实际需求其实并不复杂,核心就这几条:
- Git仓库能正常推拉,HTTP和SSH两种方式都要稳定。
- 分支保护和MR代码评审流程,不能让大家直接在master上乱推。
- 项目权限能跟成员组对齐,新人入职拉仓库,离职一键回收权限。
- 有一个能用的Web页面,同事之间互相看看代码讨论问题。
- 最好带一点自动化能力,比如提MR之后触发一份静态检查或自动部署到测试环境。
GitLab社区版当然能满足这些需求,而且很多时候比实际需要还要超出很多。问题就出在“超出”这两个字上。它那套完整体系一旦部署起来,后台会拉起几十上百个进程,数据库、Redis、Sidekiq、Gitaly、Prometheus、Alertmanager、Jaeger、Nginx、Registry、Workhorse……光是列这些组件名字就够让不懂底层的人头皮发麻。对一个小团队来说,我们真正想维护的其实只是一个稳定的Git服务,而不是一套微服务化架构的私有云平台。
这就像你家里本来只需要一辆代步车,结果为了偶尔拉一趟家具,去买了台带升降尾板的重型货车。平时你照样能上下班,但油费、停车、维护、保险全都跟着上去了。等你某天想给它换个零件,还得先翻一个比字典还厚的维修手册。
1.2 压垮骆驼的最后一根稻草
GitLab的高资源占用一开始并没有惹毛我们,毕竟功能全嘛,多花点内存也算等价交换。但后来出现了几件事,让团队里对“用GitLab”的耐心快速耗尽。
第一件是频繁的“假死”和“满身大汗地重启”。我们服务器上还跑着Jenkins和几个测试环境,GitLab经常把内存吃满,系统触发OOM后,整个服务直接响应不过来。最烦人的是,GitLab的容器虽然设置了restart: always,但容器重启往往要卡好几轮,很多依赖组件没有按顺序拉起来,页面就需要各种“等等再试”。每次出现这种情况,运维同事就要爬上服务器敲docker logs、docker restart,开一堆帖子一个个对照排查。这不是技术活,这是纯粹的体力活。
第二件事是版本升级带来的连锁反应。GitLab社区版每个月都会发新版本,里面夹杂着不少高危漏洞修复。如果有安全公告说“建议升级”,那你就得硬着头皮迁。但GitLab的升级不是随时都能跨多个大版本,官方要求必须一级一级升,中间不能跳版本。我们曾经从14.x往16.x迁,光升级过程就在服务器上耗掉一个下午,其中还遇到数据库迁移卡死、后台任务堆积。后来想想,我们只是一个不到二十人规模的团队,每次升级却搞得像公司核心系统变更一样隆重。
第三件事是某次线上事故。有个同事清理旧仓库时误删了一个大分组,我们的备份策略虽然每天有备份文件,可要恢复到新容器时才发现,备份出来的文件已经几十GB了,恢复过程还反复报权限错误。当时GitLab界面上那个422错误和各种token校验失败的提示,让整个team的耐心彻底归零。
当我们把“项目代码放在哪个平台”这个问题拿到周会上讨论时,一位同事问了一句:“我们需要的功能,真的需要一个需要4GB内存才能启动的大家伙来提供吗?”就是这个问题,开启了我们寻找轻量级替代方案的过程。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 为什么会选轻量级:不是功能越少越好,而是环境要配得上体量
2.1 货比三家之后,我们锁定了单文件二进制方案
在决定放弃GitLab之后,我们看了几类替代品。一类是继续沿着完整平台路线走的方案,比如Gitea、Forgejo、Gogs;另一类是干脆连界面都不要,直接裸Git仓库加一些脚本做权限管理。裸Git方案太野生了,团队成员互相看代码只能用命令行或者自己找工具,不符合小团队追求“轻但不糙”的定位。最后进入候选名单的,基本集中在Go语言实现的几个开源Git服务上。
其中我们测试了Gogs和Gitea,两者血缘很近,Gitea是从Gogs分叉出来的社区化项目,更新节奏更快,插件和配置项更丰富。Gitea最大的特点是可以编译成单个二进制文件,官方也提供了非常小巧的Docker镜像。它不需要独立的Java运行时,不需要Ruby,不需要一套复杂的依赖链,默认情况下用SQLite作为数据库就能跑起来,也可以切换到MySQL或PostgreSQL。虽然它不像GitLab那样自带一整套CI/CD全家桶,但它官方支持与Drone、Jenkins等外置CI工具联动,后来也原生支持了Gitea Actions,基本能满足我们“代码托管为主、流水线交给旁边CI跑”的诉求。
我们最终的对比结果大致是这样:
| 对比项 | GitLab CE(社区版) | Gitea / Forgejo 类轻量方案 |
|---|---|---|
| 安装容器镜像 | gitlab-ce镜像通常1.5GB以上 | Gitea的官方镜像通常在100MB以内 |
| 落地后磁盘占用 | 典型环境8GB~15GB | 几十个仓库全加起来,几百MB到1GB出头 |
| 内存占用 | 闲时3GB~5GB,高负载更高 | 同样的使用场景基本在300MB~600MB区间 |
| 起服务方式 | 一堆子组件互相依赖,需按序启动 | 单进程,一个二进制或一个Docker容器 |
| 日常升级 | 需逐级升版本,耗时且容易翻车 | 直接换新版本二进制/镜像,简单粗暴 |
| CI/流水线 | 自带的强大CI,但配置繁琐 | 支持外接CI,也适用轻量Actions方案 |
| 项目/权限管理 | 功能非常全,管理模型复杂 | 基础权限模型够用,理解成本低 |
这个表不是想说Gitea每一项都比GitLab好,它只是想说明一件事:对一个不需要复杂平台能力的小团队来说,Gitea这套方案把“运维负担”降到了极低的水平,同时又把“Git该有的能力”保留得很完整。
2.2 为什么“单进程”比“全家桶”更让运维安心
你可能会问,GitLab不也是一个Docker容器跑着吗?怎么就不算单进程了。实际上虽然你在宿主机上看到的只是一个gitlab容器,但它内部是通过supervisor管理一堆服务的,等于一个集装箱里装了几十个人,每个人都有自己的工作,随便一个服务出问题都会拖累全局。这就导致容器看起来“打了包”,实际上引入的资源消耗和故障点一个都没少。
而Gitea这类工具为什么能轻盈得这么夸张?核心原因是它本身由Go语言编写,Go程序可以编译出静态链接的单一可执行文件,运行时不需要特意安装额外的依赖库,也不依赖JVM这一类重量级运行时。再加上默认选择SQLite存储元数据,意味着整个应用就是“一个二进制 + 一个数据库文件 + 存放Git仓库的文件夹”,没有消息队列、没有后台异步任务调度器、没有一堆定时进程。
我在测试环境做了一次完整的数据迁移验证,从旧GitLab把所有仓库推到新的Gitea,然后观察它的磁盘和内存变化:服务启动后内存占用大约在200MB上下,随着有人并发访问、执行仓库操作会上浮到400MB左右。对比之前GitLab动不动就逼近8GB内存的情况,这个数字确实让我长舒了一口气。也就是从这个时候起,我确定这个方案可以让团队从“半夜起来救GitLab”的状态里彻底解脱。
3. 迁移实操:仓库能推过去不算完,细节才是魔鬼
3.1 迁移前先做好三件准备,别急着删旧服务
迁移开始前,我们先把任务拆成了三块:仓库数据、成员权限、历史流水线产物。仓库数据是灵魂,不能让同事的提交历史丢一个commit。成员权限也要尽量对照迁移,不然大家各建各的账号,权限关系会变成一团乱麻。至于流水线产物,我们斟酌之后决定不迁,因为Jenkins的构建记录在CI侧还有保留,以后只要把仓库地址改掉就能继续用。
第一个准备是盘点仓库清单。进GitLab后台把所有项目列表导出来,按活跃度分一下类,哪些项目必须完整迁移,哪些已经归档的死仓库可以直接从GitLab导出模块打包带走,哪些还挂在某个人名下需要先在组层面理清归属。这一步看似琐碎,却是后面所有工作的基础。我们当时整理出几十个项目,其中一半是超过一年的老仓库,最后决定全部迁移过去,免得遗留在老系统里变成信息孤岛。
第二个准备是检查Git LFS。如果团队的工作流里用了大文件存储,迁移的时候必须单独把LFS对象也导走。GitLab本身能管理LFS,Gitea也支持LFS,但两边之间的官方导入工具未必能自动把LFS对象带全,这块最容易出现“仓库推过去了、大文件拉不回来”的隐患。
第三个准备其实是沟通上的:提前通知团队成员,安排在周末某段时间内“冻结”主分支。不要指望着可以在用户完全在线使用的时候做无缝迁移,Git是分布式的,就算你迁移期间有人push了代码,你会发现旧服务器有新提交,新服务器上也有一份接近完整的镜像,两边都有数据缺口,合并起来会浪费大量时间。最简单可行的做法是低峰期锁定仓库,迁移完让所有人先fetch新地址,确认无误后再把旧服务停掉。
3.2 最省事的路径:用迁移接口导入,但不要迷信你同时将权限带全
Gitea自带一个“从其他Git服务器迁移”的功能,在站点管理的“数据迁移”入口,填入GitLab地址、用户名和访问令牌,就能把仓库、Issue、MR/PR、Wiki、标签这类内容一起拉过来。这个功能对于个人项目或者十几个仓库规模来说非常香,我们第一波就是拿它迁移了存放比较久、结构简单的项目,效果很理想,Commit历史全都在,分支也完整。
如果你用的是Docker方式部署Gitea,迁移前要记得确认容器里有没有装好Git和必要工具,否则某些操作会报“git command not found”之类的错。这个坑比较隐蔽,因为Gitea官方镜像里通常已经带了基础Git,但如果你自己折腾过精简镜像,就要手动安装。
不过要提醒的是,手工迁移/导入自动同步权限关系并不是很完善。GitLab里那种复杂的组嵌套、成员角色(Owner/Maintainer/Developer/Reporter)映射到Gitea之后,不一定能精确落到“对应的项目权限”上。我们后来的做法是先把团队账号整理成一张CSV表,给每位成员统一创建好用户,然后在Gitea里按组把成员关系重新加一遍。Gitea的权限模型直观很多:一个用户可以属于多个组织,组织下可以建team,再把team关联到仓库组。权限层级没GitLab那么深,反而省心。
3.3 手动推仓库的补救方法
如果某段Git历史自动迁移没成功,或者你打算把Gitea迁移到新服务器,最靠谱的兜底方案是手动建立镜像地址然后push:
bash复制# 在原来GitLab仓库目录里添加一个远端,名为临时迁移远端
git remote add new-gitea http://your-gitea-server/group/old-repo.git
# 把全部分支和标签推到新平台
git push new-gitea --all
git push new-gitea --tags
如果仓库很大而且分支特别多,想全量镜像可以用这条命令:
bash复制git clone --mirror http://your-gitlab/group/old-repo.git old-repo.git
cd old-repo.git
git push --mirror http://your-gitea-server/group/old-repo.git
--mirror会把GitLab端的refs配置完整复制,相当于把远端所有分支、Tag、健壮引用一股脑搬到目标地址,适合用来做完整仓库迁移。要注意的是,这只能搬运Git对象本身,仓库里的MR评论、Issue、审批记录不会跟着走。所以能走Gitea自带迁移界面的还是优先走迁移界面,只有迁移工具处理不了的老仓库才用这种方式。
3.4 端口冲突、SSH配置和Web钩子重定向
我们线下部署环境里,原有的GitLab一直把SSH端口占在2222,因为宿主机22端口留给了系统sshd。迁移之后,Gitea也要对外暴露SSH端口,这就必须做一次端口规划,不能跟系统sshd抢。我们用Docker Compose部署时做了这样的端口映射:宿主机2225对应容器内的22端口,因为团队已经习惯在clone地址里写明端口,改动影响不大。
配置完SSH之后还要把成员的SSH公钥在新平台重新添加一遍。这一步网上教程很多,但很少有人提醒:SSH公钥能不能配成功,关键取决于你最终测试的是 git@host:group/repo.git 还是 git@host:2225:group/repo.git,这两个地址在SSH配置里对应的是不同Host规则。最方便的做法是在每台开发机的~/.ssh/config里为服务器单独配一个别名,比如:
sshconfig复制Host gitea
HostName git.example.com
Port 2225
User git
IdentityFile ~/.ssh/id_ed25519
配置之后,让同事用git clone gitea:group/repo.git来拉代码,所有人在本地不用再记得写端口,体验跟原来差不多。
除此之外,原本配置在GitLab上的Web钩子也要一并处理。我们的自动部署系统是通过GitLab Webhook触发Jenkins构建,GitLab侧Webhook秘钥和Gitea侧采用的HTTP签名方案不一样,必须在新的Web端重新生成一个Secret,并同步到Jenkins那边当作认证令牌。如果忘了这一步,你会看到代码明明推上去了,流水线却不跑,排查半天才发现钩子地址还指向旧服务器。
3.5 CI/CD方案调整:不硬抄GitLab CI,用Actions或外接Runner
GitLab社区版自带GitLab CI,只要项目里放了.gitlab-ci.yml,就会有Runner跑起来。切换之后,这块需要做一个取舍。我们并没有强求在Gitea里100%复刻GitLab CI的流水线,因为那会再一次陷入“功能很多但配置很重”的怪圈。最终团队选择了外接CI——继续用Jenkins,只是在Gitea的Webhook里勾选了“Push”和“Pull Request”事件,让Jenkins收到请求后再拉代码执行构建与部署。
如果你的团队更倾向于Gitea原生自动化,可以试试它内置的Actions功能。Gitea Actions和GitHub Actions的工作流文件语法非常像,使用.gitea/workflows或.github/workflows目录定义流水线,配合act_runner这个Runner组件,能实现很轻量的构建、测试、发布流程。我们没有在首个版本把所有项目都迁到Actions,只是挑了一个内部小工具试了一下,整体体验对小型项目来说是够用的,而且它比GitLab CI好维护得多。
4. 常见问题与排查技巧:这些坑我们替你先踩了
4.1 登录报错、token失效和“隐身模式就能登录”的迷思
迁移过程里最常遇到的问题集中在登录认证上。有同事反馈说用密码登录Gitea报错,或者API访问始终提示token验证失败。这里要说一个排查顺序:先看浏览器和时间。
时间不同步是Git服务认证很常见的隐形杀手。服务器NTP没同步好,客户端机器时间和服务器差了几分钟,HTTPS和token签发就可能判定为过期,表现成“登录失败”“API token失效”。我们当时就遇到过,宿主机时间飘了将近一分钟,所有基于token的请求时好时坏。先执行date对比一下服务器和本地时间,顺手配置好chrony或ntp,能挡掉一批看起来很玄学的认证问题。
还有更多人问“为什么隐身模式能登录,普通模式就报422错误”。这种感受通常是因为正常浏览器窗口里保留了旧域名的cookies和session。你在登录界面之前先试一下隐身窗口,并不是隐身模式有什么神奇技巧,而是它默认不加载旧cookies。解决办法就是清掉该站点的cookies,或者检查一下是否开启了会影响第三方cookie的安全策略。Gitea的session处理比GitLab清爽不少,但只要是Web应用,遇到cookie缓存问题的方式都差不多。
如果要定期拉取代码或调用API,建议给Gitea账号创建一个专属Access Token,不要用明文密码。创建位置在“设置 → 应用”,一个token只授权需要的权限范围,最少够用就好。这样就算某台机器上的token泄露,也不至于把账号完全拱手让人。
4.2 升级和备份:轻量不代表可以裸奔
Gitea升级确实比GitLab省心无数倍,但后面有一个细节值得反复强调:升级前一定要把配置文件和数据库备份好。官方对配置、数据库和仓库目录的整体备份只需要拷贝对应文件夹即可,但如果你用的是Docker部署,还要注意容器内数据卷的挂载路径,别只备份了gitea.db却忘了repositories目录,那样恢复出来的平台里只剩用户账号,仓库全是空的。
我们升级过好几次,正式操作就四步:先停容器,再备份整个数据卷,然后拉新的镜像版本,最后启动新容器跑一遍自检。整个过程通常几分钟就结束,不像GitLab升级要在后台跑一堆数据库迁移脚本。但有一次我们跳过停容器直接覆盖了数据卷,结果Gitea启动时提示数据库版本不兼容,只能从备份里恢复出来重新升。从那以后我养成了一个习惯:升任何版本前先看一眼官方Release Notes,确认存储结构是否有变化,尤其是小版本跨度比较大的情况。
4.3 Git LFS、大仓库和Push超大文件失败的排查
代码仓库里的LFS文件在迁移接口里容易出问题,常见症状是仓库迁移后直接clone很顺利,但执行git lfs pull时一直报找不到对象。这种情况通常是因为Gitea端还没自动识别该仓库启用了LFS,需要到仓库设置的LFS页面确认状态,确认服务端LFS存储文件确实存在。最保险的迁移LFS方法是:在旧平台先执行一次git lfs fetch --all把LFS对象下载到本地缓存,然后git lfs push --all new-gitea把对象推到新平台。这个操作会把本地缓存里的全部大文件对象推上去,能避免HTTP迁移时漏文件的问题。
另外,Gitea虽然足够轻量,但对特别大的仓库,网络传输超时也会让人头疼。我们有一些仓库老分支里藏着几百MB的构建包,第一次clone时客户端会等很久。这个问题不完全是Gitea的锅,可以通过服务端启用git config http.receivepack相关的超时参数或者调整Nginx的proxy_read_timeout来缓解。如果项目里确实有大文件流,建议单独开一个LFS仓库来管理,不要把几个GB的大依赖直接塞进常规Git仓库。
4.4 资源占用如何做长期观测
切到Gitea之后,服务器的CPU和内存压力一下子小了很多,但我们又遇到另一个极端:因为太轻松了,差点忽略对它做监控。以前GitLab占资源多的时候,我们每天都会监控内存和磁盘,切到轻量平台后反而不怎么看了,结果某个测试仓库里的CI缓存和Docker日志把磁盘慢慢吃满了。
建议无论如何都加上基础的系统级别监控,至少要在宿主机上用docker stats和df -h留一个定时任务,或者接上Prometheus和Grafana。别因为服务轻就忘了它是一个需要照顾的系统。Gitea也提供了基于文件或数据库的会话状态管理,可以配合面板去看在线用户数,但对小团队来说,最简单的磁盘与内存告警已经足够。
5. 这次迁移后,我的体会是“工具匹配体量”比“工具够强”更重要
从GitLab迁到轻量级方案之后,最大的变化不是服务器数字变好看了,而是运维心态彻底变了。以前每次打开GitLab后台,我都隐隐有一种在开一台重型设备的压迫感;现在我可以很随意地在服务器上重启服务、看配置、翻日志,因为整个系统的复杂度已经降到我可以全脑子装下。GitLab很强,这点毫无疑问,否则不会成为那么多大中厂选择的企业级平台。但它的强是用来匹配复杂组织结构和海量用户场景的,对一个十来个人、跑在普通机房机器上的团队来说,这种强已经变成了一种额外的负担。
如果你也在纠结要不要从GitLab切到Gitea这类轻量替代品,我给的建议是先认真盘点团队规模、仓库数量、功能使用频率、管理能力和升级容忍度。如果是5到100人的团队,日常用途主要是代码托管、MR评审、权限管理,自己不希望每个月为了安全更新都折腾一次大升级,那轻量平台的体验会好很多。相反,如果你们重度依赖GitLab的仪表盘、细粒度审计日志、层级复杂的大型企业权限模型、内置的安全扫描能力,那还是老老实实继续用GitLab,不要为了省机房的钱把平台能力也省了。
对我个人而言,这次迁移最大的收获仍然是那个数字上的冲击:一个能服务几十个仓库和十多个开发者的Git平台,原来真的可以把整套运行体积压缩到几百MB。它不是靠阉割核心体验换来的,而是靠“不去做那些我们根本不需要的功能”换来的。后来我甚至开始带着这种标准审视团队里的其他自建服务,凡是能精简的都逐渐精简了,凡是负担大于价值的都开始重新评估。技术工具没有绝对的最好,只有合适的边界,找到那个边界,运维的快乐就回来了一大半。
最后分享一个小技巧:无论你最终选择迁移到哪个平台,先在测试环境完整跑一遍真实项目迁移,最好连域名、HTTPS证书、SSH端口都模拟成正式环境。把旧系统从停掉到新系统上线需要的时间准确记录下来,再乘上1.5倍作为正式窗口的预留时间。我们当时就是因为测试环境跑得太顺,正式迁移时反而被一堆杂事拖慢了流程,差一点没按计划完成切换。如果你能提前想到邮件通知、同事电脑里的git remote地址要改、CI钩子里的密钥要对齐这些细节,正式上线的时候就只剩惊喜了。
