如果你问我在团队内部搭建版本控制系统这件事上踩过多少坑,我估计能聊一下午。前年我接手团队工具链维护,四十多号人的代码散落在云端私有仓库、移动硬盘和服务器备份包里,每次发版都要靠人肉对版本号。反复对比了GitLab、Gitea和一众轻量方案之后,我才把所有仓库统一迁到了自建的Gitea上,从此再没在"最新代码在哪"这种问题上吵过架。这篇就把我的选型思路、Linux和Docker两套部署流程、仓库级Token权限的配置方法,以及这一年多攒下来的避坑经验完整写出来,适合刚准备自建版本控制系统的小团队运维,也适合想从GitLab降级到轻量方案的开发者参考。
1. 为什么自建版本控制,我绕了一圈还是回到Gitea
1.1 Gitea这个项目到底解决什么问题
Gitea是一个用Go语言写的轻量级Git托管服务,整个程序编译完只有一个二进制文件,官方给的资源建议是单机1核2G内存就能跑得很舒服。它对外能提供完整的Git仓库托管能力,包括代码浏览、Issue跟踪、Pull Request、Webhook、Wiki、以及最近几个版本加入的Gitea Actions CI/CD,基本上平时在云端托管平台上用得上的功能,它都有对应的实现。
它真正解决的问题,是"版本控制服务自部署"这件事的门槛。GitLab CE其实是同类里功能最全的,但默认安装跑起来动不动要吃几个G内存,小团队部署一台机器光跑它就显得奢侈。Gitea把目标定在"够用且轻",你不需要为了一两个小项目去养一头大象,一台闲置的小主机或者云服务器就足够了,这对预算有限的技术团队、个人开发者、以及想在内网搞代码管理又不想被商业授权绑架的场景来说,吸引力非常直接。
我记得第一次把Gitea装起来之后,启动了服务一查进程内存占用只有一两百兆,当时就有点意外。要知道同样的事情交给GitLab,光是几个Sidekiq后台进程就能吃掉将近两个G的物理内存。这个差距在小内存VPS上几乎是决定性的。
1.2 和GitLab、云端私库横向对比,Gitea赢在哪
光说"轻"还不够,选型的时候我把三条主流路线放一起做了对比:继续用云端托管平台的私有仓库、部署GitLab CE、部署Gitea。下面这张表是我当时整理的结论:
| 对比维度 | 云端私有仓库 | GitLab CE | Gitea |
|---|---|---|---|
| 部署成本 | 零部署,开箱即用 | 高,依赖组件多 | 极低,单二进制或单容器 |
| 资源占用 | 免费额度有限制 | 2G内存起步 | 1核2G足够 |
| 私仓数量/人数限制 | 有,付费解锁 | 无限制 | 无限制 |
| 内网可达性 | 受外网限制 | 可内网化 | 可内网化 |
| CI/CD能力 | 强 | 很强 | 基本能用,Actions逐步完善 |
| 升级维护成本 | 平台方负责 | 较高 | 非常低 |
| 迁移自由度 | 受限 | 完全自主 | 完全自主 |
我当时的判断依据有这么几点。第一,团队里有几个项目涉及客户私有化交付,代码不能出内网,云端私库直接排除。第二,GitLab功能虽然完整,但公司那台备用的4核8G服务器还要跑其他服务,实在匀不出几个G内存给它。第三,团队对CI/CD的需求只是"能跑编译和单元测试",还到不了重度流水线编排的程度,Gitea Actions入门完全够。综合下来,Gitea就成了那个最不折腾的选择。
1.3 合适与不合适的场景,提前说清楚
用了这一年多,我也摸清了Gitea的边界。它特别适合这几类情况:二三十个协作成员以内的小团队,仓库数量几十个、单仓历史不要特别夸张的场景;需要把代码完全收在内网、但又不愿意为Git仓库服务多花硬件预算的场景;以及个人开发者想给多个设备搞一个私有Git中央仓库的场景。
反过来,如果你的团队有几百号人、需要复杂的代码评审权限矩阵、重度依赖定制的CI/CD流水线、或者需要和公司已有的AD域控做细粒度同步,那Gitea目前的权限模型和插件生态可能会让你觉得抠手,这种情况GitLab甚至商业版方案会更合适。说白了,Gitea不是要取代所有Git托管方案,它是在"够用"和"轻量"之间找到了一个非常漂亮的平衡点。
2. Linux环境安装Gitea:纯二进制部署,半小时跑通
2.1 准备工作:系统账号与目录规划
Gitea官方推荐不要用root直接跑服务,这是所有自建服务的基本素养。我习惯在LInux服务器上先建一个专门的git系统用户,把所有和仓库相关的数据都收拢到这个用户下,这样即使Gitea进程被攻破,攻击者拿到的也只是一个受限权限的shell,不至于直接动到系统根目录。
sudo useradd --system --shell /bin/bash --create-home --home-dir /home/git git接着是目录规划。Gitea运行时主要涉及三块:工作目录(存放仓库裸数据和LFS对象)、自定义目录(存放模板和自定义配置覆盖)、日志目录。我统一放在/var/lib/gitea下面,配置文件则放在/etc/gitea,符合Linux的FHS习惯,后续做备份也只需要盯住这几个路径。
sudo mkdir -p /var/lib/gitea/{custom,data,log} sudo chown -R git:git /var/lib/gitea sudo chmod -R 750 /var/lib/gitea sudo mkdir -p /etc/gitea sudo chown root:git /etc/gitea sudo chmod 770 /etc/gitea这里有个细节容易被忽略:/etc/gitea在安装向导完成之前需要保持git用户可写,因为首次网页初始化要往里面写app.ini配置文件。等配置生成完了,再把它改成root:git和750权限,避免运行用户随意篡改自己的服务配置。这是我第一次部署时看官方文档里没细讲、后来自己踩了一脚才记住的点。
2.2 下载安装与初始化配置
Gitea的安装方式很多,官方源、包管理器、二进制都可以。我个人推荐直接用官方发布的二进制包,一来版本可控,二来不引入额外的包管理依赖。到GitHub或dl.gitea.com上挑对应平台的版本下载即可,以常见的linux-amd64为例:
GITEA_VERSION=1.22.6 wget -O /tmp/gitea https://dl.gitea.com/gitea/${GITEA_VERSION}/gitea-${GITEA_VERSION}-linux-amd64 chmod +x /tmp/gitea sudo mv /tmp/gitea /usr/local/bin/gitea gitea --version安装完二进制还不能急着启动,先确认一下git和git-lfs这两个依赖在不在。Gitea本身封装了Git操作,但底层调用的还是系统git命令,没装的话仓库的push/pull会直接报错:
sudo apt install -y git git-lfs # Debian/Ubuntu # 或者 sudo dnf install -y git git-lfs # RHEL/CentOS/Rocky启动Gitea之前,我建议先手动指定环境变量跑一次,确认它能正常拉起,再用systemd接管。第一次启动时给它一个明确的用户身份和目录环境:
sudo -u git GITEA_WORK_DIR=/var/lib/gitea GITEA_CUSTOM=/var/lib/gitea/custom \ gitea web --config /etc/gitea/app.ini看到监听3000端口的日志之后,说明二进制本身没问题,Ctrl+C停下来,进入下一步用systemd做守护。
2.3 用systemd管理Gitea服务
systemd服务单元文件放在/etc/systemd/system/gitea.service,内容如下:
[Unit] Description=Gitea (Git with a cup of tea) After=network.target [Service] User=git Group=git WorkingDirectory=/var/lib/gitea/ ExecStart=/usr/local/bin/gitea web --config /etc/gitea/app.ini Restart=always Environment=USER=git HOME=/home/git GITEA_WORK_DIR=/var/lib/gitea [Install] WantedBy=multi-user.target写完后执行:
sudo systemctl daemon-reload sudo systemctl enable --now gitea sudo systemctl status gitea这里解释两个设计决策。Restart=always保证服务进程异常退出后能自动拉起,谁也不想半夜被"仓库服务挂了"的告警吵醒。Environment里单独指定USER和HOME,是为了让Gitea在systemd环境下也能拿到正确的用户身份,否则某些依赖ssh key路径的功能会去/root底下找文件,那就全乱套了。
2.4 首次网页初始化的坑
浏览器访问http://服务器IP:3000,会进入Gitea的安装引导页。这里要填数据库配置、站点名称、服务端口和HTTP访问地址。小团队我直接选的SQLite,省掉一个数据库服务,备份也就一个文件搞定。如果预估仓库量和并发写入会比较大,再上PostgreSQL或MySQL不迟。
这个页面最容易填错的是"SSH服务器端口"。很多服务器为了安全把ssh改到了非22端口,于是有人在这里填成服务器的ssh端口,结果Gitea生成的clone地址全是错的。这里要填的是给Git SSH协议用的端口,默认22即可,除非你打算给Gitea单独映射一个入站端口。另外"Gitea HTTP监听端口"保持3000,后续我会在前面挂一层Nginx对外提供443,而不是让Gitea直接暴露公网。
安装引导页提交后会生成/etc/gitea/app.ini。生成完了记得把配置目录权限收紧:
sudo chown -R root:git /etc/gitea sudo chmod 750 /etc/gitea这步做完,一套基于Linux二进制的Gitea就跑起来了。整个过程熟练的话不到半小时,我第一次折腾时被SELinux的一块权限卡了差不多一顿饭工夫,后面在踩坑章节里会专门提。
3. Docker部署Gitea:一条命令上手的懒人方案
3.1 docker-compose编排与数据卷规划
如果你的服务器上本来就有Docker环境,或者你对Linux发行版间的系统依赖差异没把握,直接用官方镜像会更省心。Gitea官方镜像基于Alpine,已内置git、git-lfs和ssh能力,拉起来就是完整服务。我用的是docker-compose,编排文件如下:
version: "3" services: gitea: image: gitea/gitea:1.22.6 container_name: gitea environment: - USER_UID=1001 - USER_GID=1001 - GITEA__server__ROOT_URL=http://git.example.com/ - GITEA__server__HTTP_PORT=3000 - GITEA__server__SSH_PORT=2222 - GITEA__database__DB_TYPE=sqlite3 restart: always volumes: - ./gitea-data:/data - /etc/timezone:/etc/timezone:ro - /etc/localtime:/etc/localtime:ro ports: - "3000:3000" - "2222:22"USER_UID和USER_GID需要和宿主机上你准备给Gitea用的实际uid/gid保持一致,这是Docker部署Gitea最容易出问题的位置。容器内部服务用户是git(uid 1000),但如果它写出来的数据卷文件属主和宿主机账户对不上,后面备份、迁移、以及宿主机直接操作数据目录时都会被权限问题卡住。我习惯先在宿主机上建一个固定uid的普通用户,然后把这两个环境变量指过去。
SSH端口这里我做了个映射:容器内部Gitea仍然监听22,但宿主机对外暴露2222,这样不会和宿主机自己的sshd冲突。对应地,ROOT_URL里的SSH克隆地址要写成ssh://git@git.example.com:2222/...,否则克隆地址还是会带默认22,连不上门。
3.2 数据持久化和升级策略
官方镜像把所有东西都塞进/data目录,包括仓库数据、LFS、配置文件app.ini和SQLite数据库文件。所以备份就变得非常简单:要么直接定时打包宿主机上的./gitea-data目录,要么进容器执行官方提供的gitea dump命令做一致性导出。
升级的时候,先拉新镜像,再重新create容器即可:
docker compose pull docker compose up -d升级前老规矩先做两件事:备份数据目录,以及看一眼Release Notes里有没有破坏性变更。Gitea在minor版本之间的升级一般很平滑,但跨大版本(比如从1.18跳到1.22)时,建议按官方升级文档的要求一步步来,不要跳着跨多个大版本直接升,数据库结构可能会不兼容。
3.3 挂载已有的代码目录时要注意什么
有同事问过我,能不能把宿主机上现成的一堆裸仓库目录直接挂载给Gitea使用。理论上Gitea扫描的是/data/git/repositories下的仓库目录,如果你手头刚好有裸仓库,放进去、保证属主和gitea运行用户一致,再重启容器,它一般能识别出来。
但这里我强烈建议别这么做。Gitea的仓库数据除了裸仓库本身,还关联着它的权限元数据、Owner信息、仓库描述、Webhook配置,这些都存放在SQLite或数据库里。直接手动塞裸仓库进去,最直接的后果就是Gitea首页能看到仓库名字,点进去却因为数据库里没有对应的repository记录而报错。正确做法是把裸仓库用git clone --bare导入到Gitea里,走正规的创建流程,让数据库记录也一起建立起来。
4. 仓库级Token权限:只给某个仓库的令牌到底能不能配
4.1 Gitea Token体系的现状与局限
这个需求我最初是在对接CI时遇到的。公司内部有个自动化平台需要push代码到指定仓库,安全部门要求凭证最小权限——既不能给管理员密码,也不能给一个能操作全部仓库的全量Token。当时我去Gitea的"设置 -> 应用"页面创建Token,发现新版本确实支持选择权限范围了,比如read:repository、write:repository、read:issue这些,但选择范围的时候并没有"指定某个仓库"的下拉选项。
换句话说,Gitea的Personal Access Token是按权限范围划分的,不是按仓库粒度划分的。一个拥有write:repository权限的Token,对当前账号能访问到的所有仓库都拥有写权限。这是Gitea和GitHub的Fine-grained PAT在能力上的一个现实差异,至少在1.22这个版本上,网页端依然没有提供纯UI的仓库级Token创建入口。
4.2 方案一:专用账号加协作者权限,曲线实现单仓Token
既然Token权限跟着账号走,那就让一个账号只拥有一个仓库的访问权限。具体做法如下:
第一步,注册或创建一个专用账号,比如叫ci-bot。这个账号不参与日常开发,纯粹作为自动化凭证的载体。
第二步,用管理员或仓库Owner身份,把ci-bot加为目标仓库的协作者。在仓库页面进入"设置 -> 协作",添加ci-bot,权限按需给读或写。注意"写"权限指的是对仓库的常规写操作,包括push代码,但对其他仓库一点权限都没有。
第三步,登录ci-bot账号,到"设置 -> 应用"页面生成Token,权限范围只勾选真正需要的,比如write:repository。因为ci-bot这个账号只被添加到了那一个仓库的协作者列表,它生成的Token能访问的范围自然就局限在目标仓库上,再加上公开仓库的可见部分,但私密仓库基本不会多一个。
这个方案我实际验证过:用ci-bot的Token对该仓库执行clone和push都正常,去访问另一个毫无关系的私有仓库时直接被拒绝,HTTP状态码403,信息是权限不足。对"只能访问某一个仓库"的需求来说,这是目前Gitea上最正统、也最可控的做法。
如果你希望连公开仓库都不能通过这个Token读取,可以在生成Token时宁可少勾选范围,比如只勾read:repository,不给其他任何scope,把Token的破坏面压到最小。
4.3 方案二:用Deploy Key处理只读自动化场景
还有一种更Git原生的思路:Deploy Key。它本质上是一把SSH公钥,作用范围天生就被绑定在某一个仓库上,根本不存在访问其他仓库的可能性。对于只需要拉取代码的自动化场景,比如服务器上跑编译、内网机器拉部署包,这是比Token更稳妥的选择。
生成一对专用的密钥对,不要把私钥混在日常开发用的~/.ssh里:
ssh-keygen -t ed25519 -C "ci-deploy-$(date +%Y%m%d)" -f ~/.ssh/gitea_deploy把gitea_deploy.pub里的内容粘贴到目标仓库的"设置 -> Deploy Keys"里,勾选"启用写访问"则代表允许推送,不勾则只读。然后在需要拉代码的机器上配置SSH config,把该仓库的域名或host别名指向这把私钥。实际用下来,只读场景我全部推荐Deploy Key,因为它从机制上杜绝了Token泄露导致的全仓沦陷,安全和省心程度都高一个档次。
Token和Deploy Key怎么选?我个人的习惯是这样:CI需要长期稳定的写权限,就用方案一的专用账号加Token;外部服务器只读拉代码,就用Deploy Key;维护人员自己的日常自动化脚本,才用个人账号生成的Token。这样权限边界清晰,出事也容易追溯。
4.4 给自动化场景的额外提醒
最后补一条实战建议:上述Token如果服务的是移动端或远端CI,建议在生成时尽量缩短有效期,Gitea支持给Token设置过期时间,把周期压到三个月甚至更短,配合定期轮换,比一个永久Token放在CI配置文件里安全得多。另外,Token一旦写进脚本,就是明文凭证,务必把存放这些文件的目录权限收紧到最小,别随手扔在web根目录或者共享盘里。我见过不止一次因为CI脚本的.env文件权限没管好,导致整个代码库被拖走的事故,这类教训多小心都不为过。
5. 日常管理中的高频操作与真实踩坑记录
5.1 备份恢复不能只拷数据目录
Gitea的备份分两部分:文件系统和数据库。如果用的SQLite,数据库就是一个/data/gitea.db文件,理论上把整个数据目录打压缩包就能完成备份。但运行时直接复制db文件可能拿到一个不一致的副本,尤其是push操作正在写入的那一刻。稳妥做法是先用官方命令做一次带锁的一致性导出:
sudo -u git gitea dump --config /etc/gitea/app.ini --work-path /var/lib/gitea这条命令会生成一个zip包,里面包含了仓库数据、数据库快照、自定义配置、日志和LFS文件,基本上一包走天下。我现在的服务器上用cron每周日凌晨执行一次dump,再配合异机同步,灾难恢复时间能控制在半小时以内。
恢复的时候也简单:新机器装好Gitea和依赖,把zip包解压,按备份时的目录结构放回去,调整好属主,启动服务即可。实测迁移过两次,只要版本不大跨,恢复后连Webhook和密钥都原样保留。
5.2 版本升级的两个关键动作
Gitea迭代速度不慢,安全修复和功能更新都挺勤快,但升级这事儿不能无脑升级。我给自己定的规矩是:只升级补丁号和次版本号,不做大版本跨越;升级前先备份;升级后进管理面板看一遍"监控"页面,确认队列和任务没报错。
升级步骤本身不复杂。二进制部署的话,下载新版本覆盖/usr/local/bin/gitea,然后重启服务:
sudo mv /tmp/gitea /usr/local/bin/gitea sudo chmod 755 /usr/local/bin/gitea sudo systemctl restart gitea sudo journalctl -u gitea -n 100 -f盯着日志确认没有数据库迁移报错就完成了。Docker部署更省事,但注意升级后数据卷里缓存和旧的临时文件偶尔会和新版本不对付,遇到页面样式怪异或某些功能异常时,先清理一下/data目录下的临时缓存再排查别的。
5.3 我遇到过的三个坑与解决办法
第一个坑是SELinux。CentOS系服务器装完Gitea,服务一直起不来,journalctl里全是权限拒绝。排查了半天才发现是SELinux阻止了git用户对/var/lib/gitea的写入。治本的办法不是关SELinux,而是恢复正确的文件上下文:
sudo semanage fcontext -a -t httpd_sys_rw_content_t "/var/lib/gitea(/.*)?" sudo restorecon -Rv /var/lib/gitea第二个坑是反向代理后的WebSocket连接失败。我在Nginx后面做了HTTPS终结,Gitea的Web页面能开,但仓库Runner和实时通知一直异常。原因出在Nginx没有转发WebSocket的Upgrade头。解决方式是在location块里加上:
proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_set_header Host $host;第三个坑是文件上传大小限制。默认情况下Nginx对客户端请求体大小限制是1M,想往Issue里贴大截图或者推一个几十MB的压缩包附件时,会直接收到413错误。在Nginx的server或location块里调大限制就好:
client_max_body_size 512m;这几个坑都属于"不报错时永远不会想到、一旦遇到又特别抓狂"的类型,记录下来希望后面的人少走点弯路。
6. 团队里实际用了一年多,几条回头看才懂的建议
Gitea在我们这儿已经跑了快两年,从最初的四个仓库扩展到现在的四十多个,包含前端、后端、脚本、文档和客户交付包,内存占用一直稳定在几百兆的水平,中途只因为磁盘满了重启过一次。如果说要挑几条"早知道就好了"的经验,我最想分享的是这三条。
第一,仓库目录的命名和组织方式要在一开始就定好规矩。Gitea支持带路径的仓库名,比如team-a/service-order和team-b/report-web,这种两级命名在权限和检索上都会清晰得多。等仓库多起来再改名,牵涉到所有本地remote地址,成本会翻好几倍。
第二,把管理员账号和日常开发账号分开。不要拿自己的主账号去当管理员,也别把admin密码分享给所有同事。管理员账号只用来做配置变更和用户管理,日常开发都用自己的账号,出问题好定位,权限也干净。
第三,定期做一次仓库健康检查。Gitea管理后台有仓库监控和GC优化入口,隔一段时间跑一次,能明显减少仓库体积增长的速度。另外,开启Gitea的垃圾回收调度后,大仓库的clone操作会快不少。
自建版本控制系统这件事,其实没有标准答案,合适的就是最好的。Gitea对很多人来说可能不是功能最全的那个,但它用极低的维护成本换来了稳稳当当的核心体验。如果你也正在为代码四处散落发愁,照着这篇文章把Gitea跑起来,再用上两天,你大概率会和我一样,再也回不去那个靠移动硬盘传代码的原始时代了。