多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

Ubuntu服务器自建Git仓库:从SSH密钥配置到分支管理完整指南

Ubuntu服务器自建Git仓库:从SSH密钥配置到分支管理完整指南 前阵子帮一个朋友在新买的Ubuntu服务器上把Git仓库搭起来他连着问了我好几轮SSH密钥、裸仓库、push被拒绝之类的问题最后索性让我把整套流程整理成一份文档。其实他的需求我特别理解代码不想丢进公开托管平台、团队不大、又想要完整的版本控制能力这种时候在一台自有的Ubuntu服务器上自建Git仓库就是最简单也最可控的方案。这篇文章我不会绕弯子直接从一台干净的Ubuntu服务器开始把Git的安装、服务端仓库创建、SSH免密配置、客户端日常推拉流程、分支与回滚操作全部走一遍最后再把我这些年踩过的坑整理成排错手册。适合两类人看一类是刚接触Linux服务器想在服务器上存代码的新手另一类是已经在用Git但一直用托管平台想切换到自建仓库的开发者。1. 项目定位自建Git仓库要解决什么问题动手敲命令之前我建议先花两分钟想清楚你要的到底是什么。很多人一上来就搜“Git服务器搭建”结果查到的教程动不动就让你装GitLab、配Nginx、开HTTPS最后搞出一台比业务服务器还重的机器。实际上大部分个人和小团队的需求远没有那么复杂。1.1 托管平台与自建方案怎么选先说说托管平台。GitHub、Gitee这类平台确实方便Pull Request、Issue、CI/CD、代码审查一应俱全前端交互做得也好公开项目几乎零成本。但有几个场景托管平台会让人别扭代码涉及商业机密不想放在第三方服务器团队处在内网环境外网访问受限或者公司对代码托管有合规要求不允许数据出域。这些时候自建仓库就成了更踏实的选项。自建方案也有两条路。一条是装GitLab这类重量级平台Web界面、权限管理、流水线全都有但同时也要面对内存占用高、升级维护复杂、安全补丁要自己盯等一系列问题。另一条就是我下面要讲的轻量方案服务器上只装Git本体用SSH协议做传输用裸仓库保存版本数据。没有图形界面没有数据库内存占用可以忽略维护成本几乎为零。对比项托管平台GitHub/GiteeGitLab自建Git裸仓库SSH本方案部署复杂度无高低内存占用无2GB起步极小Web界面/代码浏览完整完整无权限管理平台级平台级Linux文件权限适用人数不限团队级个人/小团队我这里默认你选的是第三列因为对于一个只需要“存代码、改代码、多人协作”的场景来说这已经足够了。你没有Web界面来点按钮但你要的东西一个都不少版本记录、分支管理、权限控制、多端同步。1.2 适用场景与整体技术栈这套方案适合下面这些场景个人开发者拥有一台Ubuntu云服务器或内网服务器想把代码集中管理而不是散落在各个电脑里。小团队三五个人以内需要一个公共代码仓库但不想花时间维护重量级平台。学习Git和Linux服务器操作想通过实际搭建来理解SSH、权限、仓库这些概念。开发环境是Windows/Mac但代码仓库在Linux服务器上需要一套全平台的协作流程。整体技术栈非常精简Ubuntu系统 Git SSH。没有数据库没有Web服务没有容器编排。我用了很久这种结构最大的体会就一个字——“稳”。因为组件少出故障的可能性自然就低。就算真的有天服务器磁盘坏了直接把仓库目录备份出来放到新机器上改一下客户端remote地址就能满血复活。如果是Windows电脑开发你甚至不用额外装什么客户端。Git官方自带Git BashVSCode也内置了SSH远程开发功能能在Windows上直接连接Ubuntu服务器做代码修改配合这套仓库方案体验和本地开发几乎没区别。2. 环境准备Ubuntu服务器与Git安装搭仓库之前先把服务器基础环境理顺。这里我默认你手上已经有一台能通过SSH登录的Ubuntu服务器版本无所谓18.04以上都可以20.04、22.04我都实测过。2.1 服务器基础配置登录服务器后的第一步永远是更新软件源这一步虽然基础但很多人会跳过然后发现Git装不上或者装上了版本特别老。sudo apt update sudo apt upgrade -yapt update会刷新软件源索引apt upgrade会把系统已有的软件包升级到最新版本。如果你拿到的是刚重装过系统的服务器这一步尤其重要否则后续安装可能因为依赖版本过旧而出错。接着确认SSH服务是运行状态。Ubuntu服务器版一般会自带OpenSSH Server但如果是最小化安装可能需要手动装一下。systemctl status ssh如果提示Unit ssh.service not found说明还没装执行下面两条命令sudo apt install openssh-server -y sudo systemctl enable --now sshenable --now的意思是设置开机自启并立即启动。这一步完成后你的服务器就具备远程管理的基本能力了。另外要注意防火墙。如果你的服务器开启了UFW防火墙记得放行22端口否则客户端怎么都连不上sudo ufw allow OpenSSH sudo ufw enable2.2 创建专用Git用户这一步别忽略很多人搭建Git仓库时习惯用root用户或者自己的管理员账号直接建仓库图省事。我个人强烈建议单独创建一个git用户来管理所有仓库。理由有三个第一安全隔离。Git用户只需要读写仓库目录的权限不需要系统管理权限即使SSH密钥泄露攻击者拿到的也只是一个受限Shell。第二权限清晰。所有仓库归属在git用户下新增协作成员时只需要把对方的公钥加到git用户的authorized_keys文件里其他目录一概不暴露。第三便于备份和迁移。所有仓库都集中在一个用户目录下备份的时候打包一个目录就够了。创建用户有两种方式第一次搞的话我推荐用adduser它会交互式地帮你设置密码并且自动创建好家目录sudo adduser git过程中会让你输入两次密码、填写一些用户信息那些信息可以直接回车跳过。如果服务器上没有adduser比如最小化安装可以用useradd手动创建sudo useradd -m git -s /bin/bash sudo passwd git-m参数表示创建家目录-s /bin/bash指定默认Shell为bash。这里要注意不要用-s /usr/sbin/nologin因为后续还要用这个用户登录服务器做仓库管理操作。2.3 安装Git并完成基础配置Ubuntu安装Git非常省事一条命令搞定sudo apt install git -y装完检查一下版本确认一切正常git --version输出类似git version 2.34.1就是OK的。如果你发现版本特别老可以在后续通过添加Git官方PPA的方式升级但对当前这套方案来说Ubuntu自带源里的版本已经完全够用了。接下来要给git用户设置Git的全局身份信息这一步经常被人漏掉。虽然大部分提交是从客户端发起的但有些场景比如在服务器上直接创建仓库、写钩子脚本也需要用到Git身份提前配好可以避免一堆莫名的报错。sudo -u git git config --global user.name Git Server sudo -u git git config --global user.email gitlocalhostsudo -u git表示以git用户的身份执行命令。邮箱这里随便写一个合法的格式就行它只是作为提交记录的一部分存在。到这里服务器端的基础环境就准备好了。用一句话概括系统更新了、SSH能连了、Git装了、专用用户建好了。3. 仓库搭建在服务器端创建裸仓库基础环境OK之后我们来创建真正的代码仓库。这里有一个概念必须先讲清楚否则后面会一直懵。3.1 为什么服务端要用裸仓库而不是普通仓库Git仓库有两种形态普通仓库working tree和裸仓库bare repository。普通仓库就是你平时在电脑上git init出来的那种它包含一个.git目录里面存的是版本历史、分支引用、配置信息同时还有一份你可以直接编辑的工作目录。你在工作目录里改文件git add、git commit后改动进入.git目录里的版本库。而裸仓库没有工作目录它直接把版本库的内容摊开放在仓库目录里。你可以把它理解成一台只用来存档的保险柜里面没有你日常办公的文件只有所有历史版本的备份。服务端用裸仓库的原因非常简单没有人会在服务器上直接改代码文件。服务器上的仓库只负责接收客户端的推送、保存版本数据、响应客户端的拉取请求。如果服务端用普通仓库还可能出现一个人改了服务器上的工作区文件另一个人push时产生一堆莫名其妙的冲突。做了一个不太精确但很好记的类比普通仓库是你的办公桌工作区就是摊开在桌上正在写的东西裸仓库是公司档案室里面只有归档好的卷宗不允许人在里面办公。3.2 创建仓库目录与裸仓库初始化我习惯把所有仓库都放在/home/git/repos目录下一个项目一个文件夹整洁又好备份。先把目录建出来并把所有权交给git用户sudo mkdir -p /home/git/repos sudo chown -R git:git /home/git/reposchown把目录拥有者改成git用户这一步如果不做后续客户端push代码时会因为目录权限不足而失败。这个权限问题在后面排查章节还会重点提。然后初始化第一个裸仓库。假设项目叫hello-worldsudo -u git git init --bare /home/git/repos/hello-world.git执行后会输出类似Initialized empty Git repository in /home/git/repos/hello-world.git/说明创建成功。注意仓库名后面带.git后缀这是约定俗成的命名方式告诉你这是一个裸仓库不是普通项目目录相当于给文件夹加了个类型标签。初始化完成后进仓库目录看一眼结构ls -la /home/git/repos/hello-world.git/会看到HEAD、branches、config、description、hooks、objects、refs这些文件和目录。这些就是Git的“内脏”其中objects保存了所有的提交对象和数据对象refs保存了分支和标签的引用HEAD指向当前分支。以后每建一个新项目就重复一次这两步操作sudo mkdir -p /home/git/repos sudo -u git git init --bare /home/git/repos/项目名.git如果是团队多人用每次都给用户创建目录太麻烦。Git仓库本身不限制用户数量只要客户端SSH密钥能被服务器认可就行。但如果你需要更加细粒度的权限控制可以后面给每个项目建一个Unix用户组把成员加进去用文件系统权限来限制访问。这一层暂时不展开先保证跑通。从服务器端来看仓库的搭建到这里就结束了一共就三条命令。接下来真正的重头戏在客户端入口——SSH密钥配置。4. SSH密钥与远程连接配置服务器端仓库已经就位但客户端还不能直接使用。客户端要通过SSH协议连接服务器而SSH的认证方式里最推荐的就是密钥认证。这一节我会把客户端分Windows和Linux两种情况讲。4.1 为什么选SSH协议而不是HTTPGit可以通过HTTP、HTTPS、SSH、git协议等多种方式访问远程仓库。在自建仓库这个场景里SSH是绝对的优先选择。HTTP/HTTPS方式需要你在服务器上安装并配置Web服务Nginx、Apache再配合Git的CGI后端部署流程长而且每次push都要输入账号密码。SSH方式则完全绕开了这一步Git天生支持SSH传输服务器上只要存在SSH服务Git就能直接使用。说白了你的Ubuntu服务器本来就开着SSH服务为什么不直接利用起来少装一个Web服务就等于少了一堆潜在的安全风险和故障点。4.2 生成SSH密钥对在客户端电脑上打开终端Windows可以用Git Bash或者PowerShell执行ssh-keygen -t ed25519 -C your_emailexample.com-t ed25519指定算法为ED25519这是目前安全性和性能都很优秀的非对称加密算法生成的密钥短小精悍。如果你的Git版本比较老不支持ED25519可以用-t rsa -b 4096代替效果类似。执行过程中会出现几个交互式问题Generating public/private ed25519 key pair. Enter file in which to save the key (/home/user/.ssh/id_ed25519): Enter passphrase (empty for no passphrase): Enter same passphrase again:默认路径直接回车即可。passphrase建议设置一个它相当于密钥的“登录密码”就算别人拿到了你的私钥文件没有这个口令也没法使用。缺点就是每次push会多输一次口令不过后面可以配置SSH agent来记住它。生成完成后会得到两个文件私钥id_ed25519和公钥id_ed25519.pub。私钥留在本地公钥需要传到服务器上。4.3 公钥部署与权限配置把公钥发到服务器最方便的命令是ssh-copy-idssh-copy-id git服务器IP执行后会提示输入git用户的密码然后自动把本地的公钥追加到服务器的~/.ssh/authorized_keys文件中。如果你的系统没有ssh-copy-idWindows上比较常见就手动操作先登录服务器创建.ssh目录并给足权限然后编辑authorized_keys文件把公钥内容粘进去。# 进入git用户的家目录 sudo -u git mkdir -p /home/git/.ssh sudo -u git touch /home/git/.ssh/authorized_keys sudo -u git chmod 700 /home/git/.ssh sudo -u git chmod 600 /home/git/.ssh/authorized_keys权限这一步无论如何都不能省。.ssh目录权限必须是700authorized_keys文件必须是600权限过宽会导致OpenSSH直接忽略这个文件连接时依然报Permission denied。这是新手遇到最多的一个问题。然后编辑/home/git/.ssh/authorized_keys把客户端id_ed25519.pub文件的全部内容追加进去文件末尾要确保有一个换行符。4.4 测试远程连接一切配置完成后在客户端执行ssh git服务器IP如果一切正常SSH会建立连接。由于git用户默认没有配置任何Shell提示语连接上去后可能只是一闪而过进入一个不会显示提示符的Shell或者直接显示一些系统信息后退出。这些都是正常的说明密钥认证已经生效。这里顺带提一个后续可以做的安全加固把git用户的Shell改成git-shell这样登录者只能执行Git相关命令无法执行ls、cat等普通Shell命令进一步缩小暴露面。不过新手阶段不改也完全够用。5. Git基本使用流程从克隆到推拉的完整闭环服务器端仓库建好了SSH密钥也通了现在可以正式开始使用Git了。这一节我用一个完整的最小流程把从拉取项目到推送改动讲透。5.1 克隆仓库并完成首次提交在客户端电脑上执行git clone git服务器IP:/home/git/repos/hello-world.gitSSH连接串的格式是用户名主机地址:仓库绝对路径。这里主机地址写服务器IP后面跟的是仓库在服务器上的绝对路径也就是刚才创建的裸仓库路径。执行后会在当前目录生成一个hello-world文件夹里面是空的因为裸仓库里没有任何提交。进入这个目录创建一个README文件然后提交cd hello-world echo # Hello World README.md git add README.md git commit -m first commit git push origin master不要小看这几条命令它们恰好串联了Git的四个核心区域工作区README.md、暂存区git add之后、本地仓库git commit之后、远程仓库git push之后。如果push后控制台输出类似To git服务器IP:/home/git/repos/hello-world.git和* [new branch] master - master说明推送成功。此时再回头看裸仓库目录会看到objects目录下出现了实际的数据对象说明第一个版本已经完整入库。5.2 日常协作拉取、提交、推送从第二个版本开始流程就固定下来了。假设你在另一台电脑上继续开发这也是团队协作的常见场景先克隆一份代码到自己机器上。git clone git服务器IP:/home/git/repos/hello-world.git cd hello-world新建一个文件修改或删除已有的文件然后执行一套“三步走”git add . git commit -m fix: 修复登录页按钮样式 git push这里有个问题值得展开说一下git add和git commit到底在做什么git add是把工作区里的文件变更加入暂存区相当于把待提交的改动放进一个“购物车”。git commit则是把购物车里的改动打包成一个不可变的版本记录生成一个commit对象。每个commit都有一个唯一的SHA-1哈希值和时间戳可以在需要的时候随时回退到这个版本。git push则是把本地仓库的commit推送到远程服务器。如果push时本地比远程落后Git会拒绝推送提示non-fast-forward这种情况一般是远程仓库有别人提交了新代码。处理方式就是先拉取再推送git pull git pushgit pull实际上做了两件事从远程拉取最新数据fetch并把远程分支合并到当前分支merge。如果远程的改动和本地改动没有冲突合并会自动完成然后就能直接push了。如果有冲突Git会在冲突文件里用、、标记出双方改动需要手动编辑文件解决冲突后再提交。5.3 用git log查看提交历史版本控制最大的价值在于可以看清每个文件从哪来、到哪去。git log命令是查看历史的入口。git log --oneline --graph --all输出大概长这样* 3a1f2d9 (HEAD - master, origin/master) fix: 修复登录页按钮样式 * b32c6e8 feat: 新增用户注册模块 * a1402b5 first commit--oneline让每条提交只显示一行--graph用ASCII字符画出分支图形--all显示所有分支的提交。这几项组合起来基本能满足日常查看需求。如果想知道某个文件的最新一次改动是谁做的可以用git blame README.md6. 分支管理与版本回滚仓库能用了、提交会了接下来就到了Git真正值钱的部分分支管理和历史回退。在服务器上自建仓库尤其推荐掌握这块能力因为它能让你在代码出问题时不慌——反正随时能回到上一个正常版本。6.1 分支的新建、切换与合并分支就是一条独立的开发线。在master或main主线上你可以开出一个临时分支来开发新功能开发完再合并回去整个过程不干扰主线。创建一个新分支并切换过去git checkout -b feature/login这条命令相当于先git branch feature/login再git checkout feature/login。在feature分支上做改动并提交然后切回主分支git checkout master把feature分支合并进来git merge feature/login如果合并过程中没有冲突Git会自动生成一个merge commitfeature分支的改动就全部进入master了。合并完成后你觉得feature分支已经没有保留价值可以删掉git branch -d feature/login一个小建议项目里尽量少在master上直接改代码所有功能都走“新建分支 → 开发 → 合并 → 删除分支”的循环。一旦养成这个习惯你的master永远是一个干净稳定的版本出问题也能轻松定位是哪次合并引入的。6.2 三种reset回滚方式与revert的区别人在河边走哪能不湿鞋。代码提交错了、改的东西不对、或者就是莫名其妙搞挂了需要的操作就是回退。git reset有三种模式区别在于回退时影响的范围git reset --soft HEAD~1 git reset --mixed HEAD~1 # 默认模式 git reset --hard HEAD~1--soft撤销最近一次commit但保留暂存区和工作区的内容。相当于“反悔提交但保留改动”适合提交信息写错或漏了文件时用。--mixed默认撤销commit和暂存区只保留工作区改动。适合“提交了我本来不想提交的东西但我还想继续修改”的场景。--hard撤销commit、清空暂存区、恢复工作区到指定版本。改动彻底消失适合代码彻底坏了、不想要的情况。HEAD~1表示当前提交的父提交也就是最近一次提交之前。想回退多个版本就写HEAD~2、HEAD~3。与reset相对的另一个命令是revertgit revert HEADrevert不是“撤销”而是“逆向操作”。它会根据上一次提交的内容生成一个反向的新提交把代码恢复到那个版本的状态同时保留原来的提交记录。这在协作场景里非常重要因为reset会改写历史一旦改动已经被别人拉取过reset容易引发团队之间版本信息不一致而revert始终往历史里追加新提交不会破坏别人的本地记录。我的习惯是还没推送的commit用reset随便折腾已经推送并且可能被别人拉取的commit绝对不用reset --hard而是用revert来安全回滚。6.3 commit --amend修改最近一次提交有时候匆忙提交之后发现提交信息写错了、漏了一个文件、或者commit message有拼写错误。这种场景用git commit --amend可以直接修正最近一次提交代替原来的提交生成一个新的commit。git add 漏掉的文件 git commit --amend -m 修正后的提交信息如果只是想改提交信息不加git add直接执行也行git commit --amend -m 新的提交信息要注意的是amend同样是在改写提交历史和reset一样只适合处理还没推送的commit。如果已经push了又去amend下次push会被拒绝需要对远程做强制推送才能同步。强制推送在团队协作里属于高风险操作能不碰就别碰。7. 常见问题与排查技巧实录自建Git仓库服务器和网络环境千差万别遇到问题在所难免。这一节我把这几年实际踩过的坑、被问过的问题集中整理出来按场景分类给出一套可行的排查思路。7.1 SSH连接失败怎么办这是新手最高频的报错没有之一。错误信息通常是Permission denied (publickey)或者Connection timed out。先说Permission denied (publickey)。看到这个信息说明服务器已经响应了你的连接请求但拒绝了你的认证。排查顺序确认你连接的用户名是git而不是root或自己建的其他用户ssh git服务器IP确认公钥已经追加到/home/git/.ssh/authorized_keys文件中并且文件末尾有换行检查服务器端的.ssh目录权限是否为700authorized_keys权限是否为600检查客户端用的是不是正确的私钥ssh -v git服务器IP可以打印详细连接过程看是否用了id_ed25519文件如果本地同时存在多个密钥对需要修改客户端的~/.ssh/config文件指定使用哪个私钥Host myserver HostName 服务器IP User git IdentityFile ~/.ssh/id_ed25519再说Connection timed out。这个报错说明客户端根本没连上服务器的22端口。原因很可能是防火墙没有放行22端口或者服务器IP地址填写错误。另外很多云服务器厂商除了系统防火墙还有一层安全组策略需要在云控制台里手动放行22端口的入站流量这个和系统内的UFW是两码事少一个都会连不上。7.2 推送被拒绝non-fast-forward的处理推送被拒绝时Git会明确提示! [rejected] master - master (fetch first)或(non-fast-forward)。这个问题的根源只有一个远程仓库有本地没有的提交。处理方式也很简单git pull git pushgit pull完成远程数据的拉取和合并如果合并顺利push就能成功。如果合并时产生冲突编辑冲突文件、保留正确内容、执行git add和git commit完成合并然后再push。从根上避免这个问题的方法是推送前养成先pull的习惯或者在push之前检查一下远程状态git fetch git status7.3 Git中文乱码与显示异常在Windows上用Git管理含中文文件名的代码库时经常会发现git status显示的文件名是一堆转义字符比如\346\265\213\350\257\225。这其实是Git的默认转义行为目的防乱码但对中文用户来说反而增加了阅读难度。关闭转义让中文正常显示执行git config --global core.quotepath false另外如果commit message里的中文在终端里显示乱码通常是终端编码和Git编码不一致导致的。Ubuntu服务器端一般默认UTF-8Windows的Git Bash也默认UTF-8基本不用额外配置。如果用的是老式CMD窗口可以执行chcp 65001把代码页切到UTF-8再操作。7.4 问题排查速查表把以上问题和最常用的排查命令整理成一张表方便收藏现象可能原因排查命令/操作Permission denied (publickey)公钥未部署或权限错误检查authorized_keys、.ssh目录权限Connection timed out防火墙/安全组未放行22端口systemctl status ssh、ufw status、云控制台检查git命令找不到PATH未配置或未安装which git、apt install git推送被拒绝远程有本地没有的提交git pull 后重新 push中文文件名转义core.quotepath默认开启git config --global core.quotepath false服务器上直接git init后push失败目录归属或权限不足chown -R git:git /home/git/reposVSCode连不上远程服务器SSH服务未运行或密钥未配置确认ssh服务状态配置公钥到authorized_keys这里把“在服务器上直接git init后push失败”单独拿出来说一句。很多人图方便会在服务器上用普通仓库而不是裸仓库来存代码然后在客户端push时收到一个remote: error: refusing to update checked out branch的报错。这个错误本质上是Git拒绝覆盖远程仓库的工作区文件。解决方法就是按本文的方案使用裸仓库不要在服务器上保留工作区。我在实际维护这套方案的过程中最大的感受是Git仓库本身只是一个很小的软件真正让整套体系跑得稳的是规范的目录结构、合理的用户权限和克制的功能取舍。别一上来就想着搞复杂的权限系统、上容器、做负载均衡先把一条最简单的链路跑通后面什么都可以慢慢加。现在这套“Ubuntu Git裸仓库 SSH密钥”的方案我已经用了好几年换了三次服务器迁移成本都只是打个压缩包解压而已省下的运维时间都用在了写代码上。
返回列表