国内GitHub镜像站搭建全攻略
这篇东西其实想写很久了。镜像站(Mirror Site)这词在开源圈、运维圈不算新鲜,但很多朋友一听“镜像站”脑子里先想到的是“把整个 GitHub 全量同步下来”——我实话实说,这个目标既不现实也没必要。我自己在几家公司、实验室里陆续搭过三四套镜像服务,从最朴素的 Git 仓库裸同步方案到带 Web 管理界面的轻量平台都折腾过一轮,这篇文章就把一套“能用、够稳、不折腾”的完整方案摊开讲。
先说清楚它解决什么问题。最常见的场景是这么几个:企业内部网络隔离,开发机和公网之间带宽有限,几十个人同时拉同一个开源项目仓库,慢且不说,还特别容易超时;或者是某些离线环境需要定期获取一批固定的开源仓库做代码审计、依赖构建;再就是个人开发者手头有十几个高频使用的仓库,希望做一份本地备份,顺便让局域网里几台机器共用这份缓存。
这篇文章适合三类人看:被团队内频繁拉取外部代码仓库搞得带宽告急的运维同学;需要在离线或隔离环境里维护代码同步的研发负责人;还有单纯想搞一套个人代码缓存服务的技术爱好者。你不需要对 Git 内部原理有多深的理解,只要按步骤操作,配合文末的排查手册,基本能一次跑通。
1. 镜像站的整体设计思路与需求拆解
1.1 先搞清楚镜像站到底在“镜像”什么
很多人把“GitHub 镜像”想复杂了。站在协议层面看,Git 仓库的同步本质就是远端引用(refs)和对象(objects)的增量复制。一个开源仓库的完整内容,在服务端就是.git目录里那一堆对象文件。所以最底层的镜像服务不需要网页界面,不需要数据库,甚至不需要 Git 客户端之外的任何额外软件——只要能把远端仓库的引用和对象定期抓回来,再通过本地 Git 协议、HTTP 静态托管或者 SSH 对外提供读取服务,这就已经是一个完全合格的镜像站了。
我在第一套方案里就是用一个 Shell 脚本加 crontab 完成的。脚本遍历一份仓库清单,逐个执行git clone --mirror做初次全量同步,之后用git remote update --prune做增量更新。整个“站点”没有数据库、没有 Web 前端,对外就靠 Nginx 把目录暴露成http://mirror.internal/repo.git这种格式。读者直接用git clone http://mirror.internal/xxx.git就能拉代码,实际体验和从 GitHub 上拉几乎没差别。
但纯静态方案有它的短板:没有权限控制,任何人只要能访问这台机器就能 clone 全部仓库;也没有 Web 界面,想看看某个仓库的提交记录、分支情况不方便。于是后期我在另一套系统里换成了 Gitea——它是一个轻量级的 Git 服务端,二进制解压就能跑,自带 Web UI、用户体系和 Webhook,给团队成员分配只读账号很容易。本文后面的完整方案也会以 Gitea 为主线展开,因为它的覆盖场景更广,适合绝大多数中小团队。
1.2 为什么强调“指定仓库清单”而不是“全量同步”
GitHub 上的仓库数量级是千万级,算上仓库体积和增长速率,全量同步的地板就是几十 TB 起步,每天增量也是 TB 级别,这根本不是普通公司或个人的服务器能扛的。更重要的是,绝大多数团队根本用不上那么多仓库。
实际操作中我习惯先把仓库清单收进一个纯文本文件,按行写仓库地址,支持注释。比如:
# 核心构建依赖 https://github.com/某开源组织/某构建工具.git https://github.com/某开源组织/某解析库.git # 团队高频参考 https://github.com/某组织/某前端框架.git https://github.com/某开发者/某内网穿透组件.git这份清单就是整个镜像站的“配置中心”。新增一个同步对象,就往文件里追加一行,再跑一遍脚本就行。基于清单的另一个好处是权限边界非常清晰——镜像站只承载你明确声明要同步的仓库,不需要为“会不会同步了不该同步的东西”而焦虑。
1.3 几种主流方案选型对比
在动手之前,我把见过的方案和各自的适用场景列个表,帮助你自己判断该走哪条路线:
| 方案 | 核心组件 | 优点 | 缺点 | 适合场景 |
|---|---|---|---|---|
| 纯 Git 裸仓库 + Nginx | git clone --mirror+ Nginx | 最轻量、无外部依赖 | 无权限控制,无 Web 界面 | 个人缓存、内网完全可信环境 |
| Gitea / Gogs 同步仓库 | Gitea + 同步脚本 + Webhook | 有权限管理和 UI,支持 Web 触发同步 | 多一个 Java/Go 服务进程 | 中小团队内部代码分发 |
| GitLab 镜像功能 | GitLab 自带仓库镜像(Repository Mirror) | 官方功能,同步对用户透明 | 资源占用偏高,配置较重 | 中大型公司,已有 GitLab 体系 |
| 对象存储 + 文件网关 | 阿里云 OSS + 自定义同步层 | 容量弹性,支持大规模分发 | 架构复杂,维护成本高 | 大规模公共镜像服务 |
个人推荐普通人直接走第二条路线:Gitea。它单机部署内存占用很低(实测 512 MB 内存的空闲机器就能跑得很流畅),而且它对“外部仓库同步”这件事支持得比较自然——Gitea 支持从任意公开 Git 地址迁移仓库(Mirror 模式)。所谓 Mirror 模式,就是你在 Gitea 里创建一个“镜像仓库”,填上源仓库地址,Gitea 会在后台按你设置的间隔自动拉取更新。这套机制天然适合做内部镜像站,只是默认的同步间隔最小只能到几分钟一次,而对多数场景已经完全够用了。
2. 镜像同步的核心原理与关键机制
2.1 理解git clone --mirror和普通 clone 的区别
先补一个原理性的知识点,否则后面脚本里的参数容易让人懵。常规的git clone拿到的是一份“工作副本”:有.git目录,也有 checkout 出来的工作区文件,本地默认分支跟踪远端分支。而git clone --mirror拿到的是一份“裸镜像”:没有工作区文件,但.git里的一切——所有分支、标签、远端跟踪引用、Git 配置——都和源仓库保持一致。
一个容易忽视的细节是,--mirror会把远端的所有引用完整映射到本地。举例来说,如果源仓库有三十个分支、两百个标签,镜像仓库也会有完整的分支和标签。这也就意味着镜像仓库本身几乎就是源仓库在同步时刻的一份“完整快照”。
为什么要用--mirror而不是普通 clone?因为镜像站要承载团队成员的 clone 操作,每个成员 clone 的都是一个完整仓库。如果本地只是一个普通工作副本,分支不完整,别人 clone 过去一开始就缺信息。所以镜像仓库本身必须是“全量引用”的。
增量同步用git remote update --prune。这条命令会向远端仓库发起引用协商,只拉取本地缺失的对象。第一次全量可能耗时较长,后面基本就是秒级或毫秒级的小同步。定期跑这条命令就能维持镜像的新鲜度。我做测试时,一个约 300 MB 的开源仓库,在带宽稳定的情况下,初次全量大概几分钟,之后每五分钟一次的增量同步通常在几秒内完成。
2.2 关键细节:update-server-info与 HTTP 智能协议
如果镜像仓库只打算用 SSH 协议服务,update-server-info可以省掉。但只要你想通过 HTTP/HTTPS 让同事git clone http://mirror...,这个命令就绕不开。
Git 的 HTTP 智能协议依赖仓库内生成的info/refs文件,这个文件是服务端向客户端提供引用列表的“索引”。仓库引用更新后,如果info/refs没同步刷新,客户端拉取时可能会看到过期分支,甚至直接报错。
所以完整脚本里每次仓库更新后必须执行:
git --git-dir="$repo_dir" update-server-info我早期就栽过这个坑:同步脚本只跑了remote update,没跑update-server-info,结果用 Nginx 暴露的 HTTP 地址 clone 分支列表始终是旧的,排错排了一个下午。
2.3 清理与维护:git gc和对象压缩
镜像站跑久了,仓库体积会缓慢膨胀。因为每次增量同步都会引入新的对象,虽然 Git 的松散对象会周期性被自动压缩,但自动触发不完全可控——特别是当你手动做强制更新、或者仓库历史里有大量大文件变更时。
我建议在同步脚本里给每个仓库安排定期git gc。Gitea 或 GitLab 这类平台自带 GC 计划,而纯静态方案必须自己加。
实际执行时我习惯每月跑一次git gc --aggressive --prune=now。加--aggressive会做一次更彻底的对象压缩,代价是耗时长、CPU 占用高。仓库大的时候最好在凌晨低峰跑,避免影响正常拉取。
注意:不要在团队正集中拉代码的时段跑
git gc --aggressive,它会把仓库锁住一段时间。更稳妥的做法是只跑git gc --auto,让 Git 自己判断是否需要压缩。
3. 完整实操:从零搭建一套可用的 Git 镜像站
3.1 环境准备与基础依赖
我以 Ubuntu 22.04 LTS 为例,其他 Linux 发行版思路完全一致。需要准备的东西很少:
- 一台 Linux 服务器或虚拟机,建议至少 2 核 4 GB 内存、50 GB 可用磁盘(具体按仓库总量估算,一般给仓库预估体积留 1.5 倍余量)。
- 一个域名或者直接走 IP,内网使用的话用 IP 或内网 DNS 都行。
- 服务器能正常访问外网,能访问到 GitHub 官方地址。
先把系统基础依赖装好:
sudo apt update sudo apt install -y git nginx curl wgetGit 版本建议 2.30 以上,太老的版本对--mirror和update-server-info的支持不够完善。我实测 2.39 跑得很稳。
3.2 安装并初始化 Gitea
到 Gitea 官方发布页下载最新版二进制包(这里不写具体版本号,你操作时选最新的稳定版即可),然后放到/usr/local/bin:
sudo wget -O /usr/local/bin/gitea https://dl.gitea.com/gitea/@version@/gitea-@version@-linux-amd64 sudo chmod +x /usr/local/bin/gitea创建运行用户和目录:
sudo adduser --system --group --disabled-password --home /var/lib/gitea gitea sudo mkdir -p /var/lib/gitea/{custom,data,log} sudo chown -R gitea:gitea /var/lib/gitea sudo chmod -R 750 /var/lib/gitea用 systemd 托管 Gitea 进程:
# /etc/systemd/system/gitea.service [Unit] Description=Gitea (Git with a cup of tea) After=network.target [Service] User=gitea Group=gitea WorkingDirectory=/var/lib/gitea/ ExecStart=/usr/local/bin/gitea web --config /var/lib/gitea/custom/conf/app.ini Restart=always Environment=USER=gitea HOME=/var/lib/gitea GITEA_WORK_DIR=/var/lib/gitea [Install] WantedBy=multi-user.target启动并设为开机自启:
sudo systemctl daemon-reload sudo systemctl enable --now gitea首次访问http://服务器IP:3000会进入安装引导页。数据库选 SQLite 即可,中小团队完全够用;仓库根目录设为/var/lib/gitea/data/repositories。安装完用管理员账号登录后台,稍后会用到。
3.3 用 Gitea 创建“镜像仓库”
Gitea 建镜像仓库的路径是:右上角“+” → “新建迁移” → 选择“Git”迁移方式。填上源仓库的 Git 地址,镜像仓库名称和本地路径按自己习惯命名。
关键点在“镜像同步”这一栏:Gitea 支持两种同步方式——定期同步和 Webhook 触发同步。我个人强烈建议同时打开“同步间隔设置”,设成每 15 分钟或每 30 分钟一次,这样即使没有外部触发,也能保证镜像不会长期滞后。Gitea 的后台同步机制会自动对镜像仓库执行git remote update,不需要自己写 crontab。
你可以在同一页面配置“当源仓库发生 Webhook 推送时立即同步”。这个后面单独讲,因为第一次配置时很多人找不到入口,容易忽略。
3.4 用脚本批量同步多仓库(给纯静态方案或混合方案用)
如果你手头仓库很多,一个个去 Web 界面创建镜像仓库效率太低。我在混合方案里会直接用脚本先把仓库“种”下去,再让 Gitea 接管后续同步。
完整的同步脚本我放在下面,这个脚本适合两类场景:一是没有任何 Web 平台时,直接靠它加上 Nginx 对外服务;二是作为 Gitea 的“初始化工具”,批量生成裸仓库后导入。
#!/bin/bash # mirror_sync.sh - Git 镜像仓库批量同步脚本 # 用法: ./mirror_sync.sh set -euo pipefail # 配置区 REPO_LIST="/opt/mirror/repos.txt" # 仓库清单 MIRROR_ROOT="/data/git-mirror" # 镜像根目录,裸仓库都放这里 LOG_FILE="/var/log/mirror_sync.log" GIT="/usr/bin/git" mkdir -p "$MIRROR_ROOT" log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG_FILE" } sync_repo() { local repo_url="$1" local repo_name repo_name=$(basename "$repo_url" .git) local repo_dir="$MIRROR_ROOT/$repo_name.git" if [ -d "$repo_dir" ]; then log "更新仓库: $repo_name" $GIT --git-dir="$repo_dir" remote update --prune || log "警告: $repo_name 增量更新失败,尝试强制重建" $GIT --git-dir="$repo_dir" update-server-info || true $GIT --git-dir="$repo_dir" gc --auto || true else log "首次克隆: $repo_name" $GIT clone --mirror "$repo_url" "$repo_dir" $GIT --git-dir="$repo_dir" update-server-info fi date '+%Y-%m-%d %H:%M:%S' > "$repo_dir/.last_sync" chown -R gitea:gitea "$repo_dir" } # 逐行读取仓库清单,忽略注释和空行 grep -vE '^\s*(#|$)' "$REPO_LIST" | while IFS= read -r repo_url; do sync_repo "$repo_url" done log "本轮镜像同步完成"脚本用set -euo pipefail保证出错时及时退出,避免一个仓库的失败掩盖其他日志信息。repo_name用basename从 URL 里取出仓库名,拼成本地路径。初次同步会走git clone --mirror,后续全走增量更新。
仓库清单repos.txt示例:
# 我的核心镜像清单 https://github.com/某团队/某微服务框架.git https://github.com/某团队/某路由库.git https://github.com/某开发者/某命令行工具.git3.5 定时任务与 Webhook 触发
脚本方式依赖 crontab。打开 crontab 编辑器:
crontab -e加入一条每 30 分钟执行一次的规则:
*/30 * * * * /opt/mirror/mirror_sync.sh >> /var/log/mirror_sync_cron.log 2>&1如果你希望“源仓库只要一更新,镜像就立刻同步”,就要用 Git 自带的 Webhook 机制。GitHub 上在仓库的 Settings → Webhooks 里添加一个“镜像站触发地址”,Gitea 会暴露一个api/v1/repos/{owner}/{repo}/mirror-sync的 POST 接口。当 GitHub 源仓库发生 push 事件,GitHub 会向这个地址发请求,Gitea 收到后就立即拉起镜像同步。
这个模式的优点是镜像新鲜度极高,几乎实时;缺点是如果镜像仓库很多,或者源仓库 push 很频繁,同步任务会排长队。我实际使用的经验是:日常开发项目用 Webhook 模式,低频维护的老项目统一用定时同步,两套并存最合理。
3.6 Nginx 反向代理与 HTTPS 配置
Gitea 默认监听3000端口,直接裸奔不太好。我用 Nginx 做一层反向代理,把 80/443 端口转发给 Gitea,顺便补上 HTTPS。
Nginx 配置核心片段:
server { listen 80; server_name mirror.example.internal; client_max_body_size 1G; location / { proxy_pass http://127.0.0.1:3000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } }client_max_body_size必须调大,否则团队 push 大文件时会被 Nginx 以 413 拒绝。HTTPS 证书直接用内部 CA 或 Let's Encrypt 都行,内网环境我一般自签证书,并在团队成员机器上提前导入 CA 证书。
4. 常见问题与排查技巧实录
4.1 同步失败:网络临时抽风怎么办
镜像站对外提供的是服务,依赖的是外网拉取。GitHub 偶尔会抖动,git remote update偶发失败很正常。脚本里我已经加了失败前的日志提示,但更好的方式是配置重试机制。
给脚本加一层简单的重试,比如失败后等 30 秒再试,最多重试三次。我踩过最典型的坑是:网络抖动导致同步失败后,镜像停留在旧版本,团队拉代码撞见一个“突然回退的提交”,排查半天才发现是镜像没跟上,而不是代码真的回退了。所以同步失败必须要在日志和告警里体现出来。
4.2 仓库体积异常膨胀
镜像仓库在git gc --auto没有跑的时候会慢慢变大。几个藏在暗处的原因:
- 源仓库历史里有大文件,每次同步都会把历史对象完整拷贝。
- 有些仓库会频繁产生大量 tag,旧 tag 对象一直留着。
- Gitea 的定期同步默认不会主动清理远端已经删除的引用,除非你在镜像仓库设置里勾选了“同步删除远程分支”。
解决办法:对体积特别大的仓库单独设置git gc --aggressive --prune=now月度任务;同时确认 Gitea 镜像设置里“删除远程分支”选项是开启的。
4.3 Gitea 同步间隔最小为什么是分钟级
Gitea 的 Cron 任务那套机制里,同步间隔最小是一分钟,但对大多数场景已经足够。如果某个仓库需要秒级同步,我建议直接用外部 Webhook 触发,而不是把同步间隔调到一分钟然后频繁扫全量清单——那样极其浪费 I/O。
4.4 HTTP clone 时报错:repository not found
这个报错 90% 的原因是 Nginx 找不到对应的 Git 智能 HTTP 端点。检查两点:
- 确认 Gitea 的
app.ini中[server]段配置了正确的ROOT_URL,要和 Nginx 的对外地址一致。 - 确认仓库所有者、仓库名大小写是否匹配。Git 仓库名对大小写敏感,
Foo.git和foo.git是两个完全不同路径。
4.5 SSH clone 权限问题
如果团队通过 SSH 协议克隆,需要在 Gitea 后台给成员账号上传 SSH 公钥。常见坑是团队成员本地的 SSH key 没加进 ssh-agent,导致反复要求输入密码。另一个坑是 Gitea 默认 SSH 端口是 22,如果和服务器系统 SSH 冲突,需要改 Gitea 的 SSH 端口,并在客户端侧写清楚ssh://git@host:端口/owner/repo.git格式。
4.6 磁盘空间告警:双倍空间陷阱
最容易忽略的是“同一份仓库占两份空间”的情况。Gitea 做同步时是先拉取到临时目录再移动到正式仓库目录,如果磁盘余量刚好只够仓库体积的 1.1 倍,同步过程很可能因为临时文件写满而失败。我建议给镜像站所在分区至少留出所有仓库总大小的 1.5 倍空间,预留足够缓冲。
5. 一些老手的经验之谈
这套镜像站在我手里折腾了三个多月才稳定下来。第一个版本只用了纯脚本加 Nginx,脚本逻辑没问题,但团队里每个人都来问我“哪个仓库同步了没”这类问题,实在烦不胜烦,后来才补上 Gitea。第二个版本则踩了 Gitea 的 cron 同步间隔太死的坑,最后用 Webhook 补齐了实时性。
有几个习惯我认为是镜像站长期稳定运行的分水岭:
- 仓库清单必须用版本管理保存,每次增删仓库都是一次小发布,保留审计记录。
- 同步日志要放到独立文件并做轮转,不然跑半年日志就有几个 GB,排查问题反而难找关键点。
- 监控不只是看“同步成没成功”,更要关注“同步后仓库体积变化是否异常”——很多诡异问题都是从体积异常膨胀开始的。
如果你只是给个人电脑做一套本地缓存,那纯脚本方案十行命令就能搞定;但如果目标是服务一个团队,Gitea 加 Webhook 加 Nginx 这套组合值得从头搭一遍。镜像站本质上是一个“耐心工程”,基础设施的价值不在搭建的那一下,而在之后一年里它稳定、静默、零故障地不断把最新代码送到你手里。