做GitHub镜像站这件事听起来像是大厂才需要的基建但这两年我接触到的中小团队、实验室、个人开发者越来越多的都在考虑自己搭一个。GitHub镜像站简单说就是把你高频使用的仓库、Release文件、源码浏览入口放到自己可控的服务器或边缘节点上让拉代码更快、下载大文件不卡壳、构建依赖不再等得心慌。这篇文章会把自建GitHub镜像站的三种主流做法都过一遍Gitea仓库镜像、Nginx下载网关、Worker无服务器转发再附上我实际踩过的一些坑和一张排查表照着搭基本能落地。1. 先搞清楚镜像站要做什么1.1 镜像站不是“把GitHub搬空”很多刚接触这块的朋友会问镜像站是不是要用爬虫把GitHub全站抓下来然后做个本地搜索入口这个想法听起来很“终极”实际做起来基本不现实。GitHub上的仓库量级已经到亿级文件数量更是几十亿级别个人服务器就算有几十TB也存不下更别提同步过程中的流量和时间成本。我自己理解的自建镜像站是“按需镜像”。意思是说你只需要把你团队、你个人、你业务里真正高频访问的那一部分GitHub资源搬到你更方便访问的地方。GitHub本质上是多个数据通道的组合不同通道适合的镜像策略完全不一样不可能用一套方案通吃。1.2 三种典型镜像形态从数据特征和访问场景来分日常需要镜像的主要是下面这三类内容数据类型典型访问地址常见痛点适合的镜像方式仓库Git数据github.com/owner/repo.gitclone体积大、传输不稳定完整Git镜像 周期增量同步Release大文件github.com/owner/repo/releases/download/...单文件体积大、下载易中断下载转发网关 磁盘缓存Raw源码与Archive压缩包raw.githubusercontent.com、codeload.github.com请求频繁、热文件重复拉取边缘缓存 CDN接入这个表格基本对应了三种搭建路线。如果你只是想让团队内部拉代码方便优先看Gitea仓库镜像方案如果你主要是下载Release二进制大文件重点看Nginx和Worker方案如果你的CI/CD大量依赖raw文件或者仓库打包下载那Raw加速也不能少。这里还有一个很容易被忽视的点镜像站不能只看“GitHub主站”。很多人把注意力全放在github.com这个域名上但实际拖慢体验的往往是release-assets.githubusercontent.com、objects.githubusercontent.com这类资源域名。所以你在设计镜像站的时候一定要先明确入口域名别把所有请求都放在同一个转发规则里处理。2. 方案一Gitea仓库镜像最快落地2.1 部署一个自带镜像功能的Gitea如果目标是同步并保存完整的Git仓库最简单的方案不是自己写脚本维护裸仓库而是直接部署一个Gitea实例。Gitea是一款轻量级的Git托管服务内存占用很小1核2G的服务器就能跑得动而且原生支持“镜像仓库”功能。相比GitLab它最大的好处是资源占用低、部署简单相比裸仓库它又多了Web界面、权限管理、API和团队管理使用成本低很多。部署用Docker Compose最省事我平时习惯这样起一个实例version: 3 services: gitea: image: gitea/gitea:latest container_name: gitea environment: - GITEA__server__DOMAINgit.example.com - GITEA__server__SSH_DOMAINgit.example.com - GITEA__server__ROOT_URLhttps://git.example.com/ - GITEA__database__DB_TYPEsqlite3 ports: - 22:22 - 3000:3000 volumes: - ./gitea:/data restart: unless-stopped这里有几处值得注意。端口3000是Web入口22是SSH入口如果你的服务器22端口已经被占用了建议把宿主机端口改成一个不常用的比如2222:22然后通过设置SSH克隆地址来匹配。ROOT_URL和DOMAIN要改成你自己的域名否则后面生成出来的仓库克隆地址会是localhost这样的错误值坑过不少人。启动之后浏览器访问http://服务器IP:3000完成初始安装页面第一个注册的用户默认会成为管理员。到这里一个基础的Git服务端就跑起来了。2.2 创建镜像仓库并配置同步接下来就是核心操作把GitHub仓库镜像进Gitea。路径是进入Gitea右上角的“”按钮选择“新建仓库”。在仓库类型一栏里Gitea提供了“默认”和“镜像”两个选项选“镜像”然后填上游仓库地址就行。镜像公开仓库填https://github.com/owner/repo.git即可不需要任何认证。镜像私有仓库需要在GitHub侧生成Personal Access Token然后以上游用户名和Token作为认证信息填入。Token权限至少要勾选repo范围。创建完成后Gitea会自动做一次全量克隆。之后它会按后台任务的默认周期进行增量同步把上游仓库的新提交、新分支、新标签拉下来。Gitea镜像仓库默认的同步周期是每隔8小时检查一次这个频率对绝大多数场景都够用。如果你有特别热门的仓库需要更短周期比如3小时一次可以通过修改配置文件app.ini里的定时任务参数来调整。有一个经验要提前说第一次同步大仓库时不要急着在页面上反复点“同步”Gitea的同步是后台任务手动触发过于频繁容易导致多个同步任务排队甚至把服务器内存打爆。尤其那些包含几十GB历史的大仓库先观察第一次同步是否完成再说。2.3 用API批量维护镜像仓库团队如果只镜像三五个仓库手动点一点没问题。但如果你要维护几十甚至上百个上游仓库就需要走API了。Gitea提供了仓库迁移接口用一条命令就能创建镜像仓库curl -X POST https://git.example.com/api/v1/repos/migrate \ -H Authorization: token 你的Gitea访问令牌 \ -H Content-Type: application/json \ -d { clone_addr: https://github.com/owner/repo.git, repo_name: repo, repo_owner: yourteam, mirror: true, private: true }这个接口创建出来的仓库在Gitea界面上和手动创建的镜像仓库完全一样。访问令牌需要在Gitea右上角“设置 → 应用”里生成勾选“读写仓库”权限。配合一个简单的循环脚本就能把整批仓库一次性迁移过来。批量维护的时候我强烈建议你在上游仓库命名上做统一规范。比如统一小写、统一保留owner前缀这样镜像仓库的出入口和后续缓存路径都会清晰很多。不要小看命名的作用等到排障看日志、查缓存命中率的时候你就知道规范命名能省多少时间。3. 方案二Release下载加速网关3.1 Release链路的特点与瓶颈仓库镜像解决的是Git协议的数据同步但团队里真正让人头疼的往往是Release大文件下载。一个动辄几百MB甚至几个GB的模型包、安装包、二进制工具直接拉GitHub Releases地址体验会受限于很多因素。下载到一半断掉只能重新开始这种痛只要经历一次就忘不了。Release文件的访问逻辑和普通文件不太一样。链接会经过跳转真正承载文件下载的往往是对象存储域名响应头里有临时的签名URL。这种设计本身是为了安全但也给镜像站带来两个麻烦一是不能简单用HTTP重定向就算完二是如果缓存策略不对热文件会被反复回源拉取。3.2 用Nginx搭建下载转发网关既然Release文件是单链接大文件最直接的做法是在你的服务器上用Nginx做一个下载转发网关把Release请求转发到GitHub上游同时对热文件做磁盘缓存。下面这段配置是我在项目里用过的基础版本proxy_cache_path /var/cache/nginx/github_release levels1:2 keys_zonegithub_cache:10g max_size100g inactive7d; server { listen 80; server_name dl.example.com; location ~ ^/([^/])/([^/])/releases/download/ { proxy_pass https://github.com; proxy_ssl_server_name on; proxy_set_header Host github.com; proxy_set_header User-Agent $http_user_agent; proxy_cache github_cache; proxy_cache_key $scheme#$host$uri; proxy_cache_valid 200 206 24h; add_header X-Cache-Status $upstream_cache_status; proxy_connect_timeout 5s; proxy_read_timeout 60s; } }解释一下这段配置的关键点。proxy_pass https://github.com表示把请求转发到GitHub主站配合proxy_ssl_server_name onNginx在建立TLS连接时会发送正确的SNI否则上游握手会失败。proxy_set_header Host github.com保证上游收到的HTTP Host头是github.com不然GitHub会返回404或者403。缓存部分proxy_cache_path定义了缓存目录、内存索引大小和最大容量。proxy_cache_key用了URI作为缓存键同一个Release文件的URL是固定的后续请求可以直接命中缓存。proxy_cache_valid 200 206 24h意味着成功的完整响应和206断点续传响应都会缓存24小时。add_header X-Cache-Status $upstream_cache_status是用来调试缓存命中率的返回结果里能看到HIT还是MISS。有一点要提醒Release文件下载路径里的文件名如果发生变化缓存就会对应新键旧文件在缓存里会一直待到过期才会被清理。所以如果你经常发布同名的新版本包要留意磁盘占用必要时让定期任务去清理inactive超过一定时间的缓存文件。3.3 用Cloudflare Workers做轻量下载入口如果不想自己维护Nginx服务器或者业务场景只是偶尔需要给少数人提供下载入口可以用Cloudflare Workers做一个轻量转发层。代码量很小export default { async fetch(request, env, ctx) { const url new URL(request.url); const upstream https://github.com url.pathname url.search; const headers new Headers(request.headers); headers.delete(host); const resp await fetch(upstream, { method: request.method, headers: headers, redirect: follow }); const newResp new Response(resp.body, resp); newResp.headers.set(cache-control, public, max-age3600); return newResp; } };这段代码会把所有请求原样转发给https://github.com适合按照访问路径来区分转发范围。比如你的路由只匹配dl.example.com/owner/repo/releases/*那Worker就只会处理Release下载请求其他路径可以直接返回404。Workers免费套餐的限制是单个响应不超过100MB所以这套方案只适合几十MB以内的小文件。如果你要转发的是几个GB的模型权重文件要么升级套餐要么还是老老实实用Nginx网关后者没有响应体积限制只要磁盘和带宽扛得住就行。还有一点redirect: follow会让Worker跟随GitHub的跳转去下载对象存储里的文件这个行为在长链接场景比较消耗执行时间所以要留意Cloudflare Worker的CPU时间限额。4. 方案三Raw源码与Archive压缩包加速4.1 先认清raw和codeload这两个域名很多镜像站搭建者会忽略Raw源码和仓库打包下载这两个入口。但实际上现代CI/CD流程里这两个域名出现的频率非常高。比如你写Dockerfile里面用ADD https://raw.githubusercontent.com/...拉一个配置文件比如你用go install或者按源码编译某个工具构建脚本可能直接下载https://codeload.github.com/owner/repo/tar.gz/refs/tags/v1.0.0。这两个域名和github.com的解析目标完全不同raw.githubusercontent.com用于获取仓库里的单个原始文件响应是纯文件内容不包含网页包装。codeload.github.com用于生成仓库的tar.gz或者zip压缩包是一个轻量的存档下载服务。它们的共同点是访问频率可能很高但文件本身大多是静态的。这就非常适合做缓存。如果镜像站只处理了github.com而漏掉这两个域名那你的镜像体验始终会缺一大块。4.2 多域名的Nginx转发配置针对这两个域名可以分别做转发入口。假如你已经为Raw文件分配了raw.example.com为Archive分配了archive.example.com配置思路和Release网关类似但上游Host和SNI需要按情况切换。server { listen 443 ssl http2; server_name raw.example.com; location / { proxy_pass https://raw.githubusercontent.com; proxy_ssl_server_name on; proxy_set_header Host raw.githubusercontent.com; proxy_cache raw_cache; proxy_cache_key $host$uri; proxy_cache_valid 200 24h; add_header X-Cache-Status $upstream_cache_status; } } server { listen 443 ssl http2; server_name archive.example.com; location / { proxy_pass https://codeload.github.com; proxy_ssl_server_name on; proxy_set_header Host codeload.github.com; proxy_cache archive_cache; proxy_cache_key $host$uri; proxy_cache_valid 200 24h; add_header X-Cache-Status $upstream_cache_status; } }这里最关键的是proxy_ssl_server_name。因为Nginx和后端建立TLS连接时默认会用你配置文件里proxy_pass指定的上游域名来发送SNI。如果你不手动开启这个选项很多场景下后端会认为SNI和Host不匹配返回证书错误。这个坑在GitHub相关转发里非常常见。Raw文件通常体积不大缓存容量按20GB规划就够了Archive压缩包则可能几十MB到几百MB不等如果业务里经常打包下载缓存空间要给足。4.3 接入CDN做边缘缓存自建服务器通常只有单点节点跨地区的访问体验差异依然存在。如果这个镜像站面向上级的公网用户或者分布式团队的多个地区建议再套一层CDN。架构上就成了用户访问CDN边缘节点CDN回源到你的Nginx网关Nginx再回源到GitHub。套了CDN之后Nginx的角色就从“前面接收用户请求”变成了“CDN的源站”。这个时候反而不要过度依赖Nginx的磁盘缓存因为CDN本身会有自己的缓存体系。你可以把Nginx的缓存时间调短或者干脆让CDN来控制缓存策略。通过响应头来配合location / { expires 24h; add_header Cache-Control public, max-age86400; }CDN会优先参考源站返回的Cache-Control头来决定边缘节点的缓存时长。如果你希望边缘节点缓存时间更长可以在源站直接给max-age86400。这样不同区域的用户首次访问时才会触发一次回源后续全部命中CDN边缘节点流量成本会整体降下来访问速度也均匀很多。5. 常见问题与排查技巧实录5.1 故障速查表搭建过程中一定会遇到各种问题我整理了一个速查表按现象来查基本能覆盖大部分场景。现象可能原因解决思路镜像仓库一直停在“初始化”状态仓库体积太大服务器内存或磁盘不足先用git clone --mirror在命令行验证能否完整拉取给服务器扩内存和磁盘镜像同步后LFS文件为空未启用Git LFS同步在Gitea里开启LFS支持手动执行git lfs fetch --all验证Release下载到一半中断转发网关没有正确传递Range请求确保Nginx配置里有Range相关头不要启用压缩中间件Worker转发大文件报错免费套餐响应体积限制大于100MB的文件改用Nginx网关方案证书校验失败转发时SNI不对在Nginx里开启proxy_ssl_server_name on缓存命中率很低缓存键设计不合理或没有设置过期时间检查proxy_cache_key是否包含随机参数给静态文件设置合理的过期时间镜像上游仓库改名同步任务失败重新添加镜像仓库旧镜像保留或删除按团队策略决定5.2 几个值得记录的实操教训第一个教训不要贪心去做全站转发。最开始我为了省事把整个github.com路径都套到了Nginx转发规则里结果是服务器流量在一天内爆表大量请求来自四面八方。GitHub主站是动态页面有登录、跳转、API等多种逻辑全站转发既没必要也不安全。正确做法是把转发范围严格限定在Release路径、Raw路径、Archive路径这几类静态资源上其他路径直接返回404避免你的入口被人拿去当公共中转。第二个教训大仓库首次同步要格外耐心。一个包含多年历史、几十GB大小的仓库第一次镜像耗时可能以小时计。这时候不要反复手动触发同步也不要并发同步多个大仓库否则内存和磁盘很容易被打爆。建议先同步一个仓库观察服务器负载再决定是否提高并发。磁盘空间要按仓库体积的两倍以上预留因为Git在打包和增量合并过程中需要临时空间。第三个教训缓存目录必须设限。Nginx的proxy_cache_path如果不设置max_size缓存文件会一直增长直到撑满磁盘。有些场景下热文件不多但Release文件体积很大一天内就可能积攒上百GB。建议首次上线时就规划好max_size同时配合定时清理脚本比如用find找出超过30天未访问的缓存文件并删除。磁盘告警这个事一旦爆了连Git服务本身的仓库数据也会跟着遭殃。第四个教训注意设备和环境的差异。Release文件下载在部分网络环境下可能会走到不同的CDN节点导致你的网关回源速度也不稳定。这种情况下单纯加大Nginx超时时间不一定有用更好的办法是给这个场景单独配置更长的回源超时以及在前端做重试。不要把不同上游域名的超时时间写成一套GitHub主站和对象存储的响应特征完全不同。5.3 健康检查与日常维护镜像站上线不是终点日常维护很重要。我通常会写一个简单的巡检脚本每隔几分钟做一次检查核心就两条git ls-remote https://git.example.com/yourteam/repo.git HEAD这条命令能验证镜像仓库的Git服务是否正常响应。如果返回了HEAD引用说明仓库同步和SSH/HTTP入口都正常。另一条是检查缓存命中率curl -I https://dl.example.com/owner/repo/releases/download/v1.0/app.zip看返回头里的X-Cache-Status。如果长期都是MISS说明你的缓存策略可能有问题或者访问路径不规范。如果HIT比例很高说明热文件确实被缓存住了网关在正常工作。我还会给Gitea的同步任务日志、Nginx的error.log、Worker的异常日志各建一个独立的查看入口排查问题时能快速定位到是同步环节、转发环节还是缓存环节出了问题。镜像站涉及的模块不算多但每个模块的日志要么不报、要么报得隐晦早一点把日志规范好后期能少熬好几个夜。最后再分享一个小技巧如果你只是想让内网团队用不需要把镜像站暴露到公网尽量别开放太宽的网络访问策略直接限定访问来源范围比如只允许内网IP访问。如果确实要提供公网服务也要做好路径限制和访问量监控。这套系统本质上是一个中间缓存层越小越专一越不容易出问题。