做开发和运维这些年我发现自己有相当一部分时间不是在写代码而是在“把代码从GitHub上弄下来”。无论是编译OpenCV这种大型C项目还是给服务器部署PyTorch环境都绕不开从GitHub拉源码、下载release包。真实体会是一个大几百MB的release压缩包下载到一半断了重试又是几个小时这类事经历多了自然就想自己动手搭一个GitHub镜像站。这篇文章我会把自建镜像站的完整路线讲清楚镜像站到底在镜像哪些资源、Nginx反代怎么配置、release下载时最折磨人的302跳转怎么处理、缓存策略怎么设计以及Cloudflare Workers这类免服务器方案的适用边界。无论你是自己用还是给团队内网做加速按这套方案都能复现一套稳定能跑的镜像服务。1. 镜像站到底要镜像什么先拆开GitHub的访问链路1.1 GitHub不是只有一个域名是一整套下载链路很多人以为搭GitHub镜像就是反代github.com一个域名这个理解会埋大坑。实际上下载一个release包的过程中系统会涉及至少四个域名少代理其中一个镜像站就会出问题。第一个是github.com主站页面、release入口、git的smart HTTP接口都在这里第二个是raw.githubusercontent.com存放仓库里的裸文件也就是你在服务器上curl某个安装脚本时访问的域名第三个是objects.githubusercontent.com这是真正存放release二进制大文件的存储服务器用户点击下载时github.com会先返回一个302跳转把浏览器带过去第四个是codeload.github.com点击页面上的Download ZIP按钮时整包压缩包从这里下载。这里说的“镜像站”不是说把GitHub整个仓库同步回来而是做按需代理转发。用户访问镜像域名时Nginx把请求转发到对应的上游域名拿到结果再返回给用户。这种反向代理模式成本最低、时效性最好GitHub上的仓库一更新镜像站立刻就能访问到新内容不需要定时同步。理解了域名链路也就理解了为什么很多半吊子镜像站“能打开页面下载按钮全是坏的”——因为只代理了github.com跳转域名没有处理。这个问题我在3.2节会专门讲处理方案。1.2 三种自建方案的选型对比动手之前先把主流的三种方案摆出来对比免得搭完发现选错了方向。方案核心组件优点硬伤适合场景Nginx反向代理Nginx 海外服务器 域名性能好、可控、能缓存大文件需要自己维护服务器和带宽流量团队长期自用、内网加速Cloudflare Workersgh-proxy脚本 Cloudflare账号免费、无需服务器、部署简单响应体存在100MB硬限制大release传不了小文件、临时加速PHP转发脚本任意一台服务器原理简单、容易复现并发差、速度一般、实现粗糙学习原理、轻度使用我个人的结论先放在这如果你的核心需求是“下载一个几百MB的软件包”别犹豫直接上Nginx反代方案。如果你只是偶尔拉脚本、拉小源码包Workers能帮你省一台服务器。如果你纯粹想搞明白GitHub的跳转链路写个PHP脚本转发挺有意思但生产环境我不推荐PHP单线程模型扛不住几个人同时下载大文件内存和CPU开销也很容易被打满。另外提醒一句现成公共镜像站虽然多但稳定性和速度你控制不了一旦出问题会影响整个业务节奏。自建镜像站本质上是把网络链路的控制权拿回到自己手里这个投入对经常和GitHub打交道的人非常值。2. 环境准备服务器、域名与TLS证书2.1 服务器选型镜像站真正的瓶颈是带宽镜像站对CPU、内存的要求很低一个纯反代服务甚至1核1G都跑得动真正卡脖子的是带宽和流量。我第一台搭镜像站的机器是2核2G带宽只有10Mbps一个两三百MB的包要跑好几分钟体验和没搭差不多。后来换了带宽更大的机器体验才算质变。我的建议参数是这样位置香港、日本、新加坡等亚洲节点对GitHub回源链路更稳定延迟也低带宽不低于30Mbps预算够直接100Mbps起步流量一定注意看是不是“不限流量”很多便宜VPS标称大带宽但月流量只有几百GB被团队里几个人轮流下载release一晚上就打爆系统Debian 12或Ubuntu 22.04Nginx官方源支持好certbot相关组件也齐全如果你所在的企业内网本身有到GitHub的专线或者云厂商提供了稳定的海外加速通道也可以直接用国内机房搭。选型的核心只有一句话服务器到GitHub的上游链路要稳服务器到用户的下行带宽要足两个条件缺一个镜像站体验都好不了。2.2 DNS解析、证书申请与防火墙准备一个主域名拆出两个子域名一个做主站比如git.example.com一个做raw文件加速比如raw.example.com。DNS解析就是A记录指到服务器公网IP。TLS证书我用Lets Encrypt配合certbot自动续期。安装命令apt update apt install -y nginx certbot python3-certbot-nginx先启动Nginx确认DNS解析生效后再申请证书dig git.example.com这一步我吃过亏当时域名解析没生效就急着跑certbot结果验证请求都到了GitHub的官方内容上导致证书一直申请不下来。正确顺序是先让A记录能查到再执行certbot --nginx -d git.example.com -d raw.example.comcertbot会自动帮你修改Nginx配置并在证书快到期时通过systemd定时任务自动续期非常省心。证书申请完后记得检查防火墙云服务器安全组和本机ufw都要放行80和443端口只保留这两个端口对外即可。3. 核心实战Nginx全套反向代理配置3.1 基础反代github.com与raw子域名先贴一份我实测可用的基础配置。这段配置把HTTP请求统一跳到HTTPS然后反代github.com和raw.githubusercontent.com。server { listen 80; server_name git.example.com; return 301 https://$host$request_uri; } server { listen 443 ssl http2; server_name git.example.com; ssl_certificate /etc/letsencrypt/live/git.example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/git.example.com/privkey.pem; location / { proxy_pass https://github.com; proxy_http_version 1.1; proxy_set_header Host github.com; proxy_set_header Connection ; 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; # 关键GitHub是SNI站点这个参数不开会回源失败 proxy_ssl_server_name on; proxy_connect_timeout 15s; proxy_read_timeout 120s; proxy_send_timeout 60s; } } server { listen 443 ssl http2; server_name raw.example.com; ssl_certificate /etc/letsencrypt/live/raw.example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/raw.example.com/privkey.pem; location / { proxy_pass https://raw.githubusercontent.com; proxy_http_version 1.1; proxy_set_header Host raw.githubusercontent.com; proxy_set_header Connection ; proxy_ssl_server_name on; proxy_connect_timeout 15s; proxy_read_timeout 60s; } }这里有两个我反复强调的点。第一是proxy_ssl_server_name onNginx默认关掉SNI而上游GitHub是靠SNI路由的虚拟主机不开这个参数curl一测就是502。第二是proxy_http_version 1.1和空的Connection头这是为了兼容git smart HTTP和分块传输不加的话执行git clone时偶尔会报错。raw子域名不需要额外rewrite请求路径直接透传。配置完成后执行curl https://raw.example.com/owner/repo/main/README.md返回内容和官方raw完全一样。3.2 难点攻克处理release下载的302跳转这是整个镜像站搭建里最翻车的点。用户在release页面点下载按钮浏览器发出的请求是https://github.com/owner/repo/releases/download/v1.0/app.zip。Nginx把它反代到github.com后服务端返回302Location指向objects.githubusercontent.com上带签名参数的临时URL。如果不处理这个302浏览器会直接跳到objects.githubusercontent.com流量不经过你的服务器下载速度又打回原形而且在很多网络环境下那个域名本身就不好访问。处理思路是用proxy_redirect把回源响应的Location改写成你自己的路径location / { proxy_pass https://github.com; proxy_set_header Host github.com; proxy_ssl_server_name on; # 关键把GitHub下载跳转改写成镜像站自己的地址 proxy_redirect https://objects.githubusercontent.com/ /ghproxy/; proxy_redirect https://codeload.github.com/ /codeload/; } # 代理release真实文件 location /ghproxy/ { rewrite ^/ghproxy/(.*)$ /$1 break; proxy_pass https://objects.githubusercontent.com; proxy_set_header Host objects.githubusercontent.com; proxy_ssl_server_name on; proxy_connect_timeout 15s; proxy_read_timeout 300s; proxy_send_timeout 120s; } # 代理Download ZIP整包下载 location /codeload/ { rewrite ^/codeload/(.*)$ /$1 break; proxy_pass https://codeload.github.com; proxy_set_header Host codeload.github.com; proxy_ssl_server_name on; proxy_connect_timeout 15s; proxy_read_timeout 300s; proxy_send_timeout 120s; }注意一个细节proxy_pass后面不带URI比如https://objects.githubusercontent.com;请求URI会原样保留所以我先用rewrite把/ghproxy/前缀去掉让后端看到的是原始路径。如果你把proxy_pass写成带斜杠的https://objects.githubusercontent.com/;再同时把rewrite去掉也能达到同样效果。两种写法二选一不要混搭。我第一次搭镜像站的时候试图用sub_filter去替换页面里的下载链接搞了半天release下载始终不对。后来在浏览器Network面板里看到302跳转才恍然大悟release下载是HTTP重定向不是页面内容用内容替换工具自然没用。方向错了再怎么折腾都是白费。3.3 缓存策略让热门release文件不再重复回源反代转发能解决“通不通”但速度要再上一个台阶必须上缓存。release文件发布后基本不会变这种冷数据非常适合Nginx的proxy_cache。在http块中定义缓存区proxy_cache_path /data/nginx-cache/github levels1:2 keys_zonegithub_cache:500m inactive30d max_size50g;然后在release下载的location中启用location /ghproxy/ { rewrite ^/ghproxy/(.*)$ /$1 break; proxy_pass https://objects.githubusercontent.com; proxy_set_header Host objects.githubusercontent.com; proxy_ssl_server_name on; proxy_cache github_cache; # 这类稳定资源缓存30天 proxy_cache_valid 200 301 302 30d; proxy_cache_key $scheme://$host$uri; add_header X-Cache-Status $upstream_cache_status; }缓存键我故意去掉了query参数。release对象的签名URL里有大量跳过验证用的参数如果全带进缓存键同一个文件每次点击都会因为签名参数不同而生成不同条目磁盘很快就会被塞爆。去掉query参数后同一份资源只存一份效果最好。响应头里的X-Cache-Status在调试阶段很有用能看到MISS还是HIT。第一次访问某个文件时是MISS回源拉取第二次开始就是HIT直接从磁盘缓存返回速度快很多。3.4 git clone的URL替换与静态首页引导镜像站搭好后怎么让git clone也走镜像最简单的办法是手动把URL里的github.com替换成git.example.comgit clone https://github.com/owner/repo.git # 换成 git clone https://git.example.com/owner/repo.git如果团队里大家都要用可以配置git的全局URL替换git config --global url.https://git.example.com/.insteadOf https://github.com/这样所有https://github.com/...的clone地址都会自动走镜像站。要提醒一点这个替换规则只对https clone有效SSH协议是不支持的。为了降低使用门槛我习惯在镜像站根路径放一个简单的静态首页把三种用法直接列出来location / { default_type text/html; return 200 html headtitleGitHub Mirror/title/head body h3Usage/h3 p1. Release: https://git.example.com/owner/repo/releases/p p2. Raw: https://raw.example.com/owner/repo/main/file/p p3. Clone: https://git.example.com/owner/repo.git/p /body/html; }这个小页面非常管用。团队新人用镜像站时打开根路径就能看到用法省去一遍遍解释“这个项目应该怎么下”的沟通成本。4. 备选方案Cloudflare Workers十分钟搭建4.1 gh-proxy原理与部署步骤上面是Nginx方案适合讲性能和缓存。如果你没有服务器或者只想快速给团队一个临时加速入口Cloudflare Workers是成本最低的路子。GitHub上有个开源项目gh-proxy把GitHub代理逻辑写成了一个Workers脚本。部署流程很清晰注册Cloudflare账号进入Workers Pages面板创建新Worker名称随意比如ghproxy打开gh-proxy项目的worker.js把代码全部复制替换默认内容保存并部署你会立刻拿到一个*.workers.dev的默认地址测试访问https://你的workers名.workers.dev/https://github.com/owner/repo/archive/refs/heads/main.zip如果你有自定义域名在Workers的触发器设置里添加路由比如mirror.example.com/*再把域名的DNS记录CNAME到workers.dev地址就能得到一个体面的镜像域名。gh-proxy的原理不复杂它把用户请求重新拼装成合法的GitHub地址用fetch拉取上游响应再返回给用户。脚本里也处理了302跳转所以release下载在Workers里是通的。但这里有个容量问题见下一节。4.2 Workers方案的硬限制大文件传不了Workers方案省服务器是省心但坑也很明显Cloudflare Workers对单个请求的响应体有100MB限制这个数字在官方文档里写得很清楚。一个超过100MB的zip包、tarball通过Workers转发响应会直接被截断或报错。这意味着如果你要加速的是那种动辄几百MB的软件包Workers方案基本不可用。好在很多脚本、配置、小源码包都在100MB以内这给“拉脚本加速”场景留了一片空间。所以我的定位是Workers方案适合日常拉配置文件、小源码包不适合大软件包分发。大文件老老实实用Nginx反代两者配合使用效果最好。团队里两种需求都有的话可以两个都部署入口不同而已。5. 常见问题与排查技巧实录5.1 Nginx反代的经典故障速查现象最常见原因处理方式访问返回502没设proxy_ssl_server_name on在location里加上并reload Nginx页面能打开下载按钮坏只反代了github.com缺少/ghproxy或/codeload路由补上3.2节的配置证书申请失败certbot运行时域名解析未生效dig确认A记录后再申请HTTPS证书过期certbot定时任务没跑检查systemctl status certbot.timer下载到一半断出口带宽或流量被打满限速、控制并发、升级带宽页面样式混乱GitHub静态资源域名未代理不影响下载可忽略想优化可反代githubassets502问题值得多说一句。反代时Nginx默认proxy_ssl_server_name为off而上游是SNI虚拟主机时证书握手里的Server Name缺失上游直接拒绝连接。这个参数不加curl测试会一直收到502加上了立刻就好。不少教程压根没提这个参数我在这里帮你把雷提前排了。限速和并发控制也建议提前做。镜像站对公网开放后容易被滥用流量走起来很吓人。Nginx里限制单IP速率limit_req_zone $binary_remote_addr zonegithub_mirror:10m rate10r/s; location /ghproxy/ { limit_req zonegithub_mirror burst20 nodelay; # ... }防盗链就不建议开了。很多用户是在命令行用wget下载Referer都是空的开严格防盗链相当于把命令行用户全挡在外面。5.2 缓存相关的坑与处理缓存最大的坑是Range请求。浏览器或下载工具做断点续传时携带Range: bytes...请求头命中缓存后如果缓存键和文件处理不当可能出现返回内容损坏、拼接错位的问题。如果发现下载工具有兼容性问题最简单的方案是在release文件反代的location里对带Range头的请求跳过缓存直接转发给后端location /ghproxy/ { # ... if ($http_range) { set $no_cache 1; } proxy_no_cache $no_cache; proxy_cache_bypass $no_cache; }牺牲一点缓存命中率换来下载稳定性我觉得很值。release文件走Nginx转发时回源速度本身就不差缓存更像锦上添花。另一个坑是缓存目录权限。proxy_cache_path指向的目录如果Nginx工作进程没有写权限缓存不会生效但access_log里看不到任何异常。只有加上add_header X-Cache-Status $upstream_cache_status;看到响应里MISS一直不变HIT才会发现是权限问题。所以创建缓存目录后要记得把这些文件chown给nginx用户。5.3 运维监控与安全加固建议我实际运维了几个月积累了几个小经验。第一日志里一定要加上回源耗时。在log_format里追加$upstream_response_time如果这个值长期超过1秒说明服务器到GitHub的上游链路不行需要考虑换机房或换DNS解析节点。第二用crontab定期清理过期的release缓存。虽然inactive参数会自动淘汰但磁盘空间紧张时手动清理更快例如每天凌晨跑一次0 3 * * * find /data/nginx-cache/github -type f -mtime 7 -delete第三用uptime-kuma或类似工具做可用性监控每5分钟检测一次release下载是否返回200。我经历过一次上游证书调整导致镜像站整体不可用还是用户来反馈我才发现监控一声不吭。那次之后我立刻把监控补上了告警直接推到群里。安全方面把server_tokens off;加上避免暴露Nginx版本号如果服务器只跑镜像站防火墙只放行80和443端口就够了。最后说一点个人体会。搭GitHub镜像站这件事技术上并不难难的是把链路拆清楚、把细节做扎实。我第一次搭的时候走了很多弯路最深的教训就是不要急着一股脑反代所有域名先在自己浏览器里打开Network面板看一遍release下载的完整跳转链路你很快就知道该代理什么、改什么。镜像站上线之后团队下载依赖包、拉源码的速度提升非常明显这笔投入我觉得很值。如果后面有精力还可以把镜像站扩展成内网包管理镜像、NPM/PyPI镜像的统一入口复用同一套Nginx缓存架构。GitHub镜像只是起点背后的网络代理和缓存工程经验才是真正能沉淀下来的东西。