
1. 为什么自建Git仓库而不是直接用Gitee或GitHub很多人听到在服务器上搭建Git仓库第一反应是现在Gitee、GitHub这么方便为什么还要自己折腾这个问题我一开始也觉得犯不上直到真正工作中遇到了几个场景。第一个场景是内网环境。公司的研发网和公网隔离代码出不去也进不来但团队又想享受版本管理、分支合并这些Git带来的好处这时候本地服务器就是唯一选择。第二个场景是私有仓库的代码量敏感。有些项目连私有托管平台都不想用比如涉及客户数据的处理脚本放哪里都觉得不踏实自己服务器上划一块地方存权限捏在自己手里。第三个场景是调试和自动化。自建仓库配合Hook可以做很多事情——push完代码自动部署到测试环境自动打标签触发构建这在很多半自动化的团队里非常实用。那Ubuntu是不是唯一选择当然不是CentOS、Debian都能干这活但Ubuntu胜在两个点一是用户基数大遇到问题搜索解决方案几乎一查一个准二是软件包版本比较新Git官方维护的PPA也更新及时。我用的服务器是Ubuntu 22.04 LTS下面所有操作都基于这个版本如果你是20.04或者24.04命令基本通用个别差异我会在文中指出来。还要说明一点这篇文章不是让你在服务器上把GitLab全家桶装一遍。就我个人经验80%的自建仓库场景根本用不到GitLab那么重的方案。一个裸仓库bare repository加SSH访问就能解决绝大多数需求。装GitLab那个内存占用我自己在2GB的小服务器上试过跑起来卡得不行后来老老实实换回裸仓库方案省心太多。所以这篇文章的路线是服务器端怎么初始化环境、怎么建仓库、怎么配权限然后是客户端怎么连接、日常提交的完整流程最后是我实际使用中踩过的一些坑和几个让仓库更好用的进阶操作。无论你是个人开发者想把代码备份到自己的服务器还是小团队内网协作照着走一遍都能跑通。2. 服务器端准备从SSH到Git安装2.1 第一步先搞定SSH连接搭建Git仓库的前提是你能顺畅地操作这台服务器。我见过不少新手卡在这一步系统装了Git也装了但本地连不上服务器后面全白搭。Ubuntu服务器装好之后默认是开了SSH服务的。如果你装的是Desktop版本那默认还真不一定装openssh-server需要手动确认一下sudo apt update sudo apt install openssh-server -y sudo systemctl status ssh看到active (running)就说明SSH服务在跑。然后在本机测试连接ssh 用户名服务器IP这里要提醒一个细节root用户直接用密码登录在很多Ubuntu版本上默认是禁止的。我在配置服务器时通常会创建一个专门用户来管理Git仓库而不是用root。原因后面讲权限的时候会细说。2.2 用SSH密钥免密登录搭建Git仓库之后你会频繁地push/pull代码如果每次连接都输密码体验很差。更关键的是用密钥认证比密码登录安全得多——私钥留本地公钥放服务器别人无法通过暴力破解密码闯进来。生成本机的SSH密钥对ssh-keygen -t ed25519 -C 你的注释比如邮箱或机器名现在新机器我基本用ed25519算法比传统的RSA 2048短且安全。如果你的Git服务器比较老或者客户端环境特殊再用RSAssh-keygen -t rsa -b 4096 -C 备注生成的公钥在~/.ssh/id_ed25519.pub私钥在~/.ssh/id_ed25519。把公钥内容追加到服务器的授权列表ssh-copy-id 用户名服务器IP或者手动把公钥内容追加到服务器上~/.ssh/authorized_keys文件里。加完之后测试ssh 用户名服务器IP如果能直接登上去不用输密码密钥认证就配好了。2.3 安装Git并验证版本接下来在服务器上装Git。Ubuntu的默认源里就有Git直接sudo apt update sudo apt install git -y git --versionUbuntu 22.04默认源的Git版本大概是2.34日常用完全够了。如果你需要更新版本可以用Git官方维护的PPAsudo add-apt-repository ppa:git-core/ppa sudo apt update sudo apt install git -y装完后顺手配置一下服务器端Git的基本信息。这个配置其实主要用于git commit时记录作者信息虽然服务器端很少直接提交代码但建议还是配上sudo -u git git config --global user.name git sudo -u git git config --global user.email git服务器IP这里的git用户后面会详细介绍。3. 创建Git仓库选择目录结构与初始化方式3.1 为什么推荐创建专门的git用户在服务器上管理Git仓库我强烈建议创建一个独立的系统用户比如就叫git。这么做有几个好处权限隔离。仓库文件归git用户所有其他普通用户没有直接读写权限只能通过Git协议访问。方便管理。所有仓库集中在/home/git或/srv/git下一目了然。安全。即使某个Web应用被攻破攻击者拿到的也不是root权限无法直接篡改仓库。创建用户sudo adduser --system --shell /usr/bin/git-shell --group git这里用--shell /usr/bin/git-shell是一个安全增强。Git官方自带一个git-shell它限制用户只能执行Git相关命令不能登录服务器执行任意Shell命令。如果用户不小心公钥泄露攻击者也没法通过这个账号拿到Shell。不过需要确认git-shell存在which git-shell如果路径不对可以手动指定一下或者先不给--shell参数创建成功后再改sudo usermod -s /usr/bin/git-shell git3.2 初始化裸仓库这是整个搭建过程最关键的一步。先解释一下什么叫裸仓库。普通的仓库有一个工作目录你可以看到文件、修改文件而裸仓库没有工作目录它只保存Git的版本历史数据相当于一个纯服务器端的仓库。我们push到服务器上的就应该是裸仓库因为服务器不需要直接在这个目录里改文件。创建裸仓库的命令sudo mkdir -p /srv/git sudo chown git:git /srv/git sudo -u git git init --bare /srv/git/awesome-project.git注意仓库命名约定通常以.git结尾这样一看就知道是裸仓库。--bare参数就是创建裸仓库的意思。创建完可以在本地验证一下ls /srv/git/awesome-project.git你会看到HEAD、branches、config、objects、refs这些目录和文件。这就是一个最小的可用Git服务端。3.3 分支与HEAD设置裸仓库默认的分支通常叫master现在Git社区普遍用main。我自己更喜欢把新仓库默认分支设为main避免后面团队开发时产生分支命名混乱。设置方法sudo -u git git --git-dir/srv/git/awesome-project.git symbolic-ref HEAD refs/heads/main这行命令的作用是把裸仓库的HEAD指针指向main分支。如果设置了这一步客户端第一次clone下来时默认分支就是main。另外如果你希望服务器端仓库允许接收任何分支的推送保持默认就好。如果希望固定分支可以配置config文件。不过对小型团队来说保留默认行为更灵活。3.4 仓库的目录规划建议如果团队项目多建议按一定的目录结构组织仓库。我的习惯是这样/srv/git/ ├── awesome-project.git ├── backend-service.git └── docs-site.git每个项目一个裸仓库互不干扰。配合后面讲的gitolite或gitea可以做到更细的权限控制但对大多数人来说裸仓库加SSH已经足够了。4. 本地开发流程Clone、Commit、Push与分支管理4.1 从服务器克隆仓库服务器端配好了现在切到自己的开发机。如果你是从零开始一个新项目先在Gitee或者GitHub上建好项目也行但我们要的是走自己的服务器所以直接在本地初始化然后推送到远程。本地初始化mkdir awesome-project cd awesome-project git init -b main echo # awesome-project README.md git add README.md git commit -m initial commit然后添加远程仓库地址git remote add origin git服务器IP:/srv/git/awesome-project.git这里的git服务器IP表示以git用户的身份连接服务器路径是/srv/git/awesome-project.git。第一次连接SSH可能会提示确认服务器指纹输入yes即可。推送git push -u origin main-u参数设置上游分支以后直接git push和git pull就可以不用每次带远程名和分支名。如果仓库里已经有代码了可以用clone的方式git clone git服务器IP:/srv/git/awesome-project.git4.2 日常开发循环add、commit、push这是Git使用频率最高的三个命令。我见过很多初学者把顺序搞混其实逻辑很简单git status查看当前状态看哪些文件改了。git add把改动加入暂存区。git commit把暂存区的内容提交到本地仓库。git push把本地提交推送到远程服务器。实际命令git status git add index.html git commit -m 更新首页标题 git push关于写commit message我的建议是不要用update、fix这种含混的标题尽量写清楚这次改了什么、为什么要改。比如修复登录页在Safari下布局错乱的问题就比fix bug有价值得多。一个好的commit历史可以在项目出问题时快速定位回滚点。4.3 分支管理从创建到合并几乎每个团队都会有主分支保持稳定新功能在分支上开发的需求。Git分支的开销非常小创建合并都很方便。创建并切换到新分支git checkout -b feature/login-page等价于两条命令git branch feature/login-page git checkout feature/login-page在新的分支上开发完成后合并回主分支git checkout main git pull git merge feature/login-page git push如果合并时有冲突Git会在冲突文件里标记出和的区域你需要手动解决这些冲突再add、commit。这里有个技巧合并前先git pull把自己本地仓库更新到最新能减少很多冲突。4.4 拉取远程更新pull和fetch的区别git pull和git fetch经常有人搞混。简单说git fetch只把远程的更新下载到本地但不合并到你当前的分支。适合先看看别人改了什么再决定怎么处理。git pull等于git fetch加git merge直接拉取并合并到当前分支。日常开发中用pull更省事。不过git pull在某些场景下会产生意外的合并提交。如果团队协作频繁我建议用git pull --rebase它会把你的本地提交重放到远程分支的最新提交之上历史更线性看起来也更清爽。5. 多用户协同时的权限配置与常见报错5.1 多用户共用git账号的权限方案前面创建的git用户可以当作统一的Git访问入口。多个人要使用同一台服务器时最简单的方式是把每个人的SSH公钥都添加到git用户的authorized_keys文件里。具体做法是收集每个开发者的id_ed25519.pub公钥内容然后追加到服务器上/home/git/.ssh/authorized_keyssudo -u git mkdir -p /home/git/.ssh sudo -u git touch /home/git/.ssh/authorized_keys # 编辑文件把每个开发者的公钥逐行加入 sudo -u git vim /home/git/.ssh/authorized_keys这样所有开发者的push都会以git用户身份操作。优点是配置简单缺点是无法区分是谁提交的。但这个局限可以通过约束commit信息来解决也可以在服务端配gitolite做更细粒度的权限管理。如果你需要仓库级别的权限控制比如A只能访问项目AB只能访问项目B那gitolite是轻量级的好选择。它基于git用户再包一层权限控制逻辑用配置文件管理用户与仓库的访问关系。比GitLab轻太多适合小团队。不过配置有学习成本这篇文章先不多展开基础的裸仓库方案已经能解决大部分问题。5.2 推送失败Permission denied (publickey)这是新手最常见的问题。执行git push时提示Permission denied (publickey). fatal: Could not read from remote repository排查步骤先确认本机的公钥有没有加到服务器的authorized_keys里ssh -T git服务器IP。如果显示Welcome to Git或Hi说明认证通过。确认本机当前使用的SSH密钥是不是你添加的那把。有时候本机有多个密钥而SSH默认用的不是你加的那把。可以在~/.ssh/config里指定Host mygit-server HostName 服务器IP User git IdentityFile ~/.ssh/id_ed25519确认服务器上authorized_keys文件的权限。这个文件的属主必须是你连接的用户且权限不能太宽松sudo chown git:git /home/git/.ssh/authorized_keys sudo chmod 600 /home/git/.ssh/authorized_keys sudo chmod 700 /home/git/.ssh5.3 推送到非裸仓库时报错如果你初始化仓库时没用--bare而是用普通的git init /srv/git/awesome-project推代码时大概率会遇到这个错误remote: error: refusing to update checked out branch原因是服务器端仓库存在一个工作目录Git默认拒绝在工作目录被检出的分支上接收推送怕把你服务器上正在使用的文件搞乱。解决办法有两种推荐做法重新用裸仓库。把普通仓库复制成裸仓库git clone --bare /srv/git/awesome-project /srv/git/awesome-project.git或者如果你确实需要服务器端能checkout一份最新代码过去比如用于部署那可以把仓库设置成允许接收推送并自动更新工作目录这个就是后面要讲的Hook自动部署。5.4 文件权限导致的推送问题有时候push能成功但服务器上查看仓库文件时发现权限不对其他用户无法读取。这通常是因为创建仓库时用的是root用户导致仓库目录属主不是git用户。解决办法sudo chown -R git:git /srv/git这一步很容易被忽略但影响了多人协作时能不能正常读写。6. 进阶玩法利用Hook实现自动部署6.1 什么是Git HookGit Hook是Git在特定事件发生时自动执行的脚本。对于自建服务器来说最常用的是post-receive——当服务器接收到一次push完成之后执行。这个Hook可以在push完成后自动把代码同步到指定的Web目录实现push即部署。这个功能解决的实际问题是团队里经常有人push完代码后忘了去服务器上手动拉取更新导致线上代码和仓库不一致。配置了自动部署之后代码一到服务器Web服务立刻就用上新版本了。6.2 创建post-receive Hook实现自动部署假设你的Web站点目录是/var/www/awesome-project目标效果是有人推送代码到仓库后服务器自动把最新代码同步到这个目录。第一次需要先准备部署目录sudo mkdir -p /var/www/awesome-project sudo chown -R www-data:www-data /var/www/awesome-project然后在裸仓库的hooks目录里创建post-receive文件sudo -u git vim /srv/git/awesome-project.git/hooks/post-receive写入以下内容#!/bin/bash TARGET/var/www/awesome-project GIT_WORK_TREE$TARGET git checkout -f保存后赋予执行权限sudo chmod x /srv/git/awesome-project.git/hooks/post-receive sudo chown git:git /srv/git/awesome-project.git/hooks/post-receive原理是把仓库的工作目录临时指定为/var/www/awesome-project然后强制checkout最新代码。这个方案只适合静态站点或者纯前端项目。如果是Node、Python这种需要构建或重启服务的项目还需要在Hook里加上重启服务、执行构建脚本的命令逻辑会更复杂。我实际项目中用的Hook比这个复杂一些会先判断推到的是不是main分支只有主干更新才触发部署#!/bin/bash TARGET/var/www/awesome-project while read oldrev newrev ref do if [ $ref refs/heads/main ]; then echo 检测到main分支推送开始部署... GIT_WORK_TREE$TARGET git checkout -f # 在这里可以加上构建命令例如 # cd $TARGET npm install npm run build sudo systemctl reload nginx else echo 非main分支推送跳过自动部署 fi done写Hook脚本时注意服务器环境里的命令路径可能和交互式Shell不一样必要时写全路径比如/usr/bin/php而不是php避免脚本执行时报command not found。6.3 服务器端不鼓励使用的工作流前面说过服务器上的裸仓库没有工作目录不能直接在服务器上编辑代码。有些新手会在服务器上clone一份普通仓库然后直接在服务器上改文件再commit。这种操作方式在自建Git服务器里其实很别扭容易造成代码和远程仓库不一致。我的建议是服务器端只做存储和分支管理所有代码改动都在本地完成。这样能最大程度避免权限问题、文件锁问题也让仓库的数据更干净。7. 仓库备份与日常维护7.1 为什么自建仓库要格外重视备份用Gitee、GitHub这类托管平台时平台本身会做多副本存储数据丢失概率极低。但自建服务器不同一台机器挂了就是所有代码都丢了。所以备份这一步不能省略。备份Git仓库有两种思路一是直接备份裸仓库的目录文件二是通过在服务器上定期clone来保存一份只读副本。我两种都会用双保险。7.2 定时备份脚本创建备份脚本/usr/local/bin/git-backup.sh#!/bin/bash BACKUP_DIR/srv/git-backup DATE$(date %Y%m%d_%H%M%S) mkdir -p $BACKUP_DIR for repo in /srv/git/*.git; do name$(basename $repo) tar -czf $BACKUP_DIR/${name}_${DATE}.tar.gz -C /srv/git $name done # 保留最近7天的备份更早的删除 find $BACKUP_DIR -name *.tar.gz -mtime 7 -delete赋予执行权限加入crontabsudo chmod x /usr/local/bin/git-backup.sh sudo crontab -e在crontab里加一行0 2 * * * /usr/local/bin/git-backup.sh这样每天凌晨2点备份一次保留7天。7.3 仓库迁移换服务器时迁移仓库很简单。新服务器上创建好同样的/srv/git目录后直接把旧的裸仓库目录打包拷贝过去或者从旧服务器上clone一份裸仓库git clone --bare git旧服务器IP:/srv/git/awesome-project.git然后把生成的awesome-project.git目录移动到新服务器的/srv/git/下就行。开发者本地的remote地址会从旧IP指向新IP只需要改一下remotegit remote set-url origin git新服务器IP:/srv/git/awesome-project.git7.4 仓库体检与垃圾回收Git仓库用久了因为频繁的提交和分支操作对象数据库里会有很多不可达对象。虽然不影响功能但会占磁盘空间。可以定期执行sudo -u git git --git-dir/srv/git/awesome-project.git gc --aggressive --prunenowgit gc会清理冗余对象并压缩存储。个人经验是不要在开发高峰期执行因为比较吃CPU和IO在凌晨配合crontab跑比较合适。7.5 磁盘空间监控一个容易被忽视的点是服务器磁盘空间。代码本身不大但仓库的.git目录会随着历史提交不断膨胀尤其是包含大文件的时候。我遇到过一次服务器磁盘写满导致所有push失败的教训从那以后我在服务器上加了简单的空间监控df -h /srv/git建议每周看一眼或者在crontab里加一个磁盘使用率告警0 9 * * * df -h /srv/git | awk NR2 {if ($50 80) print 磁盘空间告警, $5} /var/log/git-disk.log8. 常见问题排查从连接失败到push冲突8.1 搭建完成后本地无法连接搭建好服务器后进行测试常见问题按下面顺序排查服务器SSH服务是否正常运行systemctl status ssh。服务器防火墙是否放行22端口。Ubuntu默认的ufw如果启用了需要执行sudo ufw allow 22/tcp。客户端是否能Ping通服务器。有些云服务器的安全组规则需要在控制台单独配置。使用的用户名和密钥是否正确。8.2 push时提示Everything up-to-date但没有推送成功这个提示其实是正常的意思是本地没有新的提交需要推送。如果你确认本地有提交但提示还是这样检查一下当前分支是否设置了上游git branch -vv如果显示[origin/main]说明上游设置好了如果显示[origin/main: gone]说明远程分支被删了或者未正确设置重新设置一下git branch --set-upstream-toorigin/main main8.3 push时报错non-fast-forward当远程仓库有本地没有的新提交而你强制推送时Git会拒绝这个操作提示non-fast-forward。这是保护机制避免覆盖别人的提交。正确做法是先拉取远程更新git pull --rebase git push如果确定要覆盖远程历史比如代码改错了想回退可以用--force参数但强烈建议不要在主分支上使用git push --force现在Git 2.30以上版本还提供了--force-with-lease这个参数会在远程分支和本地记录一致时才允许强制推送更安全git push --force-with-lease8.4 服务器重启后Git无法访问这种情况基本是SSH服务没有设置开机自启。检查sudo systemctl enable ssh sudo systemctl start ssh如果用的是阿里云、腾讯云这类云服务器还要确认安全组里有没有放行端口。这些平台即使你服务器内部防火墙开着安全组不给放行也进不来。8.5 一个奇特的坑服务器时间不准导致Git操作失败这个问题比较冷门但遇到过。Git的提交、SSH握手都会依赖系统时间如果服务器时间偏差太大可能出现证书校验失败或者奇怪的握手错误。排查方法date如果时间和实际时间差太多安装ntpdate校准一下sudo apt install ntpdate -y sudo ntpdate ntp.aliyun.com也可以直接用systemd-timesyncd同步时间sudo timedatectl set-ntp true处理好之后Git仓库的push和clone就恢复正常了。9. 给新手的几条实操建议最后聊几个我实际使用下来觉得很有用的经验。第一写commit信息一定要认真。很多人刚开始觉得能提交就行结果项目做大了之后回看提交历史全是fix、update、111这种完全定位不了问题。我自己规定commit message第一行不超过50个字符简明扼要说清楚做了什么如果要补充细节就空一行再写。这个习惯能让你在半年后依然看得懂自己当时改了什么。第二重视.gitignore。在项目一开始就创建.gitignore文件把node_modules、target、.env这些不应该进仓库的目录和文件排除掉。不要等代码已经提交了再改因为后面清理历史非常麻烦。我不止一次看到有人把数据库密码和密钥直接推到仓库里最后被迫换密码的情况。第三大文件不要往普通Git仓库里塞。Git对二进制文件、视频、压缩包这些不友好会让仓库体积迅速膨胀。如果团队确实需要存储大文件可以考虑git-lfs扩展或者把这些文件单独放到对象存储里。第四小团队的仓库管理可以简单但分支策略要有。哪怕只有两个人协作也建议约定好主干分支保持可发布状态所有新功能开分支开发合并前先review或用CI跑一遍测试。从项目第一天就养成这个习惯后面协作效率会高很多。我在Ubuntu服务器上搭裸仓库的经验就这样了。这套方案不复杂但胜在稳定、轻量运行几年也不会出大问题。如果你用下来感觉不够用再往Gitea或GitLab走也来得及——至少基础的东西已经打好了。