news 2026/10/21 13:57:14

手把手在Linux上搭建原生Git服务器:SSH公钥与裸仓库实操

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
手把手在Linux上搭建原生Git服务器:SSH公钥与裸仓库实操

1. 方案选型:为什么先从原生Git与SSH开始

这个系列文章的核心目标是教大家在Linux上搭建自己的Git服务器,而第一篇往往决定了整个系列的基础线。很多朋友一说起Git服务器,第一反应就是上Gitea、GitLab这类带Web界面的重型系统,或者再轻一点用Gitolite做权限管理。这些方案确实成熟,但第一篇我不打算直接上它们,而是先把最朴素、最底层的玩法讲透——用Linux原生Git仓库 + SSH公钥认证,手工把服务器端仓库搭起来。

选择这个路线的理由其实很实在。Git本身是分布式的,服务器端并不需要什么特殊程序在跑,它只需要提供两样东西:一个是存放裸仓库的目录和权限环境,另一个是让客户端能够安全访问仓库的通道。SSH天然就是Linux下最标准的远程访问通道,两者一组合,一台哪怕只有512MB内存的老机器,也能非常稳定地支撑一个小团队的代码托管需求。相比之下,Gitea这类程序本身就是常驻进程,要额外管理服务状态、数据库,对低配机器其实是负担。

从学习路径来看,亲手把最底层的方案跑通一遍,你对Git仓库的结构、SSH工作方式的理解会有一个质的提升。之后再去用Gitea或者GitLab,你会很清楚它们背后在做什么事情,出了问题排查起来也知道往哪个方向找。

这个方案最适合的人群分两类。一类是个人开发者,想把自己散落在几个电脑上的代码统一管理起来,不想把私有项目放到公共托管平台;另一类是小团队,可能就三五个人协作,希望数据留在自己的机器上,不想依赖外部服务。如果你正好也是这种场景,跟随这篇文章一步步操作,一个可用的Git服务器大概二十分钟左右就能跑起来。

提示:文章中的命令以Debian系系统(Ubuntu/Debian)为例,但核心步骤在CentOS、Rocky这类RHEL系系统上同样适用,差异点我会在对应位置标注出来。

2. 前置准备:系统环境与依赖安装

在动手之前,先把系统环境准备好。这里默认你已经有一台Linux服务器,能通过root或者一个有sudo权限的账号登录。服务器是裸机还是云主机无所谓,Git服务器的性能瓶颈几乎不在硬件上,反而是在磁盘IO和网络带宽上,所以普通的一核两G配置完全够用。

2.1 确认系统版本与基础信息

登录服务器后,建议先看一眼系统版本,这能帮你判断后面的操作该用哪套包管理命令。我自己的测试机器是一台日常跑着Debian 12的老机器,用它来演示整个流程。系统架构本身无所谓,只要内核别太老就行。

cat /etc/os-release

执行这条命令,你会看到类似下面的输出,其中VERSION_ID这一项就是系统的版本号:

PRETTY_NAME="Debian GNU/Linux 12 (bookworm)" VERSION_ID="12" VERSION_CODENAME=bookworm

如果是CentOS或Rocky Linux,cat /etc/redhat-release的输出会不一样,但同样能确认版本信息。拿到版本号之后,就可以放心往下装了。

2.2 安装Git

绝大多数Linux发行版的官方软件源里都有Git,直接用包管理器安装就行,不需要什么编译安装。Debian系一条命令搞定:

sudo apt update sudo apt install git -y

RHEL系对应的命令是:

sudo yum install git -y

装完之后验证一下版本,Git的版本信息看起来是下面这样:

$ git --version git version 2.39.2

顺手再把Git的全局身份信息配置一下,这是提交代码时必需的,否则Git会在第一次提交时大声报错。这里的用户名和邮箱只是标识作用,实际上Git在服务器端做权限验证时用的是SSH公钥,跟这个身份信息没有直接关系。

sudo -u git git config --global user.name "Git Server" sudo -u git git config --global user.email "git@localhost"

2.3 创建专用用户

这一步很多人会忽略,但我觉得它是整个搭建过程里最重要的一环。不要用root账号直接管理Git仓库。原因有两个层面:

从安全角度看,如果Git仓库给root管,那么所有能通过SSH访问仓库的人,拿到的一定权限等级下都能对仓库文件做各种操作,万一仓库目录配置不当,风险会非常大。更关键的是,Git仓库本身需要文件系统的读写权限来管理对象,如果这个权限挂在root名下,后续为团队成员配置精确权限时就会非常别扭。

从运维角度看,服务器上跑的服务最好是一个服务对应一个专用账号,这样各个服务之间的文件权限天然就隔离开来,日志排查、权限控制都比较清爽。我比较推荐专门创建一个名为git的用户来负责所有Git仓库的管理:

sudo useradd -m -s /bin/bash git

这条命令创建了一个名为git的用户,-m参数表示同时创建home目录,也就是/home/git。-s /bin/bash指定登录shell,这一步对后面配置SSH公钥非常有帮助。

给这个用户设置一个密码,虽然后面我们会配置SSH公钥登录,但保留一个密码可以让你在紧急情况下直接或者通过控制台登录排查问题,算是个兜底手段:

sudo passwd git

如果你希望git用户不允许密码登录SSH,后面有专门的配置方案,这篇文章里先把密码设置好。

到这里前置准备就齐了。如果一个环节都不出错,已经装好了Git、创建了专用用户。接下来要解决的问题是:Git仓库文件应该放在哪里、怎么初始化。

3. 核心实操:创建裸仓库与SSH公钥配置

这个阶段是整篇文章的实操核心。围绕两个关键问题:仓库怎么建,权限怎么放。

3.1 什么是裸仓库,为什么服务器端必须用裸仓库

Git仓库有两种形态,一种是普通工作仓库,我们在本地开发时使用,里面有.git目录,也有实际的工作目录文件。另一种是裸仓库,它没有工作目录,只包含.git目录里的那一套东西,也就是Git对象数据库、引用、配置等等,专门用来作为集中式的数据交换点。

服务器端必须使用裸仓库,原因在于:客户端推送到服务器的是提交数据,服务器本身并不需要有一个工作目录去检出这些代码。如果有人把代码推到服务器后,服务器上还带着工作区,那除了浪费磁盘空间之外,还会带来一个非常麻烦的问题——工作区的状态可能会和仓库不一致,进而导致后续推送时出现各种奇怪的分支冲突或者文件冲突。裸仓库从根源上避免了这个问题。

在git用户的home目录下创建一个专门存放仓库的目录,然后初始化裸仓库。我这里先建了一个srv目录存放所有仓库,用test-repo.git作为示例仓库名:

sudo mkdir -p /home/git/srv sudo chown git:git /home/git/srv sudo -u git git init --bare /home/git/srv/test-repo.git

初始化完成后,进入仓库目录看一眼,你会看到如下一坨东西:

drwxr-xr-x 7 git git 4096 Mar 1 10:25 branches -rw-r--r-- 1 git git 66 Mar 1 10:25 config -rw-r--r-- 1 git git 73 Mar 1 10:25 description -rw-r--r-- 1 git git 23 Mar 1 10:25 HEAD drwxr-xr-x 7 git git 4096 Mar 1 10:25 hooks drwxr-xr-x 2 git git 4096 Mar 1 10:25 info drwxr-xr-x 4 git git 4096 Mar 1 10:25 objects drwxr-xr-x 4 git git 4096 Mar 1 10:25 refs

这些目录各有分工。objects目录是Git的核心,所有提交的数据都以对象形态存在这里面。refs目录保存的是分支和标签的引用。hooks目录用来放钩子脚本,后面做自动化部署或者推送通知时就用它。现在不需要记住每个目录的作用,但了解objects和refs这两个核心概念,之后排查问题会轻松不少。

3.2 将HEAD指向master分支,规避默认分支不一致

初始化出来的裸仓库默认HEAD指向master分支。如果你本地的Git版本较新(2.28以上),git init的时候可能会默认创建main分支而不是master。这就会导致一个情况:你往服务器推送了main分支,但服务器仓库的HEAD还指向不存在的master,客户端git clone这个仓库时会跳出警告,并且无法自动检出任何分支。

处理办法是在裸仓库上执行一条命令:

sudo -u git git --git-dir=/home/git/srv/test-repo.git symbolic-ref HEAD refs/heads/master

--git-dir参数指定了仓库所在目录,symbolic-ref HEAD refs/heads/master将HEAD符号引用指向了master分支。这样客户端克隆仓库时会以master为默认检出分支,保持一致的行为预期。如果你团队习惯用main,就把命令末尾改成refs/heads/main。

3.3 生成SSH密钥对并上传公钥

服务器端的仓库已经就位,接下来解决的是客户端如何登录这个仓库。Git over SSH的原理很简单:客户端通过SSH协议连接服务器的git用户,认证通过后在服务器上执行Git命令。

SSH认证有密码和公钥两种方式。密码方式每次推送拉取都要输入口令,而且服务器还需要开启密码SSH登录,安全性和便利性都很一般。正常做法是使用SSH公钥认证。

在客户端机器上执行下面的命令生成密钥对(如果之前已经生成过,直接跳过这一步):

ssh-keygen -t ed25519 -C "your_email@example.com" -f ~/.ssh/id_ed25519

-t ed25519指定算法为Ed25519,这是目前安全性和性能平衡得比较好的算法。-C只是给公钥加一个备注标识,方便自己辨认是哪台机器的公钥。-f指定生成的密钥文件路径,如果你不想多个密钥混在一起,也可以换成其他文件名。

执行后会提示你设置passphrase(口令),直接回车留空打开私钥时需要输入口令保护,看个人需求,对于开发机来说设一个也不算麻烦。

生成完成后,在客户端执行以下命令将公钥上传到服务器:

ssh-copy-id git@服务器IP

这条命令会要求你输入git用户的密码,然后把本地公钥追加到服务器的/home/git/.ssh/authorized_keys文件中。如果你用的不是默认的密钥文件名,需要加参数指定:

ssh-copy-id -i ~/.ssh/某个密钥文件名.pub git@服务器IP

没有ssh-copy-id命令的环境(比如Windows的某些SSH客户端),手工操作也很容易:把本地公钥文件内容复制到剪贴板,然后用SSH登录服务器,执行:

sudo mkdir -p /home/git/.ssh sudo touch /home/git/.ssh/authorized_keys

然后编辑/home/git/.ssh/authorized_keys,把公钥内容粘贴进去,把文件owner改成git用户,并设置正确的权限:

sudo chown -R git:git /home/git/.ssh sudo chmod 700 /home/git/.ssh sudo chmod 600 /home/git/.ssh/authorized_keys

3.4 authorized_keys的权限为什么这么严格

authorized_keys文件的权限要求是600,.ssh目录权限要求是700,这条规则背后是因为OpenSSH对安全性的严格校验。如果这两个对象的权限过宽,比如authorized_keys是644,任何能够登录到系统的其他用户就都能读取这份文件,知道了有哪些公钥,也就等于知道了哪些用户有权限,这对系统来说是个隐私泄露入口。更严重的是,如果用户home目录权限是777,其他用户就能往你的authorized_keys文件里塞自己的公钥,直接获得访问权限。

所以在操作SSH相关文件时,我有一条很基础但很重要的心得:任何SSH配置文件的权限只能比默认配置更严格,不能更宽松。如果哪天发现SSH公钥认证突然失效,第一步不是去改服务器配置,而是先去检查这些关键文件的权限是否被某些操作不小心改松了。

3.5 验证SSH连接

上传完公钥后,先别急着克隆仓库,验证一下SSH通道是否通畅:

ssh git@服务器IP

如果一切正常,你会看到类似这样的输出然后马上退出:

Welcome to Ubuntu 22.04.5 LTS (GNU/Linux 5.15.0-116-generic x86_64) ... Last login: ...

输入exit退出。这步能确认你的公钥已经被服务器接受了。如果提示输入密码,那说明公钥没配置成功,或者sshd配置拒绝公钥认证,需要回头检查。

4. 本地克隆、推送与多人协作细节

通道通了,仓库也建好了,现在开始真正使用这个服务器仓库。这一部分的操作主要是站在开发者的角度来走一遍流程,看看怎么把本地代码推到服务器,以及和多人协作时的细节怎么处理。

4.1 从本地克隆空仓库

在本地任意工作目录下执行:

git clone git@服务器IP:/home/git/srv/test-repo.git

注意地址格式,@前面是用户名,IP后面跟的是仓库绝对路径。如果你自定义了SSH端口,这样写:

git clone ssh://git@服务器IP:端口号/home/git/srv/test-repo.git

克隆一个空仓库时,Git会提示这样一段话:

warning: You appear to have cloned an empty repository.

不用慌,这只是说明远程仓库还没有任何提交。此时进入克隆下来的目录,尝试创建一个测试文件并推送:

cd test-repo echo "hello git server" > README.md git add README.md git commit -m "init commit" git push origin master

推送成功的话,输出会显示类似master -> master的信息。此时回到服务器上看一眼仓库大小和状态,你可以验证推送是否真正生效:

sudo -u git git --git-dir=/home/git/srv/test-repo.git log --oneline

能列出commit记录,说明整个链路已经完全打通了。

4.2 普通克隆与镜像克隆的区别

先解释一个很多新手会踩的坑:为什么git clone下来后,git branch看不到远程服务端全部分支?

这个问题背后的原理是,普通克隆默认只会在本地创建工作区对应的那个分支(通常是master或main),其他远程分支只是以origin/分支名的形式存在于远程跟踪引用里,不会在本地创建。如果服务端仓库里有多个分支,你在本地直接执行git branch -a,看到的远程分支长这样:

* master remotes/origin/feature-a remotes/origin/master

想切换到feature-a分支,直接git checkout feature-a,Git会自动基于origin/feature-a创建对应的本地分支,这个机制是Git设计上的便利之处。

如果你想把服务端仓库完完整整地复制下来,包括所有分支、标签、引用,那就用镜像克隆:

git clone --mirror git@服务器IP:/home/git/srv/test-repo.git backup-repo.git

镜像克隆会把整个仓库的引用结构原样复制,不会做本地分支的转换。这种用法通常用在仓库备份或者迁移场景,平时开发用普通克隆就够了。

4.3 多用户权限管理思路

文章开头说了,原生Git搭配SSH的方案在权限管理上比较简单,那么多人协作时怎么办?常用做法有几种。

如果团队成员很少,比如就三四个人,最简单的方式就是让每个人都用自己的系统账号(这里指linux系统账号)登录服务器去访问仓库,管理员通过系统账号的权限来控制文件访问。你可以为每个成员创建一个同名系统用户,然后把仓库目录的owner设置成一个共享组:

sudo groupadd gitgroup sudo usermod -a -G gitgroup git sudo usermod -a -G gitgroup member1 sudo chown -R git:gitgroup /home/git/srv sudo chmod -R 775 /home/git/srv

也就是说仓库文件对组内成员可读可写,这样所有成员推代码就都没有问题。但这种方式在成员变多、项目变多之后会变得难维护,每个项目文件权限都要单独想清楚。

另一种办法是在SSH层做手脚,通过authorized_keys文件的command选项来限制每个公钥只允许执行特定的Git命令,或者配合git-shell这种受限shell。git-shell是Git自带的受限shell,安装Git时通常一并带上,它只允许执行Git相关的命令(git receive-pack、git upload-pack),不允许用户登录后执行任何系统命令。我们可以把git用户的登录shell改成git-shell:

sudo usermod -s /usr/bin/git-shell git

修改后再尝试SSH登录,服务器会提示:

fatal: Interactive git shell is not enabled. hint: ~/git-shell-commands should exist and be readable

这意味着普通SSH登录被禁止了,但Git命令仍然正常工作,对安全性是很大的提升。注意验证一下git-shell的路径,有些发行版在/usr/bin/git-shell,有些在/usr/lib/git-core/git-shell,不确定就执行which git-shell看路径。

4.4 服务器端配置与裸仓库管理小记

裸仓库初始化后有一个配置文件config,用编辑器看内容大概长这样:

[core] repositoryformatversion = 0 filemode = true bare = true

bare = true标记这个仓库是裸仓库。如果哪天误操作把它改成了false,服务端仓库工作区会被激活,推送行为就会变得古怪。所以记住一个原则:服务器端仓库的bare标记永远是true。

如果团队需要控制仓库大小或者禁止某些分支的强制推送,可以在裸仓库上执行这些配置。比如禁止master分支被force push:

sudo -u git git --git-dir=/home/git/srv/test-repo.git config receive.denyNonFastForwards true

这条配置会让服务器拒绝非快进推送(也就是强制推送),配合后续需要做代码评审的场景会比较有用。

5. 常见问题与排查技巧实录

搭建过程虽然步骤不多,但实际操作中总会碰到一些让人挠头的问题。把我在整个过程中遇到的高频问题整理成一个排查速查表,遇到同类问题可以对照着查。

5.1 Permission denied (publickey)

这是出现频率最高的一个错误。完整报错通常类似:

git@服务器IP: Permission denied (publickey). fatal: Could not read from remote repository.

思路先看客户端:确认公钥是否在~/.ssh下,私钥文件名是否和SSH默认读取的文件名一致。如果私钥文件名不是id_ed25519或id_rsa这种默认名,SSH不会自动加载,需要用ssh-agent或者创建~/.ssh/config来指定。

再看服务端:打开/etc/ssh/sshd_config,确认PubkeyAuthentication yes没有被注释掉,AuthorizedKeysFile路径是否指向了正确的文件。修改配置后重启sshd:

sudo systemctl restart sshd

最常见的情况是服务端authorized_keys文件权限松了或者owner不对,所以这一步排查顺序建议是:先看authorized_keys的权限,再看ssh-agent是否加载了私钥,最后再看sshd_config配置。

5.2 服务器init的仓库克隆后无法直接推送

初始化裸仓库后,本地克隆下来创建一个分支推送,服务器总是报rejected错误。通常是分支名对不上导致的。检查本地分支名到底是master还是main,执行一下git branch -a看实际名称。保持一致之后推送就正常了。

还有一个常见的场景是用户本地全局配置了init.defaultBranch为main,或者某个仓库是从别的平台拉下来的默认分支叫main,推送时没有指定目标分支,就会匹配不上。推送时直接写全:

git push origin master

或

git push -u origin main

规避这类分支名不一致的问题。

5.3 克隆后找不到服务端已有的分支

这个问题在第4.2节解释过原理。如果在服务端仓库上手动用git branch建了分支(注意是裸仓库,不能直接git branch操作工作区文件),或者别人推送了新分支,本地git clone之后git branch看不到,这是正常的。用git branch -a查看所有分支,包含remotes/origin/开头的远程分支。

如果确实希望克隆后默认就检出某个特定分支,用这个命令:

git clone -b 分支名 git@服务器IP:/home/git/srv/test-repo.git

-b参数指定克隆后检出的目标分支。

5.4 推送时提示“拒绝”或者“无法更新”

推送被拒绝的原因通常是两类。一类是本地分支落后于远端分支,需要先git pull或git fetch再git merge,这类报错一般会显示冲突文件清单。另一类是管理员设置了receive.denyNonFastForwards true,禁止强制推送。

如果确实需要强制推送,在确认不会覆盖其他人的工作的前提下,可以用git push --force或git push --force-with-lease。后者相对安全,推送前检查本地远程跟踪引用与远端一致,不一致时拒绝推送,避免误覆盖。建议永远把--force-with-lease作为默认选择。

5.5 hooks脚本没有执行

裸仓库里的hooks目录下有一堆.sample结尾的示例脚本。如果你在里面放了自定义脚本(比如push后自动部署),但它没有执行,大概率是权限或者文件名问题。hooks目录下的脚本必须带有可执行权限:

sudo chmod +x /home/git/srv/test-repo.git/hooks/post-receive

文件名也必须是Git规定的标准钩子名(不带.sample后缀),并且确保脚本里的shebang指向实际存在的解释器路径。放好之后可以往仓库推送一个空commit来测试脚本是否被触发。

6. 运维视角:仓库备份与迁移基础

既然服务器上存的是团队代码,备份这件事不能等到出事才想起来。基于原生Git仓库的备份思路很简单,不需要什么专业工具,利用Git自身的克隆机制就能做。

全量备份用镜像克隆把整个仓库备份到另一个分区或者另一台机器:

git clone --mirror git@服务器IP:/home/git/srv/test-repo.git /backup/test-repo.git

镜像克隆会把所有引用完整复制下来,备份恢复时只需要把备份好的裸仓库目录拷贝回原位置即可。如果你想要的是一个带时间戳的周期备份,可以写一个简单的定时任务,用rsync同步裸仓库目录:

rsync -av --delete /home/git/srv/ /backup/git-server-backup/

--delete参数同步时删除源端已经不存在的文件,保证备份目录和源目录结构一致。裸仓库没有工作区文件,rsync同步数据量不大,做远程备份也很方便。

如果后面想换一台服务器,直接把整个/home/git目录用tar打个包搬过去,注意保持文件owner和权限不动,新服务器上就能继续使用。这个迁移方案在仓库规模不算特别大的时候非常可靠,算是原生方案的一个优势。

7. 个人实操体会与后续扩展方向

这一套原生Git服务器搭建下来,最大的感受就是:它没有那么多花里胡哨的功能,但最基本、最核心的所有环节都摆在明面上,出了问题一眼就能看到底层逻辑。它不遮遮掩掩,仓库就是一堆文件夹,权限就是Linux文件权限和SSH配置,远程同步就是标准的SSH通道。这个简洁性在我看来是优点,它逼着你把Git和SSH的基础打牢,而这些基本功在以后用任何Git服务时都用得上。

用下来的体会还有一点,这类方案特别适合不追求界面管理、喜欢用命令行加脚本解决问题的人。比如我在裸仓库的hooks目录里放了一个post-receive脚本,实现代码推到服务器后自动检出到项目目录,再执行重启服务命令,就相当于一个很轻量的自动化部署工具。后面如果有同事非要用Web界面管理仓库,再在这个基础上把Gitea之类的服务叠加起来也不冲突,仓库数据本身是通用的。

下一篇我准备展开聊一聊怎么在这个基础上做权限控制的高级玩法,包括按项目分配不同成员的读写权限、用git-shell限制用户行为,以及如何通过hooks机制实现推送通知和自动化部署。先把这个最基础的地基打牢,后面怎么做心里都有数。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/21 13:56:56

个人技能管理体系搭建实战:从技能树到复盘

把“skills”当成一个项目标题来拆,其实是件很有意思的事。它不是一个具体功能,也不是一个系统名称,它指的是——你身上那堆能力,到底是怎么被组织起来的。我见过太多人,简历上写“掌握十几种软件”,真上手…

作者头像 李华
网站建设 2026/10/21 13:55:56

K8s网络模型:从Pod创建到Service暴露的完整链路解析

1. 先把最核心的抽象模型说清楚新手入门 K8s,最容易卡住的地方其实不是 yaml 语法,也不是 kubectl 命令,而是脑子里对“Pod 创建”和“服务暴露”这两件事始终串不起来。做容器编排的人应该都有这种经历:明明照着教程把 Pod 跑起来…

作者头像 李华
网站建设 2026/10/21 13:53:42

基于SpringBoot和Vue的儿童性教育网站管理系统设计与实现

上个月接了一个儿童教育机构的系统需求,说是要做一个基于SpringBoot和Vue的儿童性教育网站管理系统。听着就是一套标准的后台管理加内容展示项目,但真正开始梳理需求时才发现,这个系统的难点根本不在增删改查,而在三块&#xff1a…

作者头像 李华
网站建设 2026/10/21 13:53:42

知识工作插件设计:从碎片捕获到本地检索的轻量实践

最近整理手头散落的笔记、代码片段和会议记录时,发现一个很头疼的现象:明明保存过的东西,转头就忘记放在哪,也没法和当天的语境串起来。断断续续花了几天时间,把一个叫「knowledge-work-plugins」的小项目从想法捋成了…

作者头像 李华
网站建设 2026/10/21 13:50:48

BrowserUse + Agentrun Sandbox:生产级浏览器自动化任务的沙盒实践

1. 为什么我把浏览器智能体搬进了沙盒做浏览器自动化的人迟早会撞上一个诡异时刻:脚本在本地跑得好好的,换台机器一执行就崩;或者任务跑到一半,宿主机被一堆残留进程拖死;更常见的是,同事不小心执行了某个清…

作者头像 李华