很多团队用 Git 的方式是:本地写代码,push 到某个公有代码托管平台,然后服务器上再 pull 下来。平时没问题,但一旦遇到网络波动、平台调整服务条款、仓库需要放内部资料、或者公司要求代码不出内网,就会开始想"我是不是该自己搭一个 Git 服务器"。这件事听起来很重,其实分层看非常清楚:最简的一种,装完 Git 敲两行命令就能用;最重的一种,是一整套带网页界面、合并请求、CI 的协作平台。两者的差距不在难度,而在你要不要那些附加功能。
这篇文章会把三条路线摆在一起对比,然后从零开始带你搭一个最小可用的 Git 服务器:专用系统用户、裸仓库、SSH 公钥认证、权限边界。之后讲清楚团队协作时的权限模型,再用 Git Hooks 做一次自动部署的引子,最后说备份。全程给 RHEL/CentOS/Rocky 与 Debian/Ubuntu 两套命令,涉及删除和目录改动的操作会标风险。
读完你应该能自己判断:我这个场景该用裸仓库、Gitea 还是 GitLab;团队新人该怎么加;服务器被误删代码后怎么恢复。
一、先想清楚要解决什么问题
自建 Git 服务器最常见的三种诉求:一是代码必须留在内网,不接受托管到外部平台;二是团队规模小,只需要一个能 push/pull 的中转站,不需要网页界面;三是想要一套完整的协作平台,有账号体系、代码评审、议题跟踪,甚至顺带把 CI 也跑了。三种诉求对应三种方案,选错了就是用小推车干重卡的活,或者用重卡拉一个人。
下面这张表是三条路线的取舍,是我在中小团队里见过比较靠谱的结论:
| 方案 | 部署成本 | 网页界面 | 权限粒度 | 适合规模 | 典型维护负担 |
|---|---|---|---|---|---|
| 裸仓库 + SSH | 10 分钟 | 无 | 基于系统用户/文件权限 | 1-5 人 | 几乎为零 |
| Gitea / Gogs | 半小时 | 有 | 组织/团队/仓库三级 | 5-50 人 | 低,单二进制 + 数据库 |
| GitLab CE | 半天起步 | 完整 | 非常细 | 20 人以上 | 高,吃内存、升级需谨慎 |
一个经验量级:Gitea 跑在 1 核 1G 的机器上就够用几十个人,而 GitLab CE 官方建议至少 4 核 8G,实际生产里 8 核 16G 才比较舒服,内存是它最大的门槛。所以如果你的团队不到十个人,又只是想要个放代码的地方,别一上来就上 GitLab,先把裸仓库跑通,以后真需要网页界面再换 Gitea,迁移成本很低——Git 仓库本身就是个目录,直接拷过去就行。
需要提醒的是,很多人对"自建"有个误解,以为自建就一定要有网页,其实裸仓库的核心价值是"一个所有机器都能访问的权威仓库",客户端体验和用公有平台几乎一样:clone、commit、push、pull,一条命令都不少,只是没有网页看提交历史而已,用 git log --graph --oneline --all 一样能看。
二、用裸仓库搭一个最小 Git 服务器
裸仓库(bare repository)是理解一切的基础。普通的 git init 会生成一个带工作区的仓库,里面既有 .git 目录,又有你实际能编辑的文件;而 git init --bare 只生成 .git 目录里的那些东西,没有工作区。原因很直接:服务器上没人会在仓库目录里直接改文件,它只需要存提交对象、分支引用这些元数据,不需要一份可编辑的副本。如果你在服务器上用普通仓库当共享仓库,别人推送时会报错,因为 Git 会拒绝更新当前检出的分支。
第一步:确认 Git 已安装
# RHEL / CentOS 7 及以上、Rocky / AlmaLinux
sudo yum install -y git # CentOS 7 用 yum
sudo dnf install -y git # CentOS 8+ / Rocky / AlmaLinux 用 dnf
# Debian / Ubuntu
sudo apt update
sudo apt install -y git
# 验证版本,2.x 都够用,越新越好
git --version
第二步:创建专用系统用户
不要让 Git 仓库挂在 root 名下,也不要挂在日常登录账号下。专用用户的好处是权限边界清晰,将来做目录隔离、备份、审计都方便。
# 创建 git 用户,-m 建家目录,-s 指定登录 shell
sudo useradd -m -s /bin/bash git
sudo passwd git # 可选,设置一个强密码
风险提示:有些教程会让你把 shell 设成 /sbin/nologin,但对需要 SSH 推送的场景这会导致认证失败,因为 SSH 连接后无法执行命令。正确做法是用 /bin/bash 或 git-shell(后面第四节的加固方案)。
第三步:初始化裸仓库
# 切到 git 用户身份,避免目录属主混乱
sudo su - git
# 建一个统一存放仓库的目录,命名 .git 是惯例,也可以叫 repos
mkdir -p ~/repos/demo.git
cd ~/repos/demo.git
# 关键:--bare 表示不创建工作区
git init --bare
# 退出 git 用户
exit
目录名以 .git 结尾只是社区惯例,方便一眼认出这是裸仓库,不加也能用。但这个惯例很有价值,尤其在有多个仓库时,能避免误操作。
第四步:客户端克隆并首次推送
# 在你自己的开发机上
git clone git@git.example.com:/home/git/repos/demo.git
# 如果服务器 SSH 端口不是 22,比如改成了 2222
git clone ssh://git@git.example.com:2222/home/git/repos/demo.git
cd demo
echo "# demo" > README.md
git add README.md
git commit -m "init"
git push origin master # 新版本 Git 默认分支可能是 main,按提示来
推成功后,服务器上 ~/repos/demo.git 里会出现 objects、refs 等目录,refs/heads/master 指向最新的提交。到这里,一个能用的 Git 服务器就完成了,整个过程不超过十分钟。
三、SSH 公钥认证:免密与安全的基础
生产环境绝对不建议用密码推送 Git。密码认证有两个问题:一是脚本化时无法自动化,二是容易被暴力破解。SSH 公钥认证既解决免密,又比密码安全得多。
生成密钥对
# 在开发机上,-t 指定算法,-C 是备注,建议用邮箱或机器名
ssh-keygen -t ed25519 -C "dev@example.com"
# 老一些的系统或需要兼容旧设备时用 RSA 4096
# ssh-keygen -t rsa -b 4096 -C "dev@example.com"
# 一路回车,密钥默认放在 ~/.ssh/id_ed25519 和 ~/.ssh/id_ed25519.pub
# 想更安全可以给私钥设一个口令(passphrase),配合 ssh-agent 免重复输入
私钥文件(无 .pub 后缀的那个)就是你的身份凭证,绝对不能外传、不能提交进仓库。公钥(.pub 结尾)是可以随便给的。
把公钥装到服务器
# 方法一:用 ssh-copy-id,最省事
ssh-copy-id -i ~/.ssh/id_ed25519.pub git@git.example.com
# 方法二:手动追加(服务器上、git 用户身份)
sudo su - git
mkdir -p ~/.ssh
chmod 700 ~/.ssh
echo "ssh-ed25519 AAAA...你的公钥内容... dev@example.com" >> ~/.ssh/authorized_keys
chmod 600 ~/.ssh/authorized_keys
权限是这里最大的坑。SSH 对权限非常敏感:~/.ssh 必须是 700,authorized_keys 必须是 600,家目录不能对 group 和 other 可写。任何一项不对,SSH 会直接忽略这个文件,你会看到"仍然要求输密码"或者"Permission denied (publickey)",而且不给任何明确提示。
验证与排障
# 详细模式看认证过程中每一步,-v 不够就 -vvv
ssh -v git@git.example.com
# 测试具体能用哪个密钥,-i 指定私钥
ssh -i ~/.ssh/id_ed25519 -T git@git.example.com
常见的失败原因有三个:私钥权限太开放(必须是 600,ssh 会拒绝加载),服务器家目录属主不对(必须属于 git 用户),SELinux 上下文不对(RHEL 系特有,用 restorecon -Rv ~/.ssh 修复)。
四、权限与团队协作模型
裸仓库自带的权限模型很粗:能 SSH 登录这个用户,就能读写这个用户下所有仓库。这个模型对小团队够用,人多了就得升级。
从粗到细的三种模型
第一种,所有人共用 git 用户。加人就是把公钥追加到 authorized_keys,新人的所有提交都会以 git 这个身份出现,git log 里看不出是谁干的。这种模型适合 1-3 人的内部小项目,不建议再大。
第二种,一人一系统用户,外加一个共享用户组。给每个人建账号,把仓库目录的属组设为一个专用组,chmod 2775 让新建文件自动继承属组,再把每个人加进这个组。这样日志能看出人,权限也能按组控制,但仓库级隔离做不到——同组的人对所有仓库都是读写。
# 建立 devs 组
sudo groupadd devs
sudo usermod -aG devs alice
sudo usermod -aG devs bob
# 仓库目录属组交给 devs,并让新文件继承组
sudo chown -R git:devs /home/git/repos
sudo chmod -R 2775 /home/git/repos
# 注意:修改权限前先备份,且要确保原有属主不会因此丢失
风险提示:chmod -R 作用范围很大,执行前务必用 ls -l 确认目录内容,并想清楚是不是真的要递归改。如果改错了,用 sudo chown -R git:git /home/git/repos 可以退回原状。
第三种,上 Gitea 或 GitLab,用它们自带的组织、团队、仓库三级权限。能做到"这个仓库 alice 只能读,bob 能写,外部协作者只能提合并请求"。这是人多了以后的必然选择,因为手工维护文件权限到十几个人的规模就崩了。
用 git-shell 加固 SSH 登录
裸仓库方案有个隐患:能 SSH 登录的账号,理论上可以做任何事,包括在服务器上执行任意命令。如果只是想让对方推送代码,可以把这个账号的 shell 限制成 git-shell,它只允许执行 Git 相关的少数命令,其他一律拒绝。
# 确认 git-shell 路径
which git-shell # 通常 /usr/bin/git-shell
# 把 git 用户的 shell 改掉
sudo usermod -s /usr/bin/git-shell git
# 注意:改完之后你自己也无法用这个账号正常登录做维护了
# 需要维护时临时改回 /bin/bash,或用 root 直接操作
这里有个取舍:git-shell 提升了安全性,但牺牲了可维护性。多数中小团队的做法是保持 /bin/bash,同时把 SSH 的密码登录彻底关掉,只允许公钥,并把 authorized_keys 管理好。这样攻击面已经足够小。
分支保护与协作规范
Git 服务器本身不强制流程,流程靠约定和权限。最常见的约定是:master(或 main)只接受合并请求,不接受直接推送;开发在 feature/* 分支;合并前必须通过评审和自动检查。裸仓库无法技术性阻止直推(除非用钩子),但可以用 pre-receive 钩子拦截,这个在下一节讲。
五、用 Git Hooks 做自动部署
Git Hooks 是仓库里的一批脚本,在特定动作发生时自动执行,位于仓库的 hooks 目录下。对服务端来说,最有价值的是这三个:pre-receive 在收到推送但还没写入前执行,可以拒绝这次推送;update 针对每个分支执行;post-receive 在所有引用更新完成后执行,是触发自动部署的标准位置。
一个最小可用的 post-receive 长这样:
# 在服务器上 /home/git/repos/demo.git/hooks/post-receive
#!/bin/bash
# 部署目标目录
TARGET=/var/www/demo
# 用一个独立的工作区做检出,避免和裸仓库混用
GIT_WORK_TREE=$TARGET GIT_DIR=/home/git/repos/demo.git \
git checkout -f master
# 赋予执行权限
chmod +x /home/git/repos/demo.git/hooks/post-receive
这段脚本的作用是:每次有人推送 master,服务器就把最新代码强制检出到 /var/www/demo。这就是"推送即部署"的原型。真实生产里还要加上构建步骤(编译、装依赖)、重启服务、失败回滚、并发加锁,而且要注意执行钩子的身份是 git 用户,它对 /var/www/demo 必须可写,环境变量与平时登录时也不同。这些坑在 Webhook 与自动发布那篇里会详细展开,这里先埋个引子。
一个必须知道的事实:post-receive 是同步执行的,如果脚本里跑了十分钟的构建,推送方的 git push 就会卡十分钟。所以正规做法是让钩子只负责触发(比如调用一个后台任务),真正的部署逻辑异步跑,同时用日志记录结果。
六、备份:最容易忽略的一环
自建 Git 服务器意味着代码的唯一权威副本在你手上,备份不是可选项。好消息是 Git 备份很简单,因为仓库本身就是一个自包含目录。
# 冷备份:直接打包整个 repos 目录
sudo tar czf /backup/git-repos-$(date +%F).tar.gz /home/git/repos
# 更稳妥的方式:用 git clone --mirror 做镜像仓库
git clone --mirror /home/git/repos/demo.git /backup/mirror/demo.git
# 之后增量同步
cd /backup/mirror/demo.git && git remote update
两种方式各有用途。tar 打包快、能保留全部状态(包括 hooks),但不便增量;--mirror 是 Git 层面的完整镜像,包含所有分支和标签,可以随时恢复成新的裸仓库,代价是钩子不会跟着走,得单独备份 hooks 目录。
建议的节奏是:每天一次 --mirror 同步到另一台机器或对象存储,每周一次完整 tar 归档并保留若干份。恢复演练每季度做一次——只在出事时才发现备份是坏的,是最典型的运维事故。
常见问题
Q:客户端 clone 时提示 "Permission denied (publickey)",怎么查?
先确认三件事:公钥是否真的写进了 authorized_keys、私钥文件权限是不是 600、服务器上 ~/.ssh 是不是 700 且属主正确。RHEL 系还要检查 SELinux 上下文。用 ssh -vvv 能看到协商细节。
Q:服务器重启后 SSH 变慢,登录要等好几秒?
多半是 DNS 反解超时。在 /etc/ssh/sshd_config 里把 UseDNS no 打开,重启 sshd 生效。注意改 sshd 配置前先保留一个已登录的会话,改错了不至于被锁在外面。
Q:可以直接在裸仓库目录里改文件吗?
不可以。裸仓库没有工作区,你看到的都是 Git 内部对象,改了对代码没有任何意义,还可能损坏仓库。要改代码就 clone 到本地改再推。
Q:几个人共用 git 用户,怎么知道是谁推的?
git log 里的作者信息来自客户端的 user.name 和 user.email,是可以随意设置的,不具备可信性。要做可信审计就得一人一账号,并开启 git log --format=%H %ae 配合服务器端记录。这也是一人一账号模型的主要理由之一。
Q:仓库越来越大,服务器磁盘吃紧怎么办?
先看是什么占空间:git count-objects -vH 能看到松散对象和包文件的大小。大文件历史要用 git filter-repo 清理,但这会重写历史,所有协作者必须重新克隆,属于高风险操作,务必先做完整备份再动手。
总结
自建 Git 服务器没有想象中复杂,关键是把需求分层。十人以内的团队,一个裸仓库加 SSH 公钥认证,十分钟能跑起来,维护成本几乎为零;需要网页界面和细粒度权限时,换 Gitea 是性价比最高的选择;人再多、流程再重,才轮到 GitLab。
具体落地时记住四条:第一,用专用系统用户托管仓库,别用 root;第二,公钥认证替代密码登录,并把 ~/.ssh 和 authorized_keys 的权限管死;第三,权限模型要和团队规模匹配,小团队别过度设计,大团队别硬撑手工权限;第四,备份是刚需,--mirror 加定期完整归档,并且要真正演练恢复。
至于自动部署,先从 post-receive 钩子建立直觉,但不要把它当成最终方案,它同步阻塞、身份受限、缺乏回滚,只适合极简场景。真正生产级的自动发布,应该走独立的触发机制加可回滚的部署脚本。
企业QQ咨询




