☰
Linux下用SSH与裸仓库自建Git服务器:从环境准备到钩子自动部署
2026/10/21 9:51:23 网站建设 项目流程

说实话,有几年没在国内社区看到有人认真聊“Linux下自己搭Git服务器”这个话题了。现在主流做法基本是直接上云平台、用现成的代码托管服务,或者装个带Web界面的管理工具。但对于很多做嵌入式、搞内网部署、或者对数据流通有严格要求的团队来说,自己在一台Linux机器上从零搭一个Git服务器,依然是基本功。它能让你彻底搞明白远端仓库的运作机制,碰到问题的时候,不至于黑盒调试。

这篇先聊最经典也最踏实的一种方案:用普通Linux账号 + SSH协议 + 裸仓库(bare repository)来搭建Git服务器。这套组合不依赖任何额外的服务端进程,只要系统支持SSH,Git就能跑起来。文章会从环境准备、仓库初始化、客户端接入、权限控制,到钩子自动部署,把整个链条走一遍。适合刚接触Git服务端、或者想搞清楚底层原理的开发者,也适合要在隔离网络里快速拉起一个团队仓库的运维同学。

1. 搭建前必须要做的事:需求梳理与环境准备

1.1 什么样的场景适合自建Git服务器

先说点体己话。自建Git服务器不是万金油,它最适合下面这几类场景:代码数据不能出内网的保密项目、团队成员不多(几十人以内)、想要完全掌控仓库存储位置和备份策略的情况。如果你的场景是开源项目、跨地域大规模协作,那直接用现成的托管平台更省心。自建意味着从账号管理、仓库备份到权限策略,全部都得自己扛。

不过对于很多内部工具链、硬件研发、算法团队来说,Git服务器“长在自己的机房”这件事本身就很有安全感。它不依赖外网带宽,不受第三方服务可用性影响,还能和内部已有的认证体系打通。另外,自己搭一次服务器,你会真正理解克隆、推送、拉取这些操作背后发生了什么,对Git本身的掌握也会上一个台阶。

我在帮某硬件团队搭过内部代码库,他们的核心诉求就是:源码必须留在实验室的机器上,不能上传到任何外部平台。这种情况下,一套干净的Linux Git服务器就是刚需。所以先想清楚你为什么要自建,再动手,不要把后面的步骤做成了“为了搭而搭”。

1.2 Linux服务器端的Git安装与SSH准备

第一步,当然是在Linux服务器上把Git装好。不同发行版命令略有差异,但本质都一样:

# Debian / Ubuntu sudo apt update sudo apt install -y git openssh-server # CentOS / RHEL / Rocky Linux sudo yum install -y git openssh-server

装完之后,看一眼版本,确认一下:

git --version

这里我要多说一句:不要把Git装在root用户下,也别用root账号去操作仓库。最佳实践是创建一个专用系统用户来统一管理Git仓库,比如就叫git。这样既方便权限收敛,也方便以后扩展认证方式。创建用户的命令很简单:

sudo useradd -m -s /bin/bash git sudo passwd git

SSH服务是Git服务器的基础通道,如果不确定是否在运行,检查一下:

sudo systemctl status sshd

如果没启,就启用并设为开机自启:

sudo systemctl enable sshd sudo systemctl start sshd

到这一步,环境就算备好了。你会发现除了Git和SSH,啥都不用装,这正是这个方案迷人的地方——依赖少、好迁移、容易排查。

2. 第一台自建Git服务器:SSH协议是最朴实的方案

2.1 为什么先走SSH这条路,而不是写一个后台服务

Git server本质上做的事很纯粹:提供远程仓库的“存取”能力。Git本身是分布式的,服务器端不一定需要复杂的应用层逻辑。如果你在本地仓库执行过git push,那Git做的事情其实是:把本地对象打包,通过特定协议(比如SSH)送到远端仓库里,更新对应引用。

选择SSH协议作为传输通道,意味着我们根本不需要自己写任何服务端程序,不用管Websocket、不用管端口监听。系统自带的SSH服务既负责了加密传输,又负责了身份认证,只需在服务器上准备好Shell账号和目录权限,Git的推送和拉取就能跑通。这相当于把货物的“快递通道”全部外包给了SSH这个成熟体系,我们只需要管好仓库的“货架”就行。

有人会问:“那我用那个git://协议的daemon模式是不是更好?”那个方案更适合匿名只读场景,比如对外提供开源代码快照。它没有内置认证,控制力弱,不适合团队内部日常开发。HTTP协议则需要额外搭建Web服务、配置认证,复杂度上一个台阶。所以初版就用SSH,简单且可靠,出了问题也容易定位。后续如果需要Web界面、多人精细权限管理,可以再演进到更复杂的方案,那是第二篇的话题。

2.2 一条命令新建裸仓库,Push之前必知的关键点

Git服务器的核心是裸仓库(bare repository)。普通仓库带有工作区,也就是你平时代码编辑的那个目录;而裸仓库没有工作区,它只保存Git的历史对象和引用。对服务器来说,不需要也不应该有一个随时会变的文件快照,只需要作为“事实来源”存储所有提交即可。

在服务器上切换到你希望存放仓库的目录,然后执行:

sudo mkdir -p /data/git sudo chown -R git:git /data/git sudo su - git cd /data/git git init --bare my-project.git

注意仓库名后面加.git后缀,这只是一个命名习惯,不是强制。但建议保留,因为这样一眼就能看出它是一个裸仓库,而不是平时那种带工作区的目录。

裸仓库和普通工作区最大的区别,我打个比方:普通工作区就像你的私人书房,你在这里读书、做笔记,文件摊了一桌;裸仓库就像图书馆的藏书库,只负责整理归档,不能被随便翻开乱涂乱画。你在书房里做完笔记,通过git push把成果“归档”到藏书库,其他人再从藏书库“借阅”到自己的书房继续读。

初始化裸仓库时,还有一个参数值得留意:--shared=group。如果你希望同组的多个系统用户都能写这个仓库,这个参数会让仓库的权限组继承设置的组权限。不过如果只有一个git账号,暂时用不上这个参数,后面人多的时候再处理也不迟。

初始化完成后,可以看一眼仓库目录结构:

ls -la my-project.git

你会看到HEAD、config、objects、refs这些文件和目录。不用害怕,它们就是Git的骨架。objects存的是历史对象,refs存的是分支和标签指针,HEAD指向当前默认分支。到这一步,服务器端的仓库已经“地基”搭好了,接下来就是让客户端能往里推送代码。

3. 客户端接入与权限控制:让仓库真正可用

3.1 客户端生成密钥并配置免密登录

如果仓库建好了,但客户端连不进去,等于白搭。SSH协议的接入方式很简单,核心就是密钥认证。在客户端机器上生成一对SSH密钥,把公钥放到服务器的git用户里,此后推拉代码就不需要每次都输密码了。

先看本机有没有现成的密钥:

ls -la ~/.ssh/

如果没有,生成一个:

ssh-keygen -t ed25519 -C "your_email@example.com"

用ED25519算法,短且快,安全性也足够。生成之后,把公钥追加到服务器git用户的authorized_keys文件中:

ssh-copy-id git@<服务器IP>

我建议先手动用密码登录一次,这样known_hosts会自动记录服务器指纹,后面的交互会顺畅一些。如果ssh-copy-id不好使(比如服务器改了SSH端口),手动追加也很快:

cat ~/.ssh/id_ed25519.pub | ssh git@<服务器IP> "mkdir -p ~/.ssh && chmod 700 ~/.ssh && cat >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys"

这里有两个点我要强调一下,很多人栽在这上面:一是~/.ssh目录权限必须是700,二是authorized_keys文件权限必须是600。如果权限太开放,SSH服务会直接拒绝用这个文件做认证,别问我怎么知道的,排查过太多次了。

测试连接:

ssh git@<服务器IP>

如果没报错,直接进去了,说明认证成功。这里有个新手常见误区:用git账号登录后会发现一个没有任何内容的Shell,这是正常的,因为Git服务器账号本来就不需要交互式Shell,后面我们还可以进一步限制它只能执行Git命令。

3.2 基于系统账号和目录权限的仓库接入方式

钥匙配好了,仓库在那边,客户端这边可以正式接入。在你的开发机上,要么克隆一个新的仓库,要么把一个已有的本地仓库跟远端关联起来。

克隆方式:

git clone git@<服务器IP>:/data/git/my-project.git

如果你是先把代码在本地写好了,想推到服务器上,就执行:

git remote add origin git@<服务器IP>:/data/git/my-project.git git push -u origin master

注意Remote地址的写法是用户@主机:绝对路径。很多新手在这里出错是因为路径写成了相对路径,结果仓库被推到了用户家目录下的某个奇怪位置。这里写的路径直接对应服务器上的文件系统路径,不能随意省略。

推完之后,你可以到服务器端验证一下:

cd /data/git/my-project.git git log --all --oneline

如果能看到你提交的哈希,说明数据已经落盘,服务器角色正式就位。

3.3 小团队协作时的权限划分思路

服务器搭好了,团队两三个人用,那最简单:给每个人建一个系统账号,把各自的公钥放到对应账号的authorized_keys里,仓库路径放在共享目录,配合Linux用户组来控权限。例如创建一个dev组,把需要用这个仓库的几个用户都加进去,然后:

chgrp -R dev /data/git/my-project.git chmod -R g+rwX /data/git/my-project.git

这样同组的成员就都能读写这个仓库了。注意裸仓库内有一些内部文件和钩子文件需要保持可执行权限,所以X不能少。

如果团队成员稍微多一点,比如几十人,这种“每个人一个系统账号”的方式就会变得痛苦:建账号、删账号、定期轮换公钥,全是体力活。所以通常的做法是:所有开发共享一个git账号,把所有人的公钥都追加到git用户下。这做法的优点是好管理,缺点是所有人都以同一个身份操作,分不清谁是谁。

那怎么区分谁是谁?用SSH命令的authorized_keys中的注释。每个人的公钥末尾都有your_email@example.com这段注释,可以改成zhangsan_work_laptop这样的标识。虽然改注释不会改变安全性,但在查看服务器日志和做审计时,至少能看出是哪把密钥连接过。

真要追求更细的“谁对哪个仓库有写权限”,SSH + 系统账号这套方案就比较吃力了,那得往服务端加一层授权逻辑。我在前面的系列预告里提到的“进阶方案”,就是针对这个痛点来的。现在阶段,先确保仓库能正常流转,权限在团队级别上别出错,就行。

4. 让服务器自动部署:一个提高团队效率的钩子脚本思路

4.1 裸仓库上的post-receive钩子能做什么

Git服务器搭好之后,接下来最自然的需求就是:能不能省掉“手动登录服务器拉取代码、重启服务”这一步,让代码一推上去,服务器的应用目录就自动更新?

这个需求不需要外部工具,用Git钩子就能实现。裸仓库的hooks目录下放着很多示例脚本,其中post-receive在客户端推送完成之后会触发。说白了,就是“服务器收到新代码后自动执行的操作”,非常适合做自动部署。

举一个很典型的用途:某团队有个网站项目,每次开发完要向服务器同步静态文件。他们原来的流程是:本地构建,用FTP传上去,很烦。后来我在服务器的裸仓库里加了一个post-receive钩子,把最新代码同步到Web服务的目录里去,开发同事只需要git push,服务器上的网站就自动更新了。

钩子脚本可以简单,也可以复杂。从最简单的“同步工作树”开始,再到“触发CI构建”“推送后通知聊天群”,思路都是一样的。核心要点就是:钩子脚本只做一件事,并且要有日志、要能失败后重试,别把一个几百行的部署逻辑硬塞进钩子里,否则排查起来哭都来不及。

4.2 一个能自动更新工作目录的钩子小例子

下面是我常用的一段post-receive脚本,可以作为一个起点。设定一下:服务器上有一个应用目录/var/www/my-app,我们希望每次推送后,这里就变成最新代码。

在此之前,先把应用目录做成一个Git仓库的“工作区”类型结构。注意,不是裸仓库,是一个普通仓库,只用于检出代码:

sudo mkdir -p /var/www/my-app sudo chown -R git:git /var/www/my-app cd /var/www/my-app git init git remote add origin /data/git/my-project.git

然后写钩子脚本:

sudo su - git vim /data/git/my-project.git/hooks/post-receive

脚本内容:

#!/bin/bash TARGET="/var/www/my-app" export GIT_WORK_TREE="$TARGET" export GIT_DIR="/data/git/my-project.git" echo "Starting deployment..." cd "$TARGET" git --git-dir="$GIT_DIR" --work-tree="$TARGET" checkout -f echo "Deployment finished"

别忘了给执行权限:

chmod +x /data/git/my-project.git/hooks/post-receive

之后你从客户端推送一次,post-receive就会自动运行。这里用GIT_WORK_TREE和GIT_DIR指定环境和目录,是因为裸仓库本身没有工作区,而我们想要代码落地到另外的应用目录,所以必须明确告诉Git这两个路径。

4.3 钩子脚本的几个常见坑,提前帮你踩掉

钩子虽然好用,但它会以执行它的那个用户身份运行。如果你的Web服务器是nginx或Apache,进程运行用户是www-data,而钩子运行用户是git,就会出现文件所有权冲突。解决办法是让Web目录属于git用户所在组,或者用chown -R www-data:git配合组权限设置。这块很容易在“第一次自动部署成功、第二次却被权限拒绝”时暴露出来。

第二个坑是“推送成功但服务器文件没变”。99%的情况是忘了加执行权限chmod +x,或者是脚本里路径写错。调试方法很简单,在客户端推送时看服务器端返回的输出。如果脚本里有echo,这些输出会通过SSH回传给客户端,你能直接看到执行过程的报错信息。

第三个坑是钩子脚本里的环境变量不可靠。你在交互式Shell里能用的$PATH,在钩子脚本里可能是精简版。所以如果脚本里调用了非系统默认路径的命令,尽量用绝对路径,比如/usr/bin/git。曾经有同事的钩子一直报git: command not found,就是环境变量的问题。

5. 踩坑记录与调优指南:来自一线实操的速查表

5.1 这几个月收集的高频问题排行榜

搭建Git服务器的过程中,我发现大家遇到的问题其实高度集中。我把高频问题整理成了一张速查表,遇到直接对照:

现象最常见原因解决办法
克隆或推送时提示Permission denied (publickey)公钥没加全,或authorized_keys权限错误检查~/.ssh/authorized_keys内容,权限设为600
推送成功,但服务器上目录没变化post-receive钩子忘了chmod +xchmod +x后用客户端重新推送验证
SSH连接很慢,卡几秒才出提示符DNS反查问题在sshd_config里加UseDNS no
提交后中文显示为乱码Git默认编码设置不对设置git config --global core.quotepath false
分支名在服务器上看不到忘了git push -u origin <branch>推送时明确建好上游引用
不同成员谁也分不清谁提交的客户端Git用户信息没设置客户端尽早设置user.name和user.email
服务器磁盘被撑爆Git仓库体积越来越大,没做垃圾回收定期git gc --auto
端口不是默认的22,客户端连不上客户端没指定端口SSH地址改成ssh://git@host:port/path

5.2 SSH连接慢与打印欢迎语的调优

先说说最容易遇到的“慢”。新服务器配好后,客户端连SSH总是要停顿几秒才出现输入提示符。绝大多数情况是服务器开启了DNS反解析,SSH服务会尝试反查客户端的IP对应的域名。很多内网环境没有正向和反向DNS记录,这步就会白白卡几秒。

解决办法是修改/etc/ssh/sshd_config:

UseDNS no GSSAPIAuthentication no

然后重启SSH服务:

sudo systemctl restart sshd

改动之后,连接速度一般能感受到明显提升。顺便加一个限制git用户可以执行的命令的做法。既然git账号只用来做Git操作,那可以把它锁在Git命令里,不让它登录到Shell交互界面。经典做法是编辑/home/git/.ssh/authorized_keys,在每条公钥前面加一段前缀:

command="/usr/bin/git-shell -c \"$SSH_ORIGINAL_COMMAND\"",no-port-forwarding,no-X11-forwarding,no-agent-forwarding ssh-ed25519 AAAAC3... user@host

这样即使有密钥,也没法获得交互Shell,只能执行Git命令,安全性大幅提升。对只有Git需求的账号来说,这个加固是强烈推荐的。

5.3 服务器端的日常备份与仓库瘦身策略

最后一个主题,但绝对不是最不重要的——数据安全。Git服务器一旦跑起来,就是团队的“代码保险箱”,备份和仓库健康维护必须常态化。

备份最简单粗暴有效的方案是裸目录打包 + 定时同步。把/data/git目录整体打包传到异地或另一块磁盘上。因为裸仓库是自包含的,打包它不需要停服务。也可以走git bundle单仓库备份,但整体目录打包更适合多仓库场景。

仓库瘦身主要是防患于未然:如果团队经常在仓库里塞二进制文件,比如App的安装包或测试镜像,仓库体积会爆炸式增长。Git处理文本很高效,但对二进制文件很笨拙,历史上每次改动都会存一个完整副本。如果能尽早约定“不用Git提交构建产物”,可以省掉很多麻烦。

已有的胖仓库可以通过git filter-repo一类工具清理历史,这属于高级操作,操作前必须先整体备份,然后所有成员强制重新克隆,过程比较折腾,但还是比放弃仓库好。另外定期执行git gc,它能优化对象存储结构,整理冗余,释放一部分磁盘空间。服务器端可以写个定时任务,每周执行一次所有仓库的git gc --auto。

这一套做完,一台Linux下的Git服务器已经不再是“能用”的水平,而是接近“好用”了。有了SSH通道、裸仓库、密钥认证和钩子自动部署,团队日常开发的代码流转完全没问题。等到某一天,你发现最需要的其实是网页端代码浏览、Pull Request式的代码审查、或更精细的权限控制,那就说明当前的方案到了该进化的阶段了。下一篇,我想聊聊怎么在这个基础上加上访问控制层和图形化管理界面,帮你把服务器从“能用”推向“用得舒服”。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询