第一次接触 GitHub 镜像站,还是因为一个很现实的问题:团队里几个人同时从 GitHub 拉代码,拉到一半 CI 红了一大片。查了半天,不是代码问题,是网络问题。后来我意识到,与其反复等网络恢复,不如自己搭一个镜像站,把关键仓库和 Release 制品缓存到自己的服务器上。
GitHub 镜像站不是新概念,像很多企业、高校内部都有类似的东西。它的核心价值就一句话:让你和你的团队不再每次穿透公网去请求 GitHub 官方服务,而是直接从本地服务器拉取一份同步好的副本。这篇文章我会把搭建镜像站的三条路线、完整实操步骤、以及我踩过的各种坑全部写出来,适合想给团队做内网仓库源、或者想给项目做备份的开发和运维同学参考。
1. 镜像站到底解决什么问题
1.1 三个典型场景,你是哪一种
第一个场景是"开发依赖加速"。你们团队的项目引用了某个 GitHub 开源库,每次构建都要拉 tag、子模块、甚至整个仓库历史。如果这个仓库体积大、历史长,每次全量 clone 都会浪费大量时间。镜像站在这里的角色就是一台缓存机:同步一次,后面所有人都从镜像服务器拉,速度稳定了,带宽也省了。
第二个场景是"制品分发缓存"。很多项目的 Release 会附带编译好的二进制包,比如.tar.gz、.dmg、.exe,这些文件普遍几十上百 MB,甚至几个 GB。如果你有内网分发需求,把这些 Release 制品也同步到镜像站,下载体验会好非常多。
第三个场景是"数据备份与冗余"。GitHub 上的仓库可能被作者删除、改名、或者由于各种原因暂时不可用。对于那些你团队真正依赖的上游仓库,镜像一份到自己的服务器,等于买了一份保险。上游没了,你手里还有一份完整的代码和历史。
1.2 镜像站的边界:不是"复制粘贴"那么简单
很多人以为镜像就是把代码复制一份,其实远不止这些。一个完整的镜像仓库,至少包含:全部分支、全部 tag、完整的 commit 历史、Release 附件,以及 Git LFS 对象。如果你只复制了仓库里"当前最新代码",那叫快照;如果定期与上游同步保持更新,才叫镜像。
这里要提醒一点:镜像只是缓存,不改变软件许可证。你把 MIT、Apache-2.0 协议的项目放到内网,没问题,但如果对外提供服务,必须保留上游版权声明和许可证原文。另外,镜像也不代表永久可靠,它依然依赖定时同步和存储资源,是需要长期维护的。
2. 方案选型拆解:三条路线
2.1 轻量路线:裸仓库加 Nginx 静态托管
这条路线适合只需要镜像一两个仓库、或者纯粹做下载加速的场景。做法很简单:用git clone --mirror把远程仓库克隆成裸仓库,再用 Nginx 或任何静态文件服务器把目录暴露出去。用户通过git clone http://你的服务器/仓库名.git拉取代码。
优点是部署极其简单,几乎零依赖,不需要数据库、不需要额外的 Web 服务。缺点是只能做只读镜像,没有权限管理,没有 Web 界面,普通用户想知道仓库里有什么只能靠目录列表或者再去 GitHub 上看。另外如果仓库特别大,静态 HTTP 走的是 dumb protocol,效率不如 smart HTTP,后面我会专门讲怎么优化。
2.2 中间路线:Gitea 或 GitLab 的 Pull Mirror
如果你要镜像几十个仓库,而且希望团队能有个网页界面能浏览、能管理、能控制权限,那就直接上 Gitea 或 GitLab。
Gitea 自带"镜像仓库"功能,你只需要在新建仓库时勾选"镜像仓库",填上 GitHub 仓库地址,Gitea 会按调度周期自动同步。GitLab 也有类似的 repository mirroring 功能。这些方案自带 UI、用户体系、Webhook 触发、以及 HTTPS 支持,省去你自己写管理脚本的功夫。
这条路线也是最推荐给中小团队的选择。它比轻量路线多个 Gitea 服务,部署成本也不高,但换来的是一整套完整的仓库管理体验。后面我专门有一节讲 Gitea 实操。
2.3 重量路线:自建完整 Git 服务
如果你们的场景不只是镜像,而是要把 GitHub 上的大量仓库迁移到完全自建的环境,同时保留 push、code review、CI/CD 能力,那这就是"自建 Git 平台"而不是"镜像站"了。GitLab、Gitea 都支持完整的功能,但你需要考虑双写、迁移、权限映射等问题,复杂度高一个量级。
我做镜像站的建议是:别一上来就上重量方案。先确认需求是"读多写少"还是"完全替代"。
| 方案 | 适合场景 | 维护成本 | 功能丰富度 |
|---|---|---|---|
| git clone --mirror + Nginx | 单仓库/少量仓库只读加速 | 极低 | 最低 |
| Gitea Pull Mirror | 多仓库、团队内网浏览与管理 | 中 | 高 |
| GitLab 自建平台 | 完整替代 GitHub、托管开发流程 | 高 | 最高 |
3. 实操:从零搭建单仓库只读镜像
3.1 环境准备与依赖安装
我用的是 Linux 服务器,系统是 Debian 系。这一步只需要系统自带的 git 和 Nginx,不需要额外编程环境。安装命令很简单:
sudo apt update sudo apt install -y git nginx然后把镜像仓库统一放在一个目录下,比如/data/mirror,并给当前用户创建好目录权限。建议单独建一个系统用户来跑同步任务,避免用 root 操作仓库文件,防止权限混乱。
sudo mkdir -p /data/mirror sudo chown -R $(whoami):$(whoami) /data/mirror3.2 全量镜像初始化
git clone --mirror是这里最核心的命令。它等价于先做了一个裸克隆,然后又设置了remote.origin.mirror为 true,意味着之后每次 fetch 都会像上游一样更新所有 refs,包括分支、标签,甚至那些不指向提交的 refs,比如 pull request 的 refs。
初始化一个仓库:
cd /data/mirror git clone --mirror https://github.com/gohugoio/hugo.git执行完之后你会得到一个hugo.git目录,它不是工作区,而是一个裸仓库,里面就是 git 的全部对象数据库。你可以用下面的命令验证仓库是否完整:
git --git-dir=/data/mirror/hugo.git branch -a git --git-dir=/data/mirror/hugo.git tag | head如果能看到上游的默认分支和 tag,说明初始镜像成功。注意首次克隆大仓库时,网络波动会导致中断,建议用git clone --mirror --progress观察进度,断了就重新跑一次,git 会基于已下载的对象断点续传,这一点很贴心。
3.3 定时增量同步与手动触发
镜像不是拉一次就完事,得定期跟上游同步。这里我推荐用 cron 定时执行,同步命令就一行:
git --git-dir=/data/mirror/hugo.git remote update --prune--prune表示同步时顺带删除上游已不存在的远程引用,避免你这边保留一堆死掉的 tag 和分支。如果你用 Nginx 做静态托管,还建议在同步后执行一次git update-server-info,刷新 dumb HTTP 协议需要的info/refs文件。
我把任务写成一个脚本/opt/mirror/update-hugo.sh:
#!/bin/bash REPO=/data/mirror/hugo.git cd "$REPO" git remote update --prune git update-server-info然后加进 crontab,每 30 分钟同步一次:
*/30 * * * * /bin/bash /opt/mirror/update-hugo.sh >> /var/log/mirror-hugo.log 2>&1如果你想在上游仓库收到推送时立刻同步,可以配置 GitHub Webhook,在 push 事件时向你的镜像服务器发一个 POST 请求,再在这个请求里触发脚本。你可以用最轻量的 Python Flask 或 Node.js 写一个回调接口,也可以直接用 Nginx 的 Lua 模块处理,但说实话,对大多数场景来说 30 分钟一次的定期同步已经足够了。
3.4 用 Nginx 把裸仓库暴露成克隆源
这是整个镜像站最关键的对外入口。最简单的配置是这样:
server { listen 80; server_name git.internal.example.com; root /data/mirror; autoindex on; location / { try_files $uri $uri/ =404; } }这样客户端就能通过下面的地址克隆:
git clone http://git.internal.example.com/hugo.git这个配置可以工作,但走的是 git 的 dumb HTTP 协议。客户端第一次访问时会去请求info/refs,拉取所有引用,然后逐个下载对象文件,并发多、数量多时效率偏低。如果你管理的仓库比较大、用户也比较多,我建议在 Nginx 上启用 smart HTTP,这也是现代 git 最常用的方式。
你的服务器需要先安装fcgiwrap:
sudo apt install -y fcgiwrap然后增加如下 location 配置:
location ~ ^/(.*\.git)/(HEAD|info/refs|objects/info/.*|git-upload-pack)$ { include fastcgi_params; fastcgi_param SCRIPT_FILENAME /usr/lib/git-core/git-http-backend; fastcgi_param GIT_PROJECT_ROOT /data/mirror; fastcgi_param GIT_HTTP_EXPORT_ALL 1; fastcgi_param PATH_INFO $uri; fastcgi_pass unix:/run/fcgiwrap.socket; }配置完成后,客户端 clone 时就会走 smart HTTP,原理是客户端和服务器通过git-upload-pack协议进行 pack negotiation,只传缺失的对象,效率和稳定性都高很多。这一步对于内网大规模拉取来说,体验差异非常明显。
3.5 克隆验证与权限提示
配置好 Nginx 后,一定要在自己电脑上验证一次:先删掉本地可能存在的克隆缓存,再用全新的目录克隆一次,确认能在干净环境下跑通。
cd /tmp rm -rf hugo-test git clone http://git.internal.example.com/hugo.git hugo-test cd hugo-test git log --oneline -5 git tag | tail如果没有报错,说明镜像站基本可用了。这里的权限控制为零,任何能访问到你服务器的人都可以克隆,所以只建议在内网使用。如果要暴露到公网,要么加 HTTP Basic Auth,要么直接上 Gitea,用账号体系做权限管理。
4. 进阶:用 Gitea 搭建团队级镜像站
4.1 安装 Gitea 并完成基础配置
如果你嫌裸仓库方案没界面、没权限、没批量管理能力,Gitea 就是最合适的那一层包装。安装 Gitea 非常简单,下载二进制、建好运行用户和目录就行:
wget -O /tmp/gitea https://dl.gitea.com/gitea/1.21.0/gitea-1.21.0-linux-amd64 chmod +x /tmp/gitea sudo mv /tmp/gitea /usr/local/bin/gitea sudo useradd --system --home /var/lib/gitea --create-home git sudo mkdir -p /etc/gitea /var/lib/gitea/data sudo chown -R git:git /var/lib/gitea然后通过 systemd 或直接在命令行执行/usr/local/bin/gitea web --config /etc/gitea/app.ini启动。首次访问网页进入安装向导,数据库可以直接用 SQLite,仓库根目录设置为/var/lib/gitea/data/repositories,其他保持默认。
4.2 创建 Pull Mirror 仓库
Gitea 对镜像仓库的支持很成熟。新建仓库时,在创建页面的"仓库属性"里勾选"这是一个镜像仓库",然后在"镜像地址"里填 GitHub 仓库地址,比如https://github.com/gohugoio/hugo.git。
创建完成后,进入"仓库设置 -> 镜像"页面,可以看到上次同步时间、下一次同步时间,以及一个"立即同步"按钮。Gitea 默认每 8 小时同步一次,这个间隔可以在app.ini的[mirror]区间里调整:
[mirror] DEFAULT_INTERVAL = 4h改成 4 小时后重启 Gitea 生效。如果你希望上游 push 之后延迟很小,可以配合 GitHub Webhook,触发 Gitea 的同步 API,几分钟内完成同步。
4.3 用 API 批量导入镜像仓库
几十个仓库一个个在网页上创建效率太低。Gitea 提供了一个迁移接口,可以用一条命令创建镜像仓库。首先在后台申请一个 Access Token,然后执行:
curl -X POST "http://git.internal.example.com/api/v1/repos/migrate" \ -H "Authorization: token 你的TOKEN" \ -H "Content-Type: application/json" \ -d '{ "clone_addr": "https://github.com/gohugoio/hugo.git", "repo_name": "hugo", "repo_owner": "mirrors", "mirror": true, "service": "git", "uid": 1 }'把clone_addr和repo_name替换成你要镜像的仓库即可。uid是目标组织或用户 ID。如果你有一份仓库列表,完全可以用一个 Shell 循环把列表文件逐行读进去,批量创建。这样 100 个仓库也只需要几分钟就能建完。
4.4 权限、HTTPS 与运维细节
Gitea 镜像仓库默认是公开可读的,如果你只想让特定团队访问,要把仓库设为私有,然后给团队成员分配权限。对外服务时建议再用 Caddy 或 Nginx 在前面做一层 HTTPS 反向代理,避免内网传输代码被明文抓包。
Caddy 的配置尤其简洁:
git.internal.example.com { reverse_proxy 127.0.0.1:3000 }Caddy 会自动申请 Let's Encrypt 证书,省得自己折腾证书续期。镜像站日常运维中,磁盘空间、同步日志、同步失败告警这三件事一定要盯住,否则镜像会在某个时间点悄悄停在旧版本,而你还以为它是新的。
5. 存储、同步与性能的实践经验
5.1 存储增长分析与 repack 优化
镜像站的存储风险是最容易被低估的。Git 的历史对象经过多次变更后,会产生大量重复的 loose object,仓库体积会虚胖。长期运行后,你需要定期做 object 压缩。
Gitea 本身会在后台执行git gc,但如果你用的是裸仓库加 Nginx 的方案,就要自己去跑 repack。建议每个月做一次:
git --git-dir=/data/mirror/hugo.git repack -a -d --depth=50 --window=50 git --git-dir=/data/mirror/hugo.git gc --prune=nowrepack的作用是把零散对象打包压缩,gc清理不可达的对象,加--prune=now是立即清理过期垃圾对象。执行之前确认一下磁盘剩余空间,repack 过程会临时占用额外空间,别在磁盘即将写满时硬跑。
5.2 LFS 对象同步的坑
如果你的上游仓库使用了 Git LFS,普通的镜像只同步了 LFS 指针文件,真正的 LFS 大对象根本不在 git 仓库对象库里。这就是为什么很多人镜像完仓库,拉下来后本地却看不到二进制文件。
解决办法有两个。一是用 Gitea 的镜像功能,它有一定概率能同步部分 LFS 数据,但不同版本行为不一致,我不建议完全依赖它。二是自己在镜像机上也安装 Git LFS,并在同步脚本里加上:
git lfs fetch --all git lfs prune同步完成后用git lfs ls-files抽查一下,确认对象确实存在。LFS 是镜像站里最容易踩的暗坑,没有之一。
5.3 Webhook 即时同步与定时轮询怎么选
定时轮询的优点是简单可靠,缺点是同步延迟不可控,可能在某个上游刚 push 完、而你这边的定时任务还没跑的时候,用户拉到旧版本。
如果你对时效性有要求,可以给每个 GitHub 仓库配置 Webhook,payload URL 指向你自己写的一个小接口。这个接口收到 GitHub 的 push 事件后,校验一下签名,然后执行对应的git remote update或调用 Gitea 的同步 API。
我个人的习惯是:核心依赖仓库用 Webhook 做即时同步,普通仓库用 4 到 8 小时间隔的定时同步。因为 Webhook 并不是免维护的,GitHub 有时会大批量发送事件,你还需要处理签名校验、失败重试,复杂度比定时任务高。
5.4 给镜像站做限速与缓存
内网镜像站通常带宽充足,但如果你要对外提供服务,或者公司出口带宽本身有限,一定要做限速。Nginx 里一行配置即可:
location / { root /data/mirror; limit_rate 10m; }limit_rate表示单个连接的最大下载速度,这里限制为每秒 10MB,防止某个用户把带宽吃满。这个参数不是总量限制,而是单连接限制,所以实际总吞吐量会随并发数上升,需要根据你的出口带宽做合理估算。
6. 常见问题与排查速查表
镜像站搭完之后,问题主要集中在同步失败、克隆失败、存储增长这几类。我整理了一张速查表,基本都是我实际遇到过的。
| 现象 | 可能原因 | 排查与解决 |
|---|---|---|
| clone 报 repository not found | 裸仓库目录名不对,或 Nginx root 指向错误 | 确认/data/mirror下目录名是xxx.git,并且 Nginx 能访问该目录 |
| clone 慢或卡住 | 走的是 dumb HTTP 协议 | 启用 smart HTTP,按第三节的 fcgiwrap 配置处理 |
| 同步后仓库没更新 | cron 没执行,或脚本路径不对 | 手动跑一次脚本,检查/var/log/mirror-hugo.log和 crontab 日志 |
| 镜像仓库里看不到 Release 附件 | git clone --mirror本身不包含 Release 附件 | Release 附件需要单独脚本调用 GitHub API 下载,或用 Gitea Release 镜像功能 |
| LFS 文件拉不下来 | 只同步了 LFS 指针 | 在镜像机安装 git-lfs,执行git lfs fetch --all |
| repo 体积巨大 | 从未执行 repack,loose objects 太多 | 定期执行git repack -a -d和git gc --prune=now |
| 同步时上游被限流 | 客户端 IP 被 GitHub 暂时限制 | 换成认证克隆地址,或降低同步频率,避免一次拉太多仓库 |
| Webhook 触发了但没同步 | 签名校验失败或地址没配对 | 检查 Webhook 是否正确响应,查看回调服务日志 |
最后分享一个我自己的体会。镜像站最怕的不是搭不起来,而是搭起来之后没人看。你设了一个每周同步的任务,半年后磁盘满了、某个仓库被上游删了,用户来问你为什么拉不到代码,那时候才是最头疼的。所以从第一天起,就把同步日志、磁盘告警、仓库存活检查这三件事自动化,哪怕只是简单的 Shell 脚本加 cron,也比裸跑一个差不多能用的镜像站强得多。毕竟镜像站的本质不是"克隆一次",而是一个需要长期维护的服务。