多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

t3code轻量自托管代码管理方案:基于Gitea与Git Hook的私有仓库搭建实践

t3code轻量自托管代码管理方案:基于Gitea与Git Hook的私有仓库搭建实践 1. 项目概述为什么我会盯上 t3code 这个名字先说结论t3code 不是某个官方大厂的标准项目代号更像是一个轻量级、面向开发者的私有代码管理工具的代号。我第一次听到这个名字是在一个技术社群的闲聊里。有人吐槽说团队内部想搞一套“自带权限、能离线、能自托管”的代码托管方案但 GitLab 太重、Gitea 又觉得功能不够顺手最后有人甩了一句“要不看看 t3code”。当时我第一反应是这名字有点怪难道是 T3 框架T3 Stack的某个衍生品后来仔细扒了一圈才发现这更像是社区里对“第三套代码管理方案”的简称——第一套是中心化 Git 服务器第二套是 GitHub/GitLab 这类一体化平台第三套就是更轻、更定制、更贴近小团队研发节奏的私有化工具集。它解决的实际问题非常清晰中小型团队不想被云端平台的功能堆叠绑架又不想在自建 GitLab 上反复处理资源占用和升级兼容问题于是选择用最少的核心组件搭一套够用、可控、能平滑迁移的代码管理环境。这套思路在 2024 年到 2025 年之间特别流行因为越来越多团队开始回归“工具应该服务流程而不是流程迁就工具”的朴素认知。如果你是下面这几类人这篇文章应该能给你带来不少参考正在为团队挑选代码托管方案但被 GitLab 资源需求劝退的已经用着 Gitea 或者 Gogs但想在权限模型、CI/CD 集成上做进一步定制的纯粹想自己动手搭一套私有代码环境顺带搞清楚 Git 服务底层原理的对“t3code”这个名字好奇但搜不到系统资料想看看有人实操过的经验我自己在搭建和后续使用 t3code 的过程中踩了不少坑也积累了一些非常实用的经验。下面我会从整体设计思路、核心组件拆解、实际操作流程、常见问题排查四个维度把整个项目掰开揉碎讲清楚。2. 整体设计思路t3code 到底想解决什么问题2.1 轻量自托管的定位逻辑在聊具体技术之前先讲一个很多人容易忽略的问题为什么中小团队会主动放弃现成的 GitHub/GitLab/Gitea而选择自己拼一套 t3code表面上是“功能不够”或“资源受限”但本质上是对控制权的需求。云端平台的好处是零运维但代价是数据不在自己手里权限模型受平台约束CI 运行在别人家机器上。对于涉及商业保密代码、客户交付物或者合规审计的团队来说这个代价会越来越难以接受。自建 GitLab 看似解决了控制权问题可它需要 4GB 内存起步、需要配套 Redis、PostgreSQL、Sidekiq、Nginx 等一系列组件小团队一台 2G 2 核的云主机根本跑不动。t3code 的思路就很朴素与其用一个重型平台解决所有问题不如拆成几个高内聚的小服务各自只做一件事但把这件事做扎实。我在设计时参考了三层结构存储层用纯 Git 仓库作为最底层保证数据和标准 Git 协议完全兼容服务层一个轻量的 Git 守护进程负责接收 push/pull 请求和做权限校验界面层一个极简的 Web 管理面板主要做用户管理、仓库查看、Token 签发不追求花哨。这套结构和早期 Gogs 的思路有点像但在权限控制上更接近企业内部对“分支保护、代码评审、审计日志”的要求。2.2 方案选型背后的取舍原则做技术选型不能只看流行度更要看团队实际情况。我整理了一个对照表方便你看出 t3code 这类方案的取舍原则方案资源占用权限模型定制难度维护成本适用规模GitHub 私有仓库零运维较强但受平台限制低中按用户收费个人/小团队GitLab CE 自建高≥4G 内存强MR 流程完整中高中大型团队Gitea 自建低1G 内可跑基础可用中低小型团队t3code 组合低1G 内存够强可自定义 Hook高中小团队定制我自己选 t3code 而不是直接上 Gitea核心原因是团队有两条硬性要求每个仓库的安全策略不同有的仓库要求单人强制 code review有的只需要 CI 自动检查通过即可合并需要接入自定义的审批流程某些核心仓库要对接内部的发布系统不能只靠 Git 的 webhook 做单向通知而是要能拿到推送事件后阻塞合并动作。这两个需求在现成平台上实现起来都绕而在 t3code 里我可以通过自定义 Git Hook 和内部 API 轻松处理掉。2.3 这个方案的真正价值点说到底t3code 最大的价值不是“比 GitLab 轻”或“比 Gitea 可定制”而是让团队重新拿回了对代码工作流的控制权。你在 GitHub 上开 Merge Request规则是平台定的你在 GitLab 上配置 Approve Rule逻辑是平台约束的。但在 t3code 里从 push 之前怎么检查、push 之后怎么触发到合并之前调哪个接口全都可以按照自己的节奏来。这对那些真正重视研发效率和代码质量的团队来说不是“折腾”而是必要的掌控感。3. 核心组件拆解与实操要点3.1 基于 Gitea 的核心服务层如果你决定自己搭我建议不要把 t3code 理解为一个全新的软件而是把它当成一套组合方案。其中最核心的组件我选的是 Gitea。原因简单说就三条它是 Go 写的部署包就一个二进制文件内存占用起步大概 150MB1G 内存机器也能轻松跑它原生支持 Git 协议和 HTTP/HTTPS 两种访问方式和标准 Git 客户端兼容性极好它的数据库支持 SQLite、MySQL 和 PostgreSQL。小团队我直接推荐 SQLite省掉一个数据库进程。但要注意光有 Gitee 还不够。因为 t3code 的核心目标之一是“灵活的权限控制”而 Gitea 原生的权限范围只到仓库级别和团队级别没法做到“同一仓库不同分支不同权限”。这个缺口靠自己写 Hook 来补齐后面会详细说。3.2 权限与钩子机制自定义 Git Hook 是灵魂很多人听到“Git Hook”就觉得只能写点 shell 脚本做通知实际上它完全可以承担权限校验和流程控制的角色。我实现的方案是这样的Git 服务端有两个关键 Hookpre-receive 和 post-receive。pre-receive在客户端 push 的数据写入仓库之前触发脚本里可以通过标准输入读取旧版本号 新版本号 引用名比如123abc... 456def... refs/heads/master这时候我可以去解析新版本号和旧版本号之间的差异拿到涉及的文件列表、提交者信息和 commit message。然后调用内部权限接口判断这个用户是否有权限向 master 分支提交commit message 是否满足规范比如必须包含 Jira 单号是否包含已被禁止提交的文件比如密钥文件或超大二进制文件如果校验失败直接以非零状态退出Git 客户端就会收到一条错误信息push 被拒绝。这一整套逻辑不依赖任何平台限制只要你懂 Git 协议就能自由扩展。一个简单的 pre-receive 示例伪代码思路#!/usr/bin/env bash while read oldrev newrev refname; do # 只检查 master 分支 if [[ $refname refs/heads/master ]]; then # 调用远程权限服务验证 FORBIDDEN$(python3 /opt/t3code/checker.py $newrev $oldrev $USER) if [ -n $FORBIDDEN ]; then echo ERROR: $FORBIDDEN 2 exit 1 fi fi done exit 0这个脚本放在每个仓库的hooks/pre-receive路径里Gitea 每次收到 push 时都会自动执行。在项目管理页面里需通过归档或模板仓库的方式把 hook 文件复制到新仓库。3.3 前端管理面板做一个够用但克制的界面做 t3code 的第二块拼图是一个简单的管理面板。我没有选用非常庞大的前端框架而是用了一个轻量级的 Vue 3 应用打包完之后只有几十 KB部署在 Nginx 上的效果和传统工业风面板没区别但我倾向于加上 NVIDIA 公司风格的深色主题。核心功能就四个用户和 Token 管理支持创建只读 Token、读写 Token每个 Token 可以限制到具体仓库和具体操作仓库创建和 Hook 配置创建仓库时自动从模板目录复制 hooks不需要人手工登录服务器处理审计日志查看记录所有 push、pull、登录和 Token 签发行为方便追溯分支保护规则维护定义一个简单的规则 JSON比如哪些分支需要哪些人审批。这套面板不是重写 Gitea而是通过 Gitea 的 API 做管理。相当于把前端面板和第二层的 Git 服务挂接起来。3.4 数据存储SQLite 的取舍与备份策略在小团队规模下SQLite 是完全够用的。默认情况下一两 G 的仓库数据、几十个并发连接SQLite 都能轻松扛住。而且备份极其简单——直接复制data/gitea.db文件即可。但如果你团队成员超过 30 人或者有大量 WebHook 推送、审计日志查询我会建议切换为 PostgreSQL。原因有两个SQLite 同时只允许一个写连接如果推送频繁可能出现database is locked错误审计日志如果长期积累SQLite 的查询性能会显著下降。切换数据库的操作也很简单官方文档里有一个gitea dump命令和配置文件里的DB_TYPE设置。不过倒数据的时候要注意仓库 Git 数据本身是独立存储的只要保留repositories目录数据库只是元数据别怕丢。4. 完整实操过程从零搭一套 t3code 环境4.1 环境准备与参数选择硬件参考配置项目最低配置推荐配置CPU1 核2 核内存1 GB2 GB磁盘20 GB50 GBSSD系统Ubuntu 20.04Debian 12 或 Ubuntu 22.04我这里以一台 2 核 2G 内存的云主机为例系统是 Ubuntu 22.04。初始化和安装依赖sudo apt update sudo apt upgrade -y sudo apt install -y git curl sqlite3 nginx4.2 部署 Gitea 二进制服务安装 Gitea 并不复杂。到其 GitHub Releases 页面下载符合架构的二进制包。我以 1.21.x 为例# 下载二进制文件 wget -O /tmp/gitea https://github.com/go-gitea/gitea/releases/download/v1.21.11/gitea-1.21.11-linux-amd64 # 移动到目标目录并给予执行权限 sudo mkdir -p /opt/t3code/gitea sudo mv /tmp/gitea /opt/t3code/gitea/gitea sudo chmod x /opt/t3code/gitea/gitea # 创建运行用户安全考虑不要用 root 运行 sudo adduser --system --group --disabled-password --shell /bin/bash gitea # 创建必要的数据目录 sudo mkdir -p /var/lib/gitea/{custom,data,logs,repositories} sudo chown -R gitea:gitea /var/lib/gitea # 写入系统服务文件 sudo tee /etc/systemd/system/gitea.service /dev/null EOF [Unit] DescriptionGitea (t3code) Afternetwork.target [Service] Usergitea Groupgitea WorkingDirectory/var/lib/gitea/ ExecStart/opt/t3code/gitea/gitea web --config /etc/gitea/app.ini Restartalways EnvironmentUSERgitea HOME/home/gitea [Install] WantedBymulti-user.target EOF # 启动并设置开机自启 sudo systemctl daemon-reload sudo systemctl enable --now gitea配置文件/etc/gitea/app.ini中我用的比较关键的几个参数[server] PROTOCOL http HTTP_PORT 3000 DOMAIN git.yourcompany.com ROOT_URL https://git.yourcompany.com/ START_SSH_SERVER true SSH_PORT 222 [database] DB_TYPE sqlite3 PATH /var/lib/gitea/data/gitea.db [repository] DEFAULT_PRIVATE private FORCE_PRIVATE true [service] REQUIRE_SIGN_IN_VIEW true这一步有几个易错点我先提醒一下不要直接复制某些网上的配置特别是SSH_PORT。如果你在同一台机器上还跑着系统自带的 SSHD 服务默认 22 端口就必须把 Gitea 的 SSH 端口改成 222否则起冲突DEFAULT_PRIVATE private和FORCE_PRIVATE true是内部仓库的底线设置一定要开否则新仓库可能默认公开如果你的主机还有防火墙规则要记得放行 22 和 222 端口否则推代码会卡住。4.3 配置 Nginx 反向代理和 HTTPSGitea 本身监听在 3000但实际访问时建议用 Nginx 加 HTTPS 反向代理。这样一来既可以通过 443 端口统一访问也方便添加 TLS 证书和转发 WebSocket 连接Gitea 的 Git LFS 和 SSH over HTTPS 都需要。我在/etc/nginx/sites-available/t3code.conf中写入server { listen 443 ssl; server_name git.yourcompany.com; ssl_certificate /etc/ssl/private/t3code.pem; ssl_certificate_key /etc/ssl/private/t3code-key.pem; location / { proxy_pass http://127.0.0.1:3000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_read_timeout 600s; } }这里要特别说明proxy_read_timeout如果不加大推送大文件时 Nginx 会提前断开连接如果你用 Let’s Encrypt 证书可以使用 certbot 自动续期千万别把证书路径写错否则站点会直接挂掉Gitea 自身的ROOT_URL一定要改成https://git.yourcompany.com/否则页面生成的克隆地址仍然是http://推送时又会多一道麻烦。4.4 添加自定义 Pre-receive HookGitea 的仓库文件存储路径我安排在/var/lib/gitea/repositories/{owner}/{repo}.git。每个仓库下都有hooks目录Gitea 已经预留了带_sample后缀的脚本模板。我的处理方式是写一个通用的 pre-receive 脚本然后通过后台任务统一批量复制到每个仓库。这样新仓库创建后执行一次初始化脚本即可。通用脚本逻辑如下#!/usr/bin/env bash while read oldrev newrev refname; do if [ $refname refs/heads/master ] || [ $refname refs/heads/main ]; then python3 /opt/t3code/hook_check.py $oldrev $newrev $refname $USER if [ $? -ne 0 ]; then echo error: push blocked by t3code policy 2 exit 1 fi fi done exit 0hook_check.py 的核心职责是调用内部权限 APIimport requests import sys oldrev, newrev, refname, username sys.argv[1:5] resp requests.get( http://127.0.0.1:8000/v1/check, params{ oldrev: oldrev, newrev: newrev, refname: refname, username: username, }, timeout5, ) sys.exit(0 if resp.status_code 200 else -1)4.5 配置 Git 客户端访问服务端部署好后客户端直接按标准流程操作即可git remote add origin gitgit.yourcompany.com:yourteam/project.git git push -u origin master如果你用的是 HTTP 方式在克隆时需要在 URL 里带上用户名或者输入 Tokengit clone https://git.yourcompany.com/yourteam/project.git首次 push 的时候Git 客户端会提示输入用户名和密码。密码处填你的 Gitea Token 而不是登录密码否则会报权限错误。这个问题太常见了我见过好几个团队在这卡了一下午。4.6 极简管理面板的部署这个面板不是必需品因为 Gitea 自己的 Web 界面已经能完成用户管理和仓库管理。我加面板主要是为了让团队成员不需要理解 Git 权限概念也能自助完成 Token 申请和查看分支保护规则。用 Vue 3 写了一个单页应用利用 Gitea API 的GET /api/v1/repos/{owner}/{repo}/hooks、POST /api/v1/users/{username}/tokens等接口。部署也只是把打包后的静态文件放到一个 Nginx 目录下然后通过/panel/路径访问。里面有一块“分支保护规则”的维护页面规则以 JSON 形式存储到后端一个很小的 Python 服务from flask import Flask, request, jsonify import json app Flask(__name__) app.route(/rules, methods[GET, POST]) def rules(): if request.method GET: with open(rules.json) as f: return jsonify(json.load(f)) with open(rules.json, w) as f: json.dump(request.json, f) return ok4.7 审计与备份“自己搭服务”最大的责任就是数据安全。Gitea 文档里有内置备份命令也可以直接用系统定时任务做全量备份sudo -u gitea /opt/t3code/gitea/gitea dump --config /etc/gitea/app.ini如果嫌 dump 太重它会打包整个仓库目录我建议用更轻量的方式tar czf /backup/t3code-$(date %F).tar.gz /var/lib/gitea/data /var/lib/gitea/repositories /etc/gitea这个命令把数据库、仓库裸仓库、配置文件都打包了。恢复的时候只需要解压到对应目录再重启 gitea 服务即可。恢复过程不会像 GitLab 那样复杂这也是轻量方案的好处。5. 常见问题与排查技巧实录5.1 推送被拒mastodon 分支冲突问题现象开发同学 push 到 master 被拒但服务端日志里没有任何报错。排查思路先看 pre-receive 钩子是否有问题。直接在服务端sudo -u gitea手动跑一次脚本检查是否有语法问题再看内部权限 API 是否返回了非 200 状态最后检查用户 Token 权限里是否缺少对应仓库的写入权限。最常见原因权限 API 配置中用户组和仓库组不匹配。我调试时在 API 日志里看到forbidden的标记才发现后来有同事改了配置把规则覆盖错了。5.2 SQLite 报错 database is locked现象推送频繁时日志里出现SQLITE_BUSY或者database is locked is not a valid input。原因分析SQLite 在写入时会给整个数据库加锁并发一高就容易出现这个报错。解决办法先判断并发是不是真的高。如果是 5~15 人团队偶尔报错可以直接在app.ini里加大数据库连接空闲时间如果频繁报错就迁移到 PostgreSQL。迁移方法不复杂# 先导出备份 gitea dump --config /etc/gitea/app.ini # 在 app.ini 中修改数据库类型 DB_TYPE postgres # 重启服务 systemctl restart gitea只改配置还不够需要用 Gitea 自带的迁移工具把 SQLite 数据倒过去。Gitea 官方在管理界面里提供了“配置”板块可以看数据库类型是否切换成功。5.3 SSH 能连但无法 clone现象ssh -T gitgit.yourcompany.com能正常联通提示成功但git clone gitgit.yourcompany.com:team/project.git却提示仓库不存在。原因Gitea 的 SSH 服务器虽然启动了但客户端的 SSH key 没有正确添加到 Gitea 用户配置中或者 SSH 的 AuthorizedKeys 命令没有读取到 Gitea 数据库。解决步骤登录 Gitea Web 界面在用户设置里添加 SSH Key检查系统authorized_keys文件是否存在且配置正确正常情况下 Gitea 会动态管理如果 SSH 服务端口改成了 222Git 客户端需要显式指定git clone ssh://gitgit.yourcompany.com:222/team/project.git5.4 前端面板 Token 权限不生效现象签发的 Token 明明勾选了“写仓库”权限但调用 API 时还是收到 403。原因Gitea 的 Token 在创建后会做一次缓存刷新刚创建的 Token 可能需要十几秒才能完全生效。另一个常见原因是 Token 在代码里被 URL 编码处理不当导致空格变成%20权限校验失败。建议创建 Token 后立马验证一次curl -H Authorization: token token https://git.yourcompany.com/api/v1/user如果返回 JSON 为空或带 401说明 Token 没生效如果返回 404说明 URL 有拼接问题。5.5 推送大文件时连接中断现象代码仓库超过 500MBpush 时报RPC failed; curl 56 OpenSSL SSL_read: Connection was reset by peer。原因Nginx 默认client_max_body_size限制为 1MB且proxy_read_timeout默认 60 秒大文件推送必然超时。解决修改 Nginx 配置server { client_max_body_size 1024m; location / { proxy_read_timeout 600s; proxy_send_timeout 600s; } }改完重启 Nginx 再试。如果说仓库里必须保存 1GB 以上大文件那更合适的方案是启用 Git LFS而不是把普通文件硬推上去。这是绝大多数团队最容易忽视的一个点。5.6 权限模型不够用分支级保护如果你连分支级保护都想要Gitea 原生还支持通过仓库“受保护分支”设置。但在 t3code 里我建议把这些规则交给 pre-receive 钩子统一管理。这样每个分支的 push 都必须经过钩子检查没有漏网之鱼。举个例子团队想实现“只有 Release Manager 能合并到 release 分支”那你只需要在内部权限 API 里判断username是否属于某个组然后返回拒绝即可。这种扩展方式非常灵活完全不受平台功能边界影响。6. 几个踩过坑之后的深刻体会自己搭过一遍 t3code我有几个比较深的感触想分享出来这些经验是纯粹的兼容性和部署文档里很难看到的。第一个体会轻量不等于简单配置复杂度被转移了。使用 GitLab 时你的复杂点在“需要维护多个组件”使用 t3code 时复杂点转移到了“如何设计好 Git Hook 和权限逻辑”。后者对懂代码的人更友好因为你可以用自己熟悉的语言去理解和修改规则而不是在一个庞大的配置页面里找开关。第二个体会Token 管理是所有自托管的痛点。团队成员经常把 Token 配置到 IDE 的 Git 面板里Token 过期之后 IDE 会反复弹窗很多人不知道如何重新生成。我用前端面板专门做了一个“Token 自助申请”入口让普通成员在界面上生成、复制、撤销而不是教他们敲命令。这大大降低了日常使用的阻力。第三个体会审计日志不是可有可无的。开发团队内出现“谁改了线上分支却不承认”这种事一旦发生没有日志就很被动。t3code 的 Gitea 内置审计日志只记录了关键操作我额外在 pre-receive 钩子里把每次 push 的详细信息提交人、提交信息、变更文件数写到一个独立日志目录用来满足内部审计需求。这类细节平时不起眼但真正出事的时候能救急。第四个体会备份脚本一定要有演练。我在部署初期虽然配置了定时备份但从没实际恢复过。直到一次磁盘告警我才真正跑了恢复流程。中间遇到gitea dump生成的 zip 包没法直接解压因为路径嵌套层级太深后来又改成 tar 方式才算彻底解决。备份不只是“存了一份文件”而是要在恢复场景下能真正用起来。7. 后续可以继续扩展的方向t3code 这套结构搭起来之后还可以围绕它做很多增强。比如接入 CI 流水线只用写一个 webhook 接收器不限语言比如和内部运维系统打通实现“仓库创建即开通测试环境”的自动化流程再比如做代码统计和分析报告定时扫描仓库中是否有密钥泄露。但我的建议始终是先跑起来感受一下基础方案的瓶颈和团队的真实需求再决定往哪个方向扩展。自托管方案的好处就是随时可以改不会像迁移云平台那样牵一发动全身。如果你也正在纠结“到底是选一个现成平台还是自己搭一套”我的看法是有明确定制需求且团队有至少一个人愿意花时间维护基础设施就大胆试 t3code。如果只是想要一个能存代码的地方那就乖乖用现成平台省下时间专注业务。最后再多说一个小技巧t3code 这套方案虽然轻但任何自建代码托管服务都意味着你承担了“不可用”的风险。因此生产环境下强烈建议启用定期巡检比如写一个简单脚本检查磁盘使用率、服务和数据库状态配合短信或即时通信告警。这些基础设施层面的功课做得越早后面就越省心。
返回列表