1. 为什么要自己搭一个GitHub镜像站1.1 “镜像站”到底是个什么GitHub镜像站这个话题这几年在开发团队里越来越常见。很多人一听到“镜像”第一反应是把整个 github.com 复制一份页面、用户头像、Issue、Pull Request 全部一模一样。我劝你趁早放弃这个想法GitHub 全站的数据量是 PB 级光所有公开仓库的 Git 对象就已经天文数字普通团队把磁盘堆满也只是九牛一毛。我们真正需要的是面向日常开发高频动作的“功能镜像”目标非常明确clone 仓库时不再半路断掉Release 里的安装包能顺利下载经常用的仓库在内网或离线环境里也有一份可同步的副本。所以我把 GitLab、Gitea、Gogs 这类自建 Git 服务以及单纯的裸仓库同步、Release 文件离线缓存都看作是“镜像站”的组成部分。它们解决的是不同的痛点组合起来才是一个完整方案。这套思路适合谁运维、研发负责人、还要兼顾内网开发环境的个人开发者都可以参考。不需要很强的服务器也不需要一开始就追求所有仓库全覆盖先从一个高频仓库开始跑通再逐步扩大。1.2 痛点场景从一次 20 分钟的 clone 说起前两年我给团队搭镜像服务之前最典型的一个场景是这样的同事要拉一个约 500MB 的仓库早高峰时段执行git clone进度条经常卡在 “Receiving objects” 的 30% 位置然后等上十几分钟直接报fatal: remote error: early EOF。原因是多方面的跨地域的网络链路时延偏高TCP 传输容易丢包Git 协议对完整性要求又很高一旦中断就得重头再来体验非常折磨人。Release 下载也是重灾区。一个几百 MB 的 tar.gz 压缩包浏览器下载能断三四次最后大家只能挂在下载工具里慢慢续传。而我们的真实需求其实很简单代码就是那几十个仓库安装包就是固定的几个软件发布文件完全可以在本地保存一份缓存让团队直接从内部地址拿。镜像站解决的就是这种“高频但不稳定”的问题而不是把整个互联网备份一遍。1.3 说在前面合法合规的前提动手之前我先把边界讲清楚。下面所有方案都建议用于公开开源项目的缓存、备份、内网发布和开发加速并且要遵守 GitHub 服务条款、仓库自带的开源许可证以及所在地区的法律法规。不要用它去做任何绕过网络访问限制、抓取非公开数据、滥用接口的事情。镜像站如果部署在公网还要考虑带宽成本、访问控制和滥用防护不能让它变成公共下载通道。合规这件事不是空话很多团队因为图省事把镜像站裸奔在公网结果被人刷下载流量轻则账单爆掉重则域名被封最后得不偿失。2. 先搞清楚要镜像什么三类镜像需求分析2.1 Git 仓库镜像让 clone 和 pull 不再看脸第一种需求是针对 Git 仓库本身的镜像。目标是让团队在内网环境下能很快地clone、fetch、pull代码。实现方式通常有两种一种是用 Gitea/Gogs 这类自建 Git 平台把 GitHub 上的仓库“迁移”到本地然后定时同步这样团队可以直接用熟悉的 Web 界面浏览、克隆、打标签另一种是更轻量的命令行裸仓库同步只保留纯 Git 数据再用静态文件方式暴露给内网。两种方式各有适用场景我后面会详细展开。2.2 Release 与附件文件镜像最容易被忽略的痛点第二种需求是针对 Release 页面里的二进制文件、安装包、APK、模型权重等附件。很多人一开始只想着仓库代码镜像结果代码 clone 倒是快了但 release 文件还是下载不动。原因很简单这些附件走的是 CDN 链路跟 Git 数据不是同一个通道。Git 仓库可以用裸仓库同步把内容拉到本地Release 附件则需要单独写脚本去下载、断点续传、归类和清理。这块做得好体验提升往往比仓库镜像还明显因为真正让团队下载到烦躁的很多时候就是几个大安装包。2.3 网页浏览与接口转发复杂度高不建议碰第三种需求是把 GitHub 的网页也镜像一份比如仓库主页、文件浏览、历史提交记录甚至 GitHub API 也要转发。这类需求听起来很全但实际复杂度非常高页面 JavaScript 里到处是动态 URL登录态和 Cookie 管理也很麻烦再加上 GitHub 页面更新频繁今天能用的方案过两周可能就失效。我的个人建议是如果没有明确的研究需要不要做全站网页镜像。想看某个仓库的代码结构把仓库同步到 Gitea 之后在 Gitea 的 Web 界面里看就够了想下最新 Release离线文件镜像也够用。下面用一个表格把这三种需求梳理清楚镜像类型典型场景实现方式复杂度推荐程度Git 仓库镜像clone、pull、代码备份Gitea 迁移同步 / 裸仓库脚本低必做Release 附件镜像下载安装包、模型、APK脚本下载到本地 Nginx 暴露中强烈建议网页/API 转发需要实时浏览 GitHub 页面动态解析、Cookie 管理高不推荐3. 基础环境准备与域名规划3.1 服务器配置怎么选搭建镜像站不需要高配服务器但是磁盘和带宽一定要留够。我建议的起步配置是这样的CPU2 核就够。Gitea 做定时同步、Nginx 做静态文件服务压力都不大。内存4 GB 起步8 GB 更稳妥。同步大仓库时 Git 进程会有内存峰值特别是多仓库并发同步的时候。磁盘仓库同步建议至少给 1 TB 可用空间如果只做 Release 附件缓存500 GB 也能跑。带宽如果是内网镜像按内网带宽来如果部署在公网建议按流量计费避免高额固定带宽费用。内存和磁盘这两个最容易估错。我见过有人用 1 GB 内存跑 Gitea同步几十个仓库之后直接 OOM最后不得不给 Gitea 配置 swap 和限制并发同步任务。磁盘也一样Release 缓存的增长速度比你想象得快尤其是几个仓库历史版本很多的时候。3.2 域名与 HTTPS 证书规划镜像站建议绑一个独立域名比如git.example.com或mirror.example.com。域名要在最开始就规划好因为后面 Gitea 的 SSH 端口、Release 下载地址、Git LFS 的 URL 都需要用到固定域名中途更换非常麻烦。HTTPS 证书直接用 Lets Encrypt 或者云厂商的免费证书即可有效期快到期时记得续签。这里有个容易被忽略的细节如果镜像站的域名不止一个入口比如还有备用域名证书的 SAN 字段要把所有域名都覆盖到否则某些下载场景会出现 Host 头校验不通过。3.3 目录结构与数据盘规划我的推荐目录结构如下/opt/mirror ├── gitea # Gitea 数据目录 ├── release-cache # Release 附件缓存目录 ├── bare-repos # 命令行裸仓库目录 └── logs # 各类日志建议把镜像站的数据目录单独挂载到一块数据盘上不要和系统盘混在一起。原因有两个一是仓库同步会有大量小文件写入数据盘可以更灵活扩容二是系统盘满了会导致整个服务器异常分开挂载能降低风险。文件系统选 ext4 或 xfs 都可以。如果你计划把数据放到网络存储上千万注意文件锁和同步延迟否则 Gitea 这类应用可能出现并发写入问题。4. 仓库镜像用 Gitea 拉取并定时同步 GitHub 仓库4.1 Gitea 镜像仓库的原理Gitea 自带“迁移仓库”功能可以从 GitHub 导入仓库并周期性同步。原理不复杂Gitea 记录上游仓库地址后台用git fetch把上游的提交、分支、标签都拉到本地存储然后在自己的数据库里更新引用。Gitea 还可以同步 Issue、Pull Request、Release 等元数据对内部查阅很实用。操作方法也很简单登录 Gitea 管理界面在右上角“探索”或“仓库”里找到“迁移仓库”选择 GitHub 作为迁移源填入仓库 URL公开仓库可以不填 Token如果要同步私有仓库或提高接口配额建议填写一个只读 Token。同步选项里可以勾选 LFS、Issue、PR 等按需开启就好。4.2 同步周期的配置建议Gitea 外部仓库的同步是后台队列执行的。你可以在管理后台的“定时任务”里设置镜像同步间隔。根据我的经验同步周期设置在 30 分钟到 2 小时之间比较合理。太频繁会增加上游服务器负担也容易被接口限流太慢则会让镜像仓库失去时效性。需要提醒的是Gitea 的“同步”不是实时推送而是轮询拉取。如果你团队对某个仓库的时效性要求特别高比如发布前需要立刻看到最新 commit可以在仓库详情页手动点击“同步”按钮或者在脚本里调用 Gitea 接口触发同步。但大多数场景下1 小时同步一次已经足够。4.3 更好的轻量方案裸仓库加定时脚本如果不想部署 Gitea或者只需要为少数仓库做只读镜像我推荐用命令行裸仓库。操作非常轻量mkdir -p /opt/mirror/bare-repos cd /opt/mirror/bare-repos git clone --bare --mirror https://github.com/someuser/somerepo.git cd somerepo.git git remote add upstream https://github.com/someuser/somerepo.git git update-server-info--mirror模式会把上游所有分支、标签、Refs 都抓下来后续同步用git remote update --prune拉取增量并清理已删除的引用#!/bin/bash REPO_DIR/opt/mirror/bare-repos/somerepo.git cd $REPO_DIR git remote update --prune git update-server-info放到 crontab 里每小时执行一次即可。然后在 Nginx 里把/git/路径直接映射到这个目录团队就能用下面的地址 clonegit clone http://mirror.example.com/git/somerepo.git这个方案的好处是几乎没有额外依赖坏处是只能同步 Git 对象和引用Release 附件、Issue、PR 这些都没有。它适合做代码冷备份也适合只关心源码的团队。4.4 多仓库批量同步脚本如果镜像的仓库不止一两个建议把仓库清单写成一个文本文件然后用脚本循环处理#!/bin/bash REPO_BASE/opt/mirror/bare-repos LIST/opt/mirror/repo-list.txt while read -r repo; do [ -z $repo ] continue path$REPO_BASE/${repo//\//_}.git if [ -d $path ]; then cd $path || continue git remote update --prune git update-server-info else cd $REPO_BASE || continue git clone --bare --mirror https://github.com/$repo.git fi done $LIST注意几点一是仓库清单里每行写owner/repo二是用flock给整个脚本加锁防止 crontab 里上次任务没跑完下次任务又启动三是建议在脚本里记录失败日志方便排查。5. Release 附件镜像把安装包提前拉到本地5.1 为什么要单独做 Release 镜像Git 仓库同步解决的是代码下载问题但很多人忽略了大附件。GitHub 的 Release 文件尤其是那些几百 MB 的二进制包它们的下载地址并不在 Git 仓库里而是由 CDN 服务提供。如果只是同步仓库代码团队下载 Release 附件时依然要访问外部 CDN。所以 Release 附件必须单独处理。我的建议很直接用脚本把热门仓库的 Release 附件提前下载到本地然后通过 Nginx 静态服务暴露给团队。这样第二次下载时内部用户根本不需要接触慢速链路。5.2 用 GitHub API 列出 Release 附件可以使用 GitHub API 获取某个仓库的最新 Release 信息。以下命令以someuser/somerepo为例API_URLhttps://api.github.com/repos/someuser/somerepo/releases/latest curl -sL -H Authorization: token $TOKEN $API_URL \ | jq -r .assets[]?.browser_download_url如果 Token 未认证GitHub API 对单个 IP 的速率限制是每小时 60 次批量脚本基本扛不住。建议在 GitHub 设置里生成一个只读 Token配额可以提升到每小时 5000 次然后把 Token 保存到环境变量或独立配置文件中不要写死在公开脚本里。5.3 批量下载与断点续传获取到附件列表后用wget或curl下载。我推荐wget的断点续传模式mkdir -p /opt/mirror/release-cache/someuser/somerepo/latest cd /opt/mirror/release-cache/someuser/somerepo/latest wget -c -q --show-progress $DOWNLOAD_URL如果文件特别大想进一步提高下载速度可以用aria2caria2c -x 8 -s 8 -c $DOWNLOAD_URL-x 8表示每个服务器最多开 8 个连接-s 8表示分 8 段下载-c表示断点续传。不过注意并发太高会给源站造成压力也可能触发 CDN 限流一般-x 4 -s 4就够用。5.4 通过 Nginx 暴露为静态文件下载完成后目录结构可以和 GitHub 的 URL 路径尽量保持一致。例如要访问someuser/somerepo的latest版本下的app.tar.gz我们可以在 Nginx 里这样配置server { listen 80; server_name mirror.example.com; location /releases/ { alias /opt/mirror/release-cache/; autoindex off; charset utf-8; } }然后内部的下载地址就是http://mirror.example.com/releases/someuser/somerepo/latest/app.tar.gz这个路径比较轻量也方便脚本统一生成下载页面。如果你希望保留 GitHub 原路径的斜杠结构可以继续调整 Nginx 的 location 规则但不建议做得太复杂简单清晰最重要。5.5 定时同步 Release 的注意事项Release 附件不要每个小时都去拉取很多仓库几个月才发布一次新版本。建议同步频率控制在每 6~12 小时一次并且只下载新出现的附件。可以在脚本里记录已经下载过的文件列表每次请求 API 后过滤掉已经存在的文件名。这样既省流量也减少对源站的打扰。6. 细节打磨LFS、超大仓库与存储策略6.1 Git LFS 对象怎么同步如果仓库使用了 Git LFS普通git fetch只会拉取 LFS 指针文件真正的数据需要通过 LFS 接口额外获取。Gitea 在迁移仓库时可以勾选“同步 LFS 文件”会自动完成这部分工作。如果是裸仓库方案需要先安装git-lfs然后在同步脚本里加一句git lfs fetch --all这里要特别提醒GitHub 对 LFS 的流量是有配额限制的单个仓库的 LFS 带宽和存储量都有限制。如果仓库非常大同步前最好先检查一下 LFS 对象总量避免把配额跑满导致后续连正常仓库操作都受影响。6.2 超大仓库浅镜像与部分克隆有的仓库历史特别长动辄几个 GB 甚至几十 GB全量镜像的成本很高。如果团队只是想要最新代码用于构建或阅读可以考虑浅镜像只抓取最近若干次提交git clone --bare --depth50 https://github.com/someuser/somerepo.git也可以用--filterblob:none让 Git 在 clone 时不下载 blob 对象等到 checkout 时再按需获取。这种做法的缺点是后续增量同步时浅克隆需要git fetch --unshallow才能补全历史而--filter模式对某些工具链兼容性还需要测试。因此仓库镜像用什么策略最终取决于你是否需要完整历史而不是一味追求“全量”。6.3 存储扩容与数据迁移镜像站的磁盘使用量会随着仓库数量和版本更新缓慢增长建议给数据目录做监控。我常用这个命令看各家仓库占用du -sh /opt/mirror/* | sort -hr如果发现某个仓库异常膨胀可以去查一下是不是把--mirror同步成了重复历史或者是 Release 缓存里积累了太多旧版本。迁移数据时优先用rsync冷拷贝到新磁盘再切换挂载点。Gitea 服务所在的数据目录迁移前最好停掉服务避免数据库和仓库文件不一致。7. 日常运维与避坑清单7.1 API 限流与同步失败排查GitHub 镜像站最常见的故障是同步失败而其中很大一部分原因是 API 限流。排查方法很简单curl -s https://api.github.com/rate_limit | jq .rate查看remaining字段如果剩余次数为 0说明配额已经用完。遇到这种问题第一时间给所有调用 GitHub API 的脚本加上 Token并把不同用途的脚本分开使用不同 Token避免互相挤占。另外Git 协议本身的 clone/fetch 是不受 API 限制的但 Gitea 迁移时的元数据抓取会调用 API这一点要记住。7.2 同步脚本的重入与日志同步脚本最害怕的是上一次还没跑完下一次 crontab 又启动了。我给脚本加上flock后这种情况基本绝迹exec 9/tmp/mirror.lock flock -n 9 || exit 1同时把同步日志按日期写到指定文件LOG/opt/mirror/logs/sync_$(date %Y%m%d).log echo [$(date %T)] start sync $LOG有了日志你才能在后半夜收到“同步失败”告警时快速判断是网络问题、API 问题还是磁盘满了。7.3 访问控制与安全加固如果镜像站面向公网开放访问控制一定要做。最简单的就是把 Nginx 改成只允许内网 IP 访问allow 192.168.0.0/16; allow 10.0.0.0/8; deny all;如果团队有跨地域办公可以用 Basic Auth 做用户级认证。不要开autoindex至少不要把 Release 缓存目录的列表暴露出去。另外给 Nginx 加limit_req限速防止有人用脚本疯狂下载把带宽打满。7.4 我踩过的几个坑第一同步周期设得太短。曾经把一个高频仓库的同步间隔设成 1 分钟结果不到半天就触发了 GitHub API 临时限流。后来改成 30 分钟再配合手动触发问题就消失了。第二裸仓库clone时忘了加--mirror只用了--bare。结果后续remote update时没有把上游所有refs/pull/*引用同步下来团队某些基于 Pull Request 的自动化流程拿到的是残缺引用。现在统一用--mirror建仓。第三Nginx 访问 Release 文件时总是 404排查了半天发现是alias路径后面少了一个/。location /releases/对应alias /opt/mirror/release-cache/两个路径结尾必须一致否则地址拼接会多出或缺少目录层级。第四证书续期脚本没配好有一天同事反馈镜像站页面打开不安全告警排查后发现是 Lets Encrypt 的证书已经过期两天。现在我在 cron 里加了续签检查并在证书快到期前 7 天发告警。最后再说一点实际体会。搭建 GitHub 镜像站真正难的不是安装和配置而是管住自己“什么都想镜像”的冲动。把团队最常用的仓库、最新的 Release 文件、LFS 对象这几件事做好体验就已经非常好了。这个项目后续还可以继续扩展比如增加对 GitLab、Bitbucket 等多源仓库的同步支持或者把镜像站接入 CI 系统让构建环境直接从内网拉代码。不必一次做全先从一个小仓库开始跑通你会发现整个流程比想象中简单很多。