
在 Linux 下搭建 Git 服务器看起来是个十分钟的小任务装个 git建一个裸仓库让别人 clone 和 push。可真到团队日常使用时你会很快发现服务器的价值不是“能跑 git”而是访问稳定不稳定、权限是否可控、数据丢不丢得起。我在不同阶段搭过好几次踩过的坑也大多集中在目录权限、传输协议和备份策略上。这篇文章按实际动手的顺序来写适合小团队、实验环境也适合想先把 Git 服务器原理搞清楚再上生产的人。1. 先想清楚Git 服务器的本质是什么1.1 自建服务器的第一价值是可控性Git 本身是分布式的服务器的仓库并不是“唯一的真相源”但它承担了一个非常重要的角色所有成员之间交换数据的中转站。你今天提交的代码同事拉下去同事的分支你随时可以获取。要让这个中转稳定服务器端至少要有三样东西稳定的网络入口、明确的分支读写权限、以及随时可以恢复的数据副本。自建 Git 服务器本质上是把这三件事的控制权拿回到自己手里。你可以决定仓库放在哪块硬盘上决定谁有读权限、谁有写权限决定备份任务几点运行决定仓库过大时怎么处理。相比第三方托管平台自建的界面可能很朴素功能也不算丰富但自主可控这条优势在内部项目、交付类代码、不能出内网的环境中非常实用。1.2 不是所有场景都值得自建我也见过不少一开始拍板“自己搭一个”的团队最后又默默迁回托管服务。原因不是技术实现不了而是低估了后续运维成本。Linux 服务器需要打安全补丁Git 仓库会越来越大硬盘总有满的一天误删分支需要恢复成员离职要立刻收回权限。这些工作每周都会出现在你的清单里。所以动手之前可以做一次减法如果团队只有三五个人对代码是否留在内网没有硬性要求那直接用现成的代码托管平台更省心。它帮你把账号体系、备份、可用性都处理好了你只需要关心写代码。反过来如果仓库数据不允许出内网或者你需要自定义 hook、控制整条交付链路那自建就非常合理。搞清楚“为什么建”比研究“怎么建”更重要。1.3 先选协议再选工具SSH 还是 HTTP(S)自建 Git 服务器第一步不是装软件而是确定访问协议。协议决定后续的用户模型、权限模型和你需要维护的东西。SSH 协议适合开发机相对固定的团队。每个成员生成一对密钥服务器只认公钥客户端在命令行里操作设置好后几乎无感。它的优点是天然带传输加密、和 Git 命令集成紧密权限可以做到按系统用户隔离。缺点是要管理密钥浏览器里也看不到仓库内容。HTTP(S) 协议适合跨网络访问、需要在浏览器里浏览仓库、或者想用账号密码登录的场景。这类方案走的是 Git 智能 HTTP 协议服务端由 Web 服务器转发请求认证可以用 Basic Auth 或更灵活的方式。它的优点是成员不需要理解密钥缺点是权限扁平化问题更明显后面会展开讲。2. 环境准备一个专用用户和一套清晰的目录2.1 服务器与软件包准备Git 服务器的硬件要求很低。我用 1 核 512MB 内存的虚拟机跑过多仓库的小团队服务日常没有压力只在打包或执行 gc 时 CPU 会升高。真要上生产建议 2 核 2G 起步毕竟内存不仅给 Git 用还要给 SSH 进程和操作系统留余量。操作系统方面服务器尽量选长期维护的稳定版不要为了追新版本而频繁滚动升级。Git 服务的稳定性比版本号重要得多内核更新、SSH 配置迁移这些事越少越好。基础软件包只需要两个Git 和 OpenSSH。很多发行版默认装了 OpenSSH没装的话补一下。这一步别跳过系统更新见过好几次因为老版本组件和新 Git 客户端配合异常导致的传输问题源头都是服务器软件源太久没动。sudo apt update sudo apt install git openssh-server安装完成后先验证 git 版本太老的话建议从官方源或发行版仓库升级不要自己编译。自己编译 Git 不是不行但后续依赖维护很繁琐非特殊需求不推荐。2.2 创建专用的 git 系统用户我强烈不建议用 root 身份跑 Git 服务。Git 仓库一旦被某个漏洞或错误操作影响root 权限会把损失放大到整个系统。正确做法是创建一个专用系统用户所有仓库和备份目录都归它管。创建用户的命令里有一个重要选择把 shell 设成/usr/bin/git-shell而不是/bin/bash。git-shell 是 Git 自带的受限 shell只允许执行git-upload-pack、git-receive-pack这些必要的 Git 命令不允许登录后执行任意命令。这样一个用户即使通过 SSH 成功认证也无法拿到服务器 shell安全边界清晰很多。sudo mkdir -p /srv/git sudo useradd -m -d /srv/git -s /usr/bin/git-shell git如果发行版提示 git-shell 不存在先执行which git-shell确认路径再在/etc/shells里检查是否已登记。把 git-usershell 加入/etc/shells后再修改对应用户的 shell。2.3 目录布局与权限我习惯这样规划目录/srv/git/repos所有裸仓库的根目录/srv/git/backup备份文件输出目录/srv/git/tmp临时中转目录一般用于导入旧仓库目录创建好后注意两层权限外层不能对所有人开放写权限内层仓库要给读写留出合理空间。简单做法如下sudo mkdir -p /srv/git/repos /srv/git/backup /srv/git/tmp sudo chown -R git:git /srv/git sudo chmod 750 /srv/gitchmod 750的意思是属主可以读写执行属组可以读和执行其他人完全不可达。这个设置对 SSH 方案够用。如果后面要让 HTTP 进程读取仓库再单独放开而不是一开始就全局 777。一个非常容易踩的坑是仓库目录如果被创建成 755组内成员还可以读取但没有写权限如果创建成 700连组内成员都进不去。裸仓库创建时用对参数能省掉后续很多麻烦。3. 用 SSH 协议搭一台最小可用的 Git 服务器3.1 创建裸仓库档案室和工作区的区别很多人刚开始都会问为什么初始化 Git 仓库要用--bare普通工作区里有.git目录、有工作文件裸仓库则是把.git目录里的内容直接平铺出来没有工作区文件。可以把裸仓库理解成一个档案室它只负责接收和保管历史版本不负责让你在里面改文件。服务器端的仓库永远使用裸仓库因为没有人应该直接在上面修改文件所有修改都通过 push 提交进来。创建仓库的命令要放在 git 用户下执行而不是 rootsudo -u git git init --bare --sharedgroup /srv/git/repos/demo.git--sharedgroup这个参数会把仓库目录的组权限设置成可读写并且让仓库内的对象和引用权限也按组权限处理。当团队有多个成员通过不同系统用户访问时这个参数能减少很多权限报错。3.2 配置成员公钥成员客户端生成密钥这一步在每个人自己的开发机上执行ssh-keygen -t ed25519 -C alicedev-machine生成后把~/.ssh/id_ed25519.pub的内容发给服务器管理员。管理员把公钥追加到 git 用户的authorized_keys文件里sudo mkdir -p /srv/git/.ssh sudo touch /srv/git/.ssh/authorized_keys sudo chmod 700 /srv/git/.ssh sudo chmod 600 /srv/git/.ssh/authorized_keys sudo chown -R git:git /srv/git/.ssh编辑authorized_keys时每行开头可以加上restrict,no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty这几个限制项。restrict是 SSH 提供的快捷方式一次性关闭不必要的转发功能。因为 Git 操作不需要终端交互也不需要端口转发限制后即使公钥泄露攻击者也很难把它当成跳板。3.3 客户端完成首次克隆与推送全部配置好后客户端这样操作cd ~/work git clone git服务器地址:/srv/git/repos/demo.git cd demo echo # demo README.md git add README.md git commit -m docs: 初始化 readme git branch -M main git push -u origin main第一次 push 时如果仓库刚创建、还没有任何分支Git 可能提示warning:push.defaultunspecified或没有 HEAD 的问题。git branch -M main先把本地分支改成 main再git push -u origin main就能避免这个别扭。服务端用 git-shell 限制后客户端克隆时是看不到欢迎提示的这是正常现象。如果看到fatal: Interactive git shell is not enabled说明有人试图直接登 shell对 Git 协议并没有影响。3.4 服务端验证和查看提交历史服务端想确认 push 成功了不需要也不可能直接切换进裸仓库编辑文件可以用 git 命令直接查看sudo -u git git --git-dir/srv/git/repos/demo.git log --oneline -5 sudo -u git git --git-dir/srv/git/repos/demo.git branch -a这里用--git-dir明确指定仓库路径不需要cd进目录。想查看仓库体积时用du -sh /srv/git/repos/demo.git想查看引用状态用git show-ref。到这一步一个最小可用的 SSH Git 服务器已经跑起来了。可别急着发布下一节要处理多用户和权限这才是团队协作里真正让人头疼的部分。4. 进一步加固多用户、组权限与只读角色4.1 两种用户模型共享账号还是系统账号小团队搭建 Git 服务器通常会在两套方案里纠结共享一个 git 账号还是给每个成员创建系统账号。共享账号方案的好处是管理简单成员公钥都放在 git 用户的authorized_keys里谁加入谁退出只改这个文件。缺点是一旦进入仓库目录所有内容对所有已授权成员一视同仁无法区分谁只能读、谁能写。系统账号方案则是给每个成员单独建一个 Linux 用户再把用户加进同一个组。这样可以按用户的系统身份控制仓库权限但成员在客户端连接时要写自己的用户名和服务器地址例如alice服务器地址而不是git服务器地址。我的建议是如果团队少于十人仓库也不多就先用共享账号别给自己上复杂模型。权限扁平并不是大问题真正的大问题是成员离职时有没有及时把公钥删掉。共享账号方案里删公钥就是删一行非常干脆。4.2 用组权限管理可读写范围如果确实需要系统账号模型可以把仓库组设置为共享组并把成员加进这个组。创建仓库时使用--sharedgroup会让仓库目录的属组变成创建者所在组。把成员账号加入 git 组后他们在仓库目录里就拥有组权限。这样每个成员用自己的账号认证登录 shell 可以限制成 git-shell避免拿到真实 shell。sudo useradd -m -s /usr/bin/git-shell alice sudo usermod -aG git alice这里有一个隐藏问题组成员对仓库目录是可读可写的这意味着任何组内成员都能 push 任何一个仓库。想细分到“仓库 A 只有张三能写仓库 B 只有李四能写”用裸仓库目录权限做会非常痛苦需要用到 ACL 甚至更复杂的中间层。对于小团队我赞成形式简单一点要么所有人都有写权限要么用单独的代码评审流程来控质量而不是在文件权限层面过度设计。4.3 用 git-shell 和 SSH 限制收紧入口单一 git 账号方案里还有一个很多人忽略的收尾工作git 用户的主目录里authorized_keys权限必须是 600.ssh目录必须 700否则 SSH 服务会直接忽略这个文件表现为“明明加了公钥还是提示登录失败”。另外建议在authorized_keys里手动添加限制参数restrict,no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty ssh-ed25519 AAAAC3Nza...restrict的效果相当于同时关闭端口转发、X11 转发、agent 转发和 pty 分配。对于只需要 Git 命令的用户这些功能一个都用不上关了反而安全。如果你有成员反映“push 时看不到进度提示”原因就是no-pty这是完全正常的。最后一层加固是服务器防火墙只开放 22 端口其他端口按需开放。Git 服务本身只依赖 SSH 的 22 端口多开的端口都是额外攻击面。5. 走 HTTP(S) 通道给浏览器用户开一扇门5.1 为什么需要 HTTP(S)SSH 方案对命令行用户很友好但有一个天然短板没法在浏览器里浏览仓库。项目经理想看最新提交、新同事想快速浏览代码结构、或者你在外面用一台没有私钥的电脑这些场景都需要 HTTP(S) 通道。Git 的智能 HTTP 协议没有那么神秘它本质上就是把git-upload-pack和git-receive-pack这两个后端程序暴露给 Web 服务器。客户端发送的请求由 Web 服务器接收调用对应的 CGI 程序返回结果给客户端。实现了这套机制后git clone后面的地址可以从gitserver:...变成http://server/git/demo.git。5.2 Apache git-http-backend 的最小配置以常见的 Apache 为例先安装包并启用 CGI 模块sudo apt install apache2 apache2-utils sudo a2enmod cgi sudo systemctl restart apache2然后创建一个配置文件内容里最关键的是三行仓库根目录、导出全部仓库、以及 CGI 映射SetEnv GIT_PROJECT_ROOT /srv/git/repos SetEnv GIT_HTTP_EXPORT_ALL ScriptAlias /git/ /usr/lib/git-core/git-http-backend/GIT_HTTP_EXPORT_ALL的作用是允许导出所有仓库否则客户端只能看到带git-daemon-export-ok标记文件的仓库这个细节很容易让人排查半天。接着加权限控制。虽然ScriptAlias已经把请求交给了 git-http-backend但还需要对访问/git/的请求做身份验证Location /git Options ExecCGI AuthType Basic AuthName Private Git Server AuthUserFile /etc/apache2/git-htpasswd Require valid-user /Location创建密码文件时htpasswd的-c参数只在第一次新建文件时使用后续添加用户不要带-c否则会覆盖之前所有账号sudo htpasswd -c /etc/apache2/git-htpasswd alice sudo htpasswd /etc/apache2/git-htpasswd bob这里还有个权限细节点HTTP 进程默认以www-data用户运行如果仓库目录是git:git 750HTTP 进程连目录都进不去。最简单的办法是给目录加其他用户的读取权限sudo chmod ox /srv/git /srv/git/repos sudo chmod -R or /srv/git/repos如果还要允许 HTTP 方式 push那必须给www-data写权限这会让权限模型一下子扁平化。我的建议是 DIY 场景下 HTTP 通道优先定位成只读浏览需要多人写入时直接启用更完整的 Web 管理方案。5.3 加上 HTTPS 证书不加密的 HTTP 会把 Git 仓库内容明文传输任何人都能抓包看到代码这个基本安全意识不能少。正式环境优先使用受信任的证书签发机构内部测试可以用自签名证书。自签名证书生成方法sudo openssl req -x509 -nodes -days 365 -newkey rsa:2048 \ -keyout /etc/ssl/private/git-self.key \ -out /etc/ssl/certs/git-self.crt \ -subj /CNgit.internal.invalid然后在 Apache 的 SSL 虚拟主机里引用这两个文件。自签名证书的问题是客户端不信任它克隆时 Git 会报证书校验失败。最简单的处理方式是让客户端在本地信任该证书而不是关掉校验。直接设置http.sslVerifyfalse会让整个连接失去完整性校验我不推荐使用。5.4 HTTP 方案的边界在哪里如果团队需求是“多个仓库、每个仓库不同角色的人有不同的读写权限”那就不要指望上面这套简易 HTTP 方案了。git-http-backend本身不管理每个仓库的细分权限它最多配合 Web 服务器的认证模块做全局控制。更合理的路径有两个一是继续用 SSH 方案配合系统用户权限二是部署一套带 Web 界面的代码托管系统它有内置的用户角色、分支保护和仓库级权限。自建时先想清楚权限颗粒度做到哪一层再决定要不要上重型方案否则很容易陷入“能跑但没法维护”的窘境。6. hook 与自动化提交后即时响应6.1 post-receive 能做的几件事Git 服务器的威力有一部分体现在 hook 上。hook 是服务器仓库目录下的一组可执行脚本在特定动作发生时被 Git 主动调用。对服务器运维最有用的是pre-receive和post-receive。pre-receive在对象写入仓库之前执行适合做校验。比如检查提交信息是否包含指定编号、检查正在推送的分支是否被保护、检查二进制文件是否误入库。一旦脚本返回非零状态整个推送会被拒绝客户端会看到报错信息。post-receive在对象写入成功后执行适合做通知和联动。比如推送完成后自动触发构建、发送站内通知、备份到另一个存储、或者同步到测试服务器。6.2 一个简单的通知 hook 示例以post-receive为例脚本内容如下#!/usr/bin/env bash # 位于 /srv/git/repos/demo.git/hooks/post-receive repo_namedemo while read oldrev newrev ref; do case $ref in refs/heads/main) echo main 分支收到更新可以触发后续动作 # curl -sS -X POST http://your-server/api/notify \ # -d repo$repo_namenewrev$newrev ;; esac done脚本开头最好加上#!/usr/bin/env bash并设置执行权限sudo chmod x /srv/git/repos/demo.git/hooks/post-receivehook 里的环境变量和交互式 shell 不一样。Git 在调用 hook 时不会自动加载/etc/profile所以脚本里用到的命令尽量写绝对路径或者像示例那样在脚本开头显式导出 PATH。否则 cron 或 SSH 环境里正常执行的命令在 hook 里可能直接command not found。6.3 hook 调试要注意的环境变量调试 hook 时最直观的方式是在脚本里加echo。因为 hook 的输出会回传给客户端所以你先在本地 push再从客户端看输出就能定位问题。有个隐藏的坑是 hook 的当前工作目录。当 Git 调用 hook 时脚本会在裸仓库根目录下运行所以脚本里写相对路径很容易踩错。建议脚本开头就确定一个绝对路径作为项目根再把后续操作都基于它展开。hook 的执行用户是谁如果是通过 SSH 访问那么执行用户是发起 SSH 认证的那个用户如果是共享 git 账号那就是 git 用户。这决定了 hook 里写入备份目录或其他路径时会不会遇到权限不足的问题。7. 备份、恢复与仓库维护7.1 备份裸仓库而不是工作区很多新手备份 Git 服务器时习惯性以为要备份整个源码目录。恰恰相反服务器上真正有用的只有裸仓库目录因为它包含了完整的历史、所有分支、所有 tag而没有任何临时工作文件。如果备份时把工作区文件也带上既占空间又容易混入编译产物。裸仓库是一堆对象文件和引用文件正常情况下 rsync 或 tar 都能直接归档。唯一需要留意的是备份过程中仓库可能在写入所以不要让备份任务和高峰提交时间重叠。实在要稳妥可以先把仓库打成 bundle再对 bundle 备份。对单仓库生成 bundle 的示例sudo -u git git --git-dir/srv/git/repos/demo.git bundle create /srv/git/backup/demo_$(date %F).bundle --allbundle 文件是一个打包好的 Git 仓库内容恢复时直接把它当作远端来克隆即可。这个方法适合冷备份、本机迁移、或者把仓库快递到另一台机器。7.2 两套可落地的备份方案第一套是本地定时脚本。每天凌晨把裸仓库目录 rsync 到另一块磁盘或另一台机器rsync -av --delete /srv/git/repos/ /backup/repos/因为裸仓库目录结构简单rsync 增量同步效率很高而且--delete能帮我们清理已删除的仓库。缺点是没有做版本化误删只能恢复到最近一次同步的状态。第二套是 git bundle 加保留多天历史。每天生成一个 bundle定期清理五天前的旧文件。这套的优点是恢复时能精确到某个备份时间点适合对历史提交有较强保留需求的场景。实际生产中很多团队两套都跑rsync 做快速恢复bundle 做历史归档。无论用哪套我都建议每个月做一次真实的恢复演练。不是验证“备份文件在不在”而是验证“能不能一分钟内把仓库恢复到可用状态”。真等服务器硬盘坏了再做恢复那就晚了。7.3 常规维护与磁盘检查Git 服务器日常运维量不大但有几件小事要固定下来。第一是定期 gc。裸仓库在多次 push 后对象库会产生冗余执行git gc --auto可以自动清理。不要随意对仓库使用git gc --aggressive这个操作会重写所有对象很吃 CPU而且效果并不一定比普通 gc 好。第二是检查磁盘空间。仓库增长往往比预期快尤其是有人误提交了编译产物或二进制包。发现某个仓库体积异常后第一步先找出占空间的大对象再判断是否需要从历史中清理。改历史是可逆性很差的操作不要在生产仓库上随便用。第三是关注文件和目录权限变化。比如有没有人把仓库目录改成 777有没有非 git 用户出现在仓库属主里。权限松散是许多事故的开始每周花一分钟检查一下比事后处理简单得多。8. 常见问题排查实录8.1 克隆时提示 Permission denied这是 SSH 方案里最常见的报错。排查顺序是固定的先看认证再看文件权限。认证层面先确认客户端连接时是不是用的 git 用户ssh -T git服务器地址如果返回Permission denied (publickey)说明公钥没被接受。这时服务端要检查authorized_keys的权限.ssh目录必须是 700authorized_keys必须是 600两者都不能被其他用户修改。这算是最容易被忽略的一个点目录权限一旦过宽SSH 为安全起见会忽略公钥文件。文件权限层面确认 git 用户对/srv/git目录有执行权限而且仓库目录的属主不是某个普通用户。用sudo -u git git --git-dir仓库路径 branch测一下如果服务端本地都读不了那就不是客户端的问题。8.2 推送时报 RPC failed / 413 / 502HTTP 方案里推送大仓库或大量对象时可能出现RPC failed、413 Request Entity Too Large或502 Bad Gateway。原因通常是 Web 服务器限制了请求体大小或者是代理缓冲区太小。对 Apache 来说可以在配置里加大请求体限制LimitRequestBody 0对反向代理模式更大的可能性是缓冲超时或缓冲大小不足。一般把代理的 client_max_body_size 和 proxy buffer 调大后能解决。另外客户端也可以配合调整 http.postBuffergit config --global http.postBuffer 524288000这个值是客户端发送请求时使用的缓冲上限很多“大包推送失败”的问题靠调大它就能缓解。不过这只是治标如果仓库长期很大最该做的还是把大文件从仓库里清出去。8.3 分支误删后如何找回服务器上误删分支恢复手段主要靠 reflog 和 fsck。裸仓库同样有 reflog只要分支在最近一段时间内存在过可以用sudo -u git git --git-dir/srv/git/repos/demo.git reflog找到对应的 commit hash 后再新建分支指过去sudo -u git git --git-dir/srv/git/repos/demo.git branch recover-branch commit-hash如果连 reflog 都丢了那就用git fsck --lost-found扫描所有悬空对象从输出里找最近的 commit。这个操作不一定每次都成功但比直接放弃强很多。8.4 日常检查清单我给自己定过一个简单检查清单大概是这样每周确认备份任务成功执行并随机找一个仓库试克隆。每周看一次磁盘空间仓库根目录超过警戒线就排查大对象。每次成员变动立刻增删公钥或账号。每月跑一次整体恢复演练至少验证一个仓库能从备份恢复。每季度检查系统安全更新并重启一次服务确认无异常。这套清单不重但能覆盖 Git 服务器 90% 的常见风险点。真等仓库丢了、硬盘满了、权限泄露了才发现那就不是几分钟能解决的事了。9. 最后再分享几条经验搭建 Git 服务器这件事技术门槛真不高高的是后续坚持维护。我个人最深的体会是一开始不要追求功能全先保证“成员能稳定 push、备份能恢复、权限能收回”这三条底线剩下的都可以往后放。很多团队一开始就上了复杂权限模型结果日常管理成本失控最后又退回最简单的共享账号方案。第二点经验是换个协议等于换一套运维方法。SSH 方案认证简单但公钥管理要跟上HTTP 方案能看网页但权限扁平化问题更明显。不要换来换去选一个适合团队习惯的持续用下去比反复横跳更重要。最后想说的是Git 服务器本质上是个很“安静”的服务稳定运行几天甚至几周都感觉不到它的存在。这恰恰是它最该有的状态。真正的好方案不是功能列表有多齐全而是它在你不需要想起它的时候不添麻烦。希望这篇记录能帮你少踩一些我自己踩过的坑也欢迎在实践中根据你们的团队规模、网络环境和安全要求对方案做出更合理的取舍。