为什么需要 Bare 仓库
在日常的 Git 工作流中,我们习惯于使用
1 | git clone |
或
1 | git init |
创建的普通工作仓库。这些仓库包含一个
1 | .git |
目录用于存储版本控制数据,以及项目的实际工作文件。然而,当你需要一个纯粹作为协作中心的远程仓库时——比如公司内部不想依赖 GitHub/GitLab 的场景——Bare 仓库就是最佳选择。
Bare 仓库(裸仓库)与普通仓库的核心区别在于:它没有工作目录,只包含 Git 对象数据库和引用。这意味着你无法直接在 Bare 仓库中编辑文件,但它可以完美地充当远程推送和拉取的中心节点。在服务器上,Bare 仓库就是 Git 协议的源站。
本文将从 Bare 仓库的基础概念出发,逐步带你构建一个具备权限控制、自动化钩子和镜像同步能力的轻量级自托管 Git 服务方案。

Bare 仓库的本质与内部结构
创建 Bare 仓库
创建一个 Bare 仓库非常简单:
1
2
3
4
5
6
7
8
9 # 在服务器上创建 bare 仓库
mkdir -p /opt/git/repos
git init --bare /opt/git/repos/my-project.git
# 查看目录结构
ls -la /opt/git/repos/my-project.git/
# 输出:
# HEAD branches/ config description hooks/ info/
# objects/ packed-refs refs/
注意目录名通常以
1 | .git |
结尾,这是 Git 裸仓库的命名惯例。整个目录内容等价于普通仓库中
1 | .git/ |
目录的内容。没有
1 | .git |
子目录,没有工作文件,只有版本控制的元数据和对象。
Bare 与 Non-Bare 的关键差异
| 特性 | 普通仓库 | Bare 仓库 | ||
|---|---|---|---|---|
| 工作目录 | 有 | 无 | ||
|
可以切换分支 | 不可用 | ||
|
正常使用 | 不可用 | ||
接收 |
默认拒绝(需配置 receive.denyCurrentBranch) | 正常接收 | ||
配置 |
false | true | ||
| 典型用途 | 开发者本地开发 | 服务器端远程中心 |
一个重要的细节:
1 | core.bare |
的值决定了 Git 如何解释仓库类型。你可以手动修改这个值来在两种模式间切换,但强烈不建议这么做——因为工作目录的存在与否是客观事实,改配置不会凭空生成或删除工作文件。
搭建 SSH 协议的 Git 服务器
Git 原生支持四种传输协议:Local、SSH、HTTP 和 Git。其中 SSH 协议是最常用的自托管方案,它兼顾了安全性和配置简便性。
创建 Git 专用用户
1
2
3
4
5
6
7
8
9 # 创建 git 用户,禁用 shell 登录
sudo useradd -m -s /usr/bin/git-shell git
# 可选:将你的 SSH 公钥添加到 git 用户的 authorized_keys
sudo mkdir -p /home/git/.ssh
sudo cp ~/.ssh/id_rsa.pub /home/git/.ssh/authorized_keys
sudo chown -R git:git /home/git/.ssh
sudo chmod 700 /home/git/.ssh
sudo chmod 600 /home/git/.ssh/authorized_keys
这里使用
1 | git-shell |
作为登录 shell 是一个安全最佳实践。
1 | git-shell |
是 Git 自带的受限 shell,只允许执行
1 | git push |
、
1 | git pull |
等 Git 操作,用户无法获得完整的 bash 访问权限。
配置仓库权限与共享
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 # 将仓库所有权交给 git 用户
sudo chown -R git:git /opt/git/repos/my-project.git
# 启用共享仓库模式
cd /opt/git/repos/my-project.git
git config core.sharedRepository group
# 设置组写权限
sudo chmod -R g+ws .
# 创建 git 用户组并添加成员
sudo groupadd gitusers
sudo usermod -aG gitusers git
sudo usermod -aG gitusers alice
sudo usermod -aG gitusers bob
1 | core.sharedRepository |
设置为
1 | group |
(或
1 | true |
、
1 | 0x660 |
等)后,Git 会自动确保新建的对象文件和引用具有正确的组权限,避免多用户协作时的权限冲突问题。
克隆与推送测试
1
2
3
4
5
6
7
8
9 # 从客户端克隆
git clone git@your-server:/opt/git/repos/my-project.git
# 添加文件并推送
cd my-project
echo '# Hello' > README.md
git add .
git commit -m 'Initial commit'
git push origin main

利用 Hooks 实现自动化工作流
Bare 仓库的
1 | hooks/ |
目录是自托管 Git 服务中最强大的自动化入口。与普通仓库的客户端钩子不同,Bare 仓库的服务端钩子可以对所有推送者的行为进行统一管控。
post-receive:推送后的自动部署
1 | post-receive |
是最常用的服务端钩子,它在所有引用更新完成后触发。以下是实现自动部署到 Web 目录的脚本:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25 #!/usr/bin/env bash
# /opt/git/repos/my-project.git/hooks/post-receive
DEPLOY_DIR="/var/www/my-project"
GIT_DIR="/opt/git/repos/my-project.git"
while read oldrev newrev refname; do
branch=$(git rev-parse --symbolic --abbrev-ref "$refname")
if [ "$branch" = "main" ]; then
echo "Deploying branch $branch to $DEPLOY_DIR..."
# 使用 git archive 导出文件(无需 clone)
mkdir -p "$DEPLOY_DIR"
git --work-tree="$DEPLOY_DIR" --git-dir="$GIT_DIR" checkout -f main
# 执行部署后脚本
cd "$DEPLOY_DIR"
if [ -f deploy.sh ]; then
bash deploy.sh
fi
echo "Deployment complete."
fi
done
关键技巧:
1 | git --work-tree=... --git-dir=... |
允许你从 Bare 仓库中检出文件到指定目录,而无需先 clone 整个仓库。这是 Bare 仓库特有的用法,因为普通仓库的工作目录就是当前目录,无需指定。
pre-receive:推送前的权限校验
1 | pre-receive |
在引用更新之前触发,如果脚本返回非零退出码,整个推送将被拒绝:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37 #!/usr/bin/env bash
# /opt/git/repos/my-project.git/hooks/pre-receive
# 规则1:禁止强制推送到 main 分支
while read oldrev newrev refname; do
branch=$(git rev-parse --symbolic --abbrev-ref "$refname")
# 检测是否为强制推送
if [ "$branch" = "main" ]; then
merge_base=$(git merge-base "$oldrev" "$newrev" 2>/dev/null)
if [ "$merge_base" != "$oldrev" ]; then
echo "ERROR: Force push to main branch is not allowed!"
exit 1
fi
fi
done
# 规则2:检查提交者邮箱是否在允许列表中
ALLOWED_DOMAINS="@company.com @company.cn"
while read oldrev newrev refname; do
for commit in $(git rev-list "$oldrev".."$newrev"); do
email=$(git show -s --format='%ae' "$commit")
matched=false
for domain in $ALLOWED_DOMAINS; do
if [[ "$email" == *"$domain"* ]]; then
matched=true
break
fi
done
if [ "$matched" = false ]; then
echo "ERROR: Commit $commit by $email is not from allowed domain!"
exit 1
fi
done
done
exit 0
update:细粒度的分支级控制
1 | update |
钩子与
1 | pre-receive |
类似,但它是为每个被更新的引用单独调用一次,而非批量处理。签名格式为
1 | update <ref> <oldrev> <newrev> |
,更易于编写针对特定分支的规则:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23 #!/usr/bin/env bash
# /opt/git/repos/my-project.git/hooks/update
refname="$1"
oldrev="$2"
newrev="$3"
branch=$(git rev-parse --symbolic --abbrev-ref "$refname")
# 保护 release/* 分支:只允许合并提交
case "$branch" in
release/*)
for commit in $(git rev-list "$oldrev".."$newrev"); do
parents=$(git show -s --format='%P' "$commit" | wc -w)
if [ "$parents" -lt 2 ]; then
echo "ERROR: Only merge commits allowed on $branch."
echo "Commit $commit is a non-merge commit."
exit 1
fi
done
;;
esac
exit 0
Bare 仓库与 Git Worktree 的协同实战
前面提到 Bare 仓库没有工作目录,但
1 | git worktree |
命令为 Bare 仓库打开了一扇新的大门——你可以在一个 Bare 仓库上附加多个工作树,每个工作树检出不同的分支。这在 CI/CD 和多环境部署场景下极为实用。
为 Bare 仓库添加工作树
1
2
3
4
5
6
7
8
9
10 # 从 bare 仓库创建工作树
cd /opt/git/repos/my-project.git
git worktree add /var/www/staging staging
git worktree add /var/www/production main
# 查看所有工作树
git worktree list
# /opt/git/repos/my-project.git (bare)
# /var/www/staging abc1234 [staging]
# /var/www/production def5678 [main]
现在,每当开发者推送代码到
1 | staging |
分支,你可以通过
1 | post-receive |
钩子自动在 staging 工作树中拉取最新代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21 #!/usr/bin/env bash
# post-receive hook with worktree auto-update
while read oldrev newrev refname; do
branch=$(git rev-parse --symbolic --abbrev-ref "$refname")
case "$branch" in
staging)
echo "Updating staging worktree..."
git --work-tree=/var/www/staging checkout staging
cd /var/www/staging && npm install && npm run build
echo "Staging updated and built."
;;
main)
echo "Updating production worktree..."
git --work-tree=/var/www/production checkout main
cd /var/www/production && npm install && npm run build
echo "Production updated and built."
;;
esac
done
这种方案的优势在于:多个工作树共享同一个 Git 对象数据库,无需为每个环境完整 clone 一份仓库,既节省磁盘空间,又保证了引用的一致性。

构建自动化镜像同步方案
许多团队需要在自托管仓库和 GitHub/GitLab 之间保持双向同步——可能是出于灾备、开源发布或合规审计的需求。利用 Bare 仓库的
1 | post-receive |
钩子和
1 | git mirror |
功能,可以构建一套零延迟的镜像同步方案。
单向镜像:自托管到 GitHub
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24 #!/usr/bin/env bash
# /opt/git/repos/my-project.git/hooks/post-receive
# 镜像推送到 GitHub
GITHUB_REMOTE="github-mirror"
while read oldrev newrev refname; do
# 跳过删除操作
if [ "$newrev" = "0000000000000000000000000000000000000000" ]; then
continue
fi
branch=$(git rev-parse --symbolic --abbrev-ref "$refname")
# 只同步 main 和 release/* 分支
case "$branch" in
main|release/*)
echo "Mirroring $branch to GitHub..."
git push "$GITHUB_REMOTE" "$refname" 2>&1 || {
echo "WARNING: Mirror push to GitHub failed for $branch"
}
;;
esac
done
首先需要在 Bare 仓库中添加 GitHub 远程:
1
2 cd /opt/git/repos/my-project.git
git remote add github-mirror git@github.com:company/my-project.git
完整双向镜像同步
双向同步需要额外处理:从上游拉取变更并注入到本地仓库。推荐使用一个独立的同步脚本配合 cron:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26 #!/usr/bin/env bash
# /opt/git/scripts/sync-mirror.sh
REPO_DIR="/opt/git/repos/my-project.git"
UPSTREAM="github-mirror"
LOG_FILE="/var/log/git-mirror.log"
echo "[$(date)] Starting mirror sync..." >> "$LOG_FILE"
cd "$REPO_DIR" || exit 1
# 从 GitHub 拉取所有引用
git fetch "$UPSTREAM" '+refs/*:refs/*' 2>&1 >> "$LOG_FILE"
# 检查是否有新提交需要同步到本地分支
for branch in $(git branch -r | grep "$UPSTREAM" | sed "s|$UPSTREAM/||"); do
local_sha=$(git rev-parse "refs/heads/$branch" 2>/dev/null)
remote_sha=$(git rev-parse "refs/remotes/$UPSTREAM/$branch" 2>/dev/null)
if [ "$local_sha" != "$remote_sha" ] && [ -n "$remote_sha" ]; then
echo "[$(date)] Syncing $branch: $local_sha -> $remote_sha" >> "$LOG_FILE"
git update-ref "refs/heads/$branch" "$remote_sha"
fi
done
echo "[$(date)] Mirror sync complete." >> "$LOG_FILE"
配合 crontab 实现定时同步:
1
2 # 每5分钟同步一次
*/5 * * * * /opt/git/scripts/sync-mirror.sh
使用 git clone –mirror 实现一键完整镜像
如果你需要创建一个与上游完全一致的只读镜像仓库,
1 | git clone --mirror |
是最简单的方式:
1
2
3
4
5
6
7
8
9 # 创建镜像仓库
git clone --mirror git@github.com:company/large-project.git
# 定期更新
cd large-project.git
git remote update
# --mirror 等价于 --bare + 配置 remote.origin.fetch 为 +refs/*:refs/*
# 这意味着它不仅同步分支,还同步标签、notes 等所有引用
安全加固与最佳实践
SSH 密钥管理
对于多用户环境,不要把所有公钥都写入
1 | authorized_keys |
。推荐使用
1 | ssh-command |
方案或轻量级授权管理工具:
1
2
3
4 # /home/git/.ssh/authorized_keys 格式示例
# 通过 command= 限制每个密钥可执行的命令
command="/usr/local/bin/git-auth alice",no-port-forwarding,no-X11-forwarding,no-agent-forwarding ssh-rsa AAAA... alice@laptop
command="/usr/local/bin/git-auth bob",no-port-forwarding,no-X11-forwarding,no-agent-forwarding ssh-rsa BBBB... bob@desktop
其中
1 | git-auth |
脚本可以根据用户身份实施仓库级别的访问控制:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28 #!/usr/bin/env bash
# /usr/local/bin/git-auth
USER="$1"
ORIGINAL_CMD="$SSH_ORIGINAL_COMMAND"
# 解析仓库名
REPO=$(echo "$ORIGINAL_CMD" | grep -oP "'/?.+.git" | tr -d "'")
# 访问控制列表
case "$USER" in
alice)
ALLOWED="my-project shared-lib"
;;
bob)
ALLOWED="my-project api-service infra-config"
;;
esac
# 验证权限
for repo in $ALLOWED; do
if [[ "$REPO" == *"$repo"* ]]; then
exec git-shell -c "$ORIGINAL_CMD"
fi
done
echo "Access denied: $USER cannot access $REPO"
exit 1
日志与审计
在
1 | post-receive |
钩子中记录所有推送事件,为安全审计提供数据:
1
2
3
4
5
6
7
8
9
10
11 #!/usr/bin/env bash
# 在 post-receive 开头添加审计日志
AUDIT_LOG="/var/log/git-audit.log"
PUSHER="$GL_USER" # 如果使用 Gitolite; 否则用 $USER
while read oldrev newrev refname; do
branch=$(git rev-parse --symbolic --abbrev-ref "$refname")
timestamp=$(date '+%Y-%m-%d %H:%M:%S')
echo "$timestamp | $PUSHER | $branch | $oldrev -> $newrev" >> "$AUDIT_LOG"
done
性能优化建议
- 定期执行
:对 Bare 仓库执行1git gc1git gc --aggressive
可以压缩对象存储,减少磁盘占用和克隆时间。建议在低峰期通过 cron 每周执行一次。
- 启用
:在大仓库中,设置为1core.preloadIndex1true
可以并行加载索引,加速
1git status和
1git diff操作。
- 配置
:限制打包时的内存使用,防止1pack.windowMemory1git gc
或
1git push在大仓库上消耗过多内存:
1git config pack.windowMemory 256m - 使用
:将所有松散对象和旧 pack 文件合并为一个新 pack,消除冗余。1git repack -a -d
完整方案:从零搭建的自动化流程
将上述所有组件组合起来,我们得到一套完整的自托管 Git 服务方案:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 # 1. 创建目录结构
sudo mkdir -p /opt/git/repos
sudo mkdir -p /opt/git/scripts
sudo mkdir -p /var/log/git
# 2. 创建 bare 仓库
sudo git init --bare /opt/git/repos/my-project.git
# 3. 添加 GitHub 镜像远程
cd /opt/git/repos/my-project.git
git remote add github-mirror git@github.com:company/my-project.git
# 4. 创建 worktree 用于部署
git worktree add /var/www/staging staging
git worktree add /var/www/production main
# 5. 设置权限
sudo chown -R git:git /opt/git
sudo chown -R git:git /var/www/staging /var/www/production
总结与对比
通过 Bare 仓库 + Hooks + Worktree 的组合,我们构建了一套轻量但功能完备的自托管 Git 服务。下面与主流方案做个对比:
| 方案 | 资源占用 | 学习成本 | 权限管理 | CI/CD 集成 | 适用场景 |
|---|---|---|---|---|---|
| 本文方案(Bare+Hooks) | 极低 | 中等 | 自定义脚本 | 原生支持 | 小团队、内网环境 |
| Gitolite | 低 | 较高 | 配置文件驱动 | 需额外配置 | 中团队、精细权限 |
| Gitea | 中等 | 低 | Web 界面 | 内置 Actions | 中团队、Web 管理 |
| GitLab CE | 高(4GB+ RAM) | 低 | Web 界面 | 内置 CI/CD | 大团队、全功能 |
本文方案的核心价值在于:极致轻量——一台 512MB 内存的最小 VPS 就能稳定运行,无需数据库,无需 Web 服务器,只需 SSH 和 Git。同时,通过 Hooks 的可编程性,你可以实现与 GitLab CI 等重量级方案相同的自动化效果,只不过以脚本的方式呈现,更加透明和可控。
当团队规模增长到需要 Web 界面管理、代码审查和 Issue 追踪时,再平滑迁移到 Gitea 或 GitLab 即可——你的 Bare 仓库可以直接作为它们的存储后端,无需重新导入历史。
汤不热吧