
做开发这些年我前前后后搭过好几套GitHub镜像站有给团队内部用的也有给实验室共享资源的。镜像站本质上就是在你和GitHub之间加一个可控中转点让你日常clone代码、下载Release安装包、访问raw文件的时候不再受海外站点网络质量波动的影响。这篇文章就是一份GitHub镜像站搭建全流程指南从架构设计、域名拆分、Nginx配置到缓存限流、安全加固和排障按我实操的顺序完整走一遍。如果你正好在折腾github连接不稳定、clone超时、脚本拉raw文件失败这些事又想自己维护一套内部镜像这篇文章可以直接照着做。先说清楚我不是介绍某个现成镜像服务而是教你自己从一台空服务器开始把镜像站跑起来。整个过程只需要一台能正常访问GitHub的服务器、一个域名、一个SSL证书以及一点耐心。适合运维、后端开发和实验室管理员也适合对Nginx配置有兴趣、想彻底搞懂反向代理细节的同学。1. 镜像站到底要解决什么问题1.1 先搞清楚你要做哪一类镜像站很多人一听到“镜像站”第一反应是“把GitHub整个网站复制一份”。这个理解既对也不对。对的是镜像确实让你能通过自己的域名访问到GitHub的内容不对的是你几乎不可能也没必要把GitHub全站数据同步到本地。实际工作中大家需要的镜像通常是下面这四种形态中的一种或几种镜像形态典型场景对外表现的域名难度Web页面镜像浏览仓库、看issue、看README和文件列表github.mirror.example.com低Git仓库镜像团队固定维护一批仓库clone、fetch稳定快速git.mirror.example.com中Release下载加速下载发布包、二进制文件避免下载到一半断掉release.mirror.example.com中API只读代理对接GitHub API做自动化统计、机器人、CI回调api.mirror.example.com低这四种形态可以独立存在也可以组合到一起。我见过不少团队只做Release下载加速因为他们的核心痛点是“安装包怎么也下不动”也见过实验室只做Git仓库镜像因为学生需要大量clone仓库做课程作业。你动手之前先想清楚自己真正需要哪一种避免一上来就把范围铺得太大。1.2 镜像站和本地加速工具的本质区别市面上有不少现成的GitHub加速工具改个hosts、装个插件、或者用某个加速域名拼一下URL确实能解燃眉之急。但它们都有一个共同问题只解决“个人访问”场景不解决“团队共享”场景。镜像站是一个长期运行的中间服务团队成员只要把域名换成你的镜像域名之后所有人的clone、下载、API请求都走同一个稳定入口。运维人员可以统一做缓存、限流、日志分析、访问控制这是零散工具完全做不到的。用一个直观点的时间线来对比临时方案今天网络卡了改hosts解决明天换了网络又要重新折腾项目里有20个成员你得帮每个人配一遍。镜像站方案配置一次写进团队文档Nginx替你扛下所有回源压力访问速度稳定可预期。所以我的建议是如果你只是个人偶尔用没必要自建如果你想给团队、班级、实验室提供一个长期稳定的访问入口镜像站才是正解。1.3 什么情况下不建议自己搭镜像站不是万能的也不是所有场景都适合自建。如果你属于下面几类情况我更建议你使用现成的开源加速服务或者商业CDN省心很多团队非常小只有三五个人偶尔clone一次用不到专门的镜像。你没有一台海外网络质量稳定的服务器镜像站前端的网络都自身难保回源质量可想而知。你需要的只是某个大文件的一次性加速下载不值得为此维护一套长期服务。你不想处理证书续期、缓存清理、突发流量这些运维琐事。我见过一些同学兴冲冲搭好镜像站结果源站连通性不稳定镜像站整天502最后又拆掉。所以搭建之前先确认服务器到GitHub的回源链路是否稳定这是最容易被忽略的硬性前提。1.4 一台什么配置的服务器够用镜像站对服务器性能要求并不高核心瓶颈通常在网络带宽和磁盘IO上。以一个50人左右的团队为例2核4G、带宽10Mbps的轻量云服务器完全够用。内存主要被Nginx进程和系统缓存占据2G内存也可以跑4G更从容。磁盘方面因为要缓存clone的压缩包和Release附件建议至少保留100GB可用空间。如果只做Web页面镜像和raw文件加速50GB就够了。流量方面加了缓存之后重复请求基本不会回源实际消耗会远小于你的预期这一点后面会在缓存章节细说。2. 整体架构与域名拆分设计2.1 域名规划一个入口五个子域名镜像站的第一设计决策不是配置语法而是域名规划。GitHub的线上服务其实分布在多个不同的域名上每个域名承载的请求特性完全不一样。你如果只是简单地把所有请求都转发到github.com大概率会在某个环节踩坑。我的建议是先规划好下面这张域名映射表GitHub原始域名你的镜像域名服务的资源主要特征github.comgithub.mirror.example.com网页、git clone、动态内容页面API混合缓存价值低raw.githubusercontent.comraw.mirror.example.com文件原始内容小文件适合缓存codeload.github.comcodeload.mirror.example.com仓库zip/tar.gz打包下载大文件流量大头objects.githubusercontent.comrelease.mirror.example.comRelease附件、Git LFS对象大文件需要处理302跳转api.github.comapi.mirror.example.comREST API接口JSON响应需要限流这个表格看起来简单但它决定了后面所有Nginx配置的骨架。五个子域名分别对应五个server块互不干扰每个域名可以单独设置缓存时间、限流策略、日志文件。如果你暂时不需要某个功能比如不需要API代理那就不配置对应子域名等需要时再加不影响整体结构。2.2 为什么必须拆五个域名很多第一次搭镜像站的朋友都会问我我只反代github.com不行吗答案是真不行原因有两层。第一层是资源加载问题。GitHub页面里面充斥着大量指向raw.githubusercontent.com、objects.githubusercontent.com、avatars.githubusercontent.com等其它域名的请求。你只镜像github.com页面框架能打开但头像、README里的图片、Release下载按钮全部指向原始域名用户访问时照样不稳定体验还是断裂的。第二层是响应特性差异太大。github.com的HTML页面动态性强不适合长缓存raw文件内容稳定可以直接缓存一小时以上codeload和release下载是几十MB到几百MB的大文件需要完全不同的缓冲策略API接口有严格的速率限制必须单独做限流。把它们混在一个server块里配置会变成一团乱麻出问题也不好排查。所以拆域名不是故意折腾而是让流量天然分流后面每一层的缓存、限流、监控都更有针对性。2.3 回源DNS的动态解析问题Nginx的proxy_pass如果直接写成proxy_pass https://github.com;在Nginx启动或reload时只会对域名做一次DNS解析。一旦GitHub的解析结果发生变化或者解析服务暂时不可用你的镜像站可能直接启动失败。更好的做法是使用resolver配合变量的方式让Nginx在运行期动态解析。这样即使DNS变化Nginx也能在下一个请求时用新的解析结果重新连接上游不需要人工干预。下面这段配置就是动态解析的标准写法resolver 1.1.1.1 8.8.8.8 valid30s; set $github_upstream github.com; proxy_pass https://$github_upstream;这里valid30s表示DNS解析结果每30秒重新验证一次。你可以根据实际网络情况调整这个值但不要设得太短否则DNS请求频率过高会带来额外延迟。2.4 SSL证书与请求头设计所有子域名都需要配置HTTPS证书建议统一使用Lets Encrypt免费证书通过certbot申请并配置自动续期。证书申请成功后把fullchain.pem和privkey.pem放在/etc/nginx/certs/目录后续所有server块统一引用。反代GitHub时有一个特别容易踩的坑没有传递正确的SNIServer Name Indication和Host头。GitHub服务端会根据这两个信息判断你要访问的站点如果缺失或错误会返回403或触发风控。必须在每个server块里显式加上这两行proxy_ssl_server_name on; proxy_set_header Host github.com;proxy_ssl_server_name on让Nginx在建立上游TLS连接时发送正确的SNIproxy_set_header Host把原始Host传递过去。这两个参数组合在一起GitHub才认为你是一个合法客户端。3. 从零开始搭建核心步骤3.1 基础环境与连通性检查我以Ubuntu 22.04为例其他发行版命令大同小异。先安装Nginx和certbot然后做一次基础连通性检查。apt update apt install -y nginx certbot python3-certbot-nginx curl -I https://github.com curl -I https://raw.githubusercontent.com curl -I https://codeload.github.com这三条curl命令很关键。镜像站的前置条件是服务器到上述三个域名都能正常访问只要有一个不通对应功能就做不了。我见过有人搭好主站才发现raw文件全挂回头一查是服务器到raw域名的回源本身就有问题。所以这个检查一定要放在最前面做不要跳过。3.2 配置GitHub主站反向代理主站的Nginx配置是整个镜像站的地基。这里给出一个可直接使用的完整server块server { listen 443 ssl http2; server_name github.mirror.example.com; ssl_certificate /etc/nginx/certs/fullchain.pem; ssl_certificate_key /etc/nginx/certs/privkey.pem; resolver 1.1.1.1 8.8.8.8 valid30s; set $github_upstream github.com; proxy_ssl_server_name on; proxy_set_header Host github.com; proxy_set_header Accept-Encoding ; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_redirect https://github.com/ https://github.mirror.example.com/; proxy_redirect http://github.com/ https://github.mirror.example.com/; location / { proxy_pass https://$github_upstream; proxy_ssl_name github.com; } }这里有一个细节值得单独说明proxy_set_header Accept-Encoding ;这行看起来奇怪但它是让sub_filter替换生效的前提。如果不禁用上游的压缩响应Nginx收到的是gzip后的内容sub_filter无法正确改写内容会导致页面打不开或样式错乱。这个坑我最初踩过一次后来凡是做内容替换的代理都会默认带上这一行。3.3 处理页面中的链接与静态资源纯反代能打开页面但页面里的资源链接还指向原始域名。需要在主站server块内加上sub_filter把内容中的GitHub地址替换为镜像地址sub_filter_once off; sub_filter_types text/css application/javascript; sub_filter https://github.com https://github.mirror.example.com; sub_filter http://github.com https://github.mirror.example.com;这里我刻意只替换了github.com本身的链接没有把avatars.githubusercontent.com等资源域名强制改成镜像域名。为什么因为这些CDN资源本身走HTTPS而且大部分情况下是可以稳定访问的改了反而增加回源压力。如果你们网络环境对这类CDN域名访问也不稳定再单独加一层针对avatars.githubusercontent.com的镜像也行但不要一开始就全改。配置改完后用nginx -t检查语法然后systemctl reload nginx。打开浏览器访问https://github.mirror.example.com按F12看Network面板确认页面里所有请求都在镜像域名下且CSS、JS资源都正常加载。3.4 raw文件与源码压缩包下载加速raw文件和codeload是团队clone之外最常用的两个功能。它们的配置思路类似但细节不同。raw文件的server块server { listen 443 ssl http2; server_name raw.mirror.example.com; ssl_certificate /etc/nginx/certs/fullchain.pem; ssl_certificate_key /etc/nginx/certs/privkey.pem; resolver 1.1.1.1 8.8.8.8 valid30s; set $raw_upstream raw.githubusercontent.com; proxy_ssl_server_name on; proxy_set_header Host raw.githubusercontent.com; location / { proxy_pass https://$raw_upstream; proxy_ssl_name raw.githubusercontent.com; proxy_cache github_cache; proxy_cache_key $uri$is_args$args; proxy_cache_valid 200 1h; add_header X-Cache-Status $upstream_cache_status; } }codeload的server块大文件比较多需要特别处理大文件缓冲server { listen 443 ssl http2; server_name codeload.mirror.example.com; ssl_certificate /etc/nginx/certs/fullchain.pem; ssl_certificate_key /etc/nginx/certs/privkey.pem; resolver 1.1.1.1 8.8.8.8 valid30s; set $codeload_upstream codeload.github.com; proxy_ssl_server_name on; proxy_set_header Host codeload.github.com; location ~ \.(zip|tar\.gz|tgz)$ { proxy_pass https://$codeload_upstream; proxy_ssl_name codeload.github.com; proxy_cache github_cache; proxy_cache_key $uri$is_args$args; proxy_cache_valid 200 7d; proxy_max_temp_file_size 4096m; proxy_buffering off; add_header X-Cache-Status $upstream_cache_status; } }这里proxy_buffering off的意思是关闭代理缓冲下载响应直接流式转发给客户端而不是先在Nginx临时文件里攒一份。这样对大文件下载更省内存但代价是不经过Nginx磁盘缓存。实际测试下来对1GB以上的Release包直接流式转发体验更好如果想同时兼顾缓存可以去掉这行但要把proxy_max_temp_file_size调大避免大文件缓存写失败。3.5 Release附件302跳转的处理Release下载和Git LFS对象藏在objects.githubusercontent.com这个域名后面GitHub返回的通常是一个302跳转地址。如果你只反代到github.com浏览器访问Release下载链接时会先到github.com拿302然后自动跳到objects.githubusercontent.com。镜像站要正确处理这个跳转核心是两件事。第一在主站server块的proxy_redirect里把真实跳转地址改写为镜像域名proxy_redirect https://objects.githubusercontent.com/ https://release.mirror.example.com/;第二单独为release.mirror.example.com建一个server块代理objects.githubusercontent.comserver { listen 443 ssl http2; server_name release.mirror.example.com; ssl_certificate /etc/nginx/certs/fullchain.pem; ssl_certificate_key /etc/nginx/certs/privkey.pem; resolver 1.1.1.1 8.8.8.8 valid30s; set $objects_upstream objects.githubusercontent.com; proxy_ssl_server_name on; proxy_set_header Host objects.githubusercontent.com; location / { proxy_pass https://$objects_upstream; proxy_ssl_name objects.githubusercontent.com; proxy_cache github_cache; proxy_cache_valid 200 30d; proxy_max_temp_file_size 8192m; proxy_buffering off; add_header X-Cache-Status $upstream_cache_status; } }这样用户点击Release下载按钮时完整的链路是先请求你的主站拿到改写后的302跳转然后浏览器直接请求release.mirror.example.com由它回源拉取真实文件。链路虽然比直连多一跳但全程都在你控制之下可以缓存、限流、记录日志。3.6 给团队提供的Git clone加速方案网页和下载都搞定之后还有最后一块拼图git clone加速。这个场景有好几种实现思路按复杂度从低到高排序。最轻量的方式是让团队成员修改git配置走你的镜像域名git config --global url.https://github.mirror.example.com/.insteadOf https://github.com/这样git clone https://github.com/owner/repo.git会自动改写为https://github.mirror.example.com/owner/repo.git走镜像站回源。这种方式配置简单但每个成员都要执行一次命令适合快速落地。如果你要镜像一批固定仓库比如团队内部的核心依赖库可以写一个定时同步脚本git clone --mirror https://github.com/owner/repo.git /data/mirror/repo.git cd /data/mirror/repo.git git remote update然后把这批仓库用git daemon或cgit暴露出去团队clone时直接走本地仓库速度比走Nginx回源更快而且源站万一临时不可用也不受影响。代价是你要维护仓库列表和同步频率属于一劳永逸但前期投入更大的方案。4. 缓存、限流与安全加固4.1 磁盘缓存配置与缓存时间策略镜像站如果每次请求都回源海外链路带宽消耗会非常大。我最初搭的那版没有缓存团队十几个人一天用下来服务器带宽直接被打满。加缓存之后流量成本肉眼可见地降了下来。在/etc/nginx/nginx.conf的http块中定义缓存区域proxy_cache_path /var/cache/nginx/github levels1:2 keys_zonegithub_cache:50m max_size100g inactive30d;参数含义说明levels1:2表示缓存目录分两级避免单个目录文件过多。keys_zonegithub_cache:50m定义共享内存区域50MB大概能存几百万个缓存键。max_size100g是缓存磁盘上限根据你的磁盘空间调整。inactive30d表示30天内未被访问的缓存会被清理。缓存时间需要按资源类型分别设计。HTML页面动态性强缓存5到10分钟就够缓存太久会让用户看不到仓库的新提交raw文件内容相对稳定缓存1到2小时codeload和release附件基本不会变缓存一周甚至一个月都没问题。静态资源CSS、JS可以缓存7天头像这类更新频率低的资源可以缓存30天。4.2 限流与UA过滤镜像站一旦被公开就会被各种爬虫盯上。我见过有人在校园网上搭了个镜像不设限流一个月跑掉几个TB流量最后被网管约谈。所以限流必须从一开始就写进配置。在http块定义限流区域limit_req_zone $binary_remote_addr zonegithub_web:10m rate10r/s; limit_req_zone $binary_remote_addr zonegithub_api:10m rate5r/s;在server块中引用location / { limit_req zonegithub_web burst20 nodelay; proxy_pass https://$github_upstream; }API单独用更严格的限流location / { limit_req zonegithub_api burst10 nodelay; proxy_pass https://$api_upstream; }UA过滤也很重要。很多异常流量来自脚本或扫描工具可以通过Nginx的if块直接拒绝if ($http_user_agent ~* (curl|python-requests|scrapy|masscan|nmap)) { return 403; }但要注意这个规则不要误伤正常开发者比如有些程序员喜欢用curl测试接口看你的场景决定是否要这么严格。如果镜像站只给内部团队用更稳妥的方式是加一层HTTP Basic Authauth_basic Private Mirror; auth_basic_user_file /etc/nginx/.htpasswd;内部工具链再配合token或专属Header做访问控制基本上可以杜绝绝大多数外部滥用。4.3 证书自动续期与日志监控Lets Encrypt证书有效期90天手动续期不现实。用certbot的自动续期任务处理echo 0 3 * * * certbot renew --quiet --deploy-hook systemctl reload nginx | crontab -每天凌晨3点检查一次证书到期前自动续期并重载Nginx。这个定时任务一般配一次就不用管了但如果你自建CA或者使用其他证书方案需要自己维护续期逻辑。日志方面五个子域名的访问日志最好分开存方便排查。在每个server块里单独指定日志文件access_log /var/log/nginx/github_main.access.log; error_log /var/log/nginx/github_main.error.log;这样哪类流量异常、哪个域名被刷打开对应日志一目了然。我还会用goaccess定期生成访问报告看看哪些路径最热门、哪些仓库下载量最大为后续优化提供依据。4.4 一段简单的健康检查脚本镜像站挂了团队成员往往比你知道得还早。与其等用户报障不如写个健康检查脚本每分钟检查一次主站和几个关键域名。#!/bin/bash urls( https://github.mirror.example.com https://raw.mirror.example.com https://codeload.mirror.example.com https://release.mirror.example.com ) for url in ${urls[]}; do code$(curl -s -o /dev/null -w %{http_code} --max-time 10 $url) if [ $code ! 200 ] [ $code ! 301 ] [ $code ! 302 ]; then echo $url return $code /var/log/mirror-healthcheck.log fi done把脚本放进crontab每分钟跑一次只记录异常不告警。如果配合企业微信或者钉钉机器人可以把异常推送推到你手机上效果更好。健康检查的重点不是发现已经在线的故障而是尽早发现回源质量劣化比如响应速度变慢、证书即将过期这类问题通常比直接挂掉更隐蔽。5. 常见问题与排查实录5.1 问题速查表把我在实操中遇到的高频问题整理成一张表方便你直接对照排查现象可能原因排查思路页面能打开但CSS全挂sub_filter没有生效或上游压缩未禁用检查proxy_set_header Accept-Encoding ;是否配置到位clone成功但push失败镜像站不支持写入push提示用户push直连GitHub或用SSH协议直连Release下载变成连环302只配置了主站反代没有处理跳转域名检查proxy_redirect和release.mirror.example.com是否配置下载到一半断掉大文件缓冲策略不对或磁盘空间不足检查proxy_buffering、proxy_max_temp_file_size和df -h返回502 Bad Gateway回源DNS解析失败或源站连接超时用curl -I逐层测试源站连通性检查resolver配置缓存了错误的Error页面proxy_cache_valid把5xx也缓存了单独限制只缓存200和301/302或加proxy_cache_valid 500 502 1m;冷门配置访问很慢但不是502回源链路质量不稳定检查服务器到GitHub各域名的延迟和丢包确认网络没有跑到对端限速区间5.2 案例一页面正常但CSS/JS全部加载失败这是我第一次搭镜像站时踩的坑。当时页面框架能打开但所有样式和脚本都是空白F12一看全是mime type错误。排查了半天发现是上游返回的Content-Encoding: gzip没有被解压Nginx拿到的是一堆压缩乱码sub_filter自然无法替换内容。解决办法就是在主站server块加上proxy_set_header Accept-Encoding ;让上游返回未压缩的内容。这个参数对网页类代理是通用的但很多Nginx教程根本不会提因为普通反代不需要改写内容。5.3 案例二Release下载无限重定向另一个印象深刻的问题是Release下载。配置主站反代之后点击下载按钮浏览器一直在跳转页面提示“too many redirects”。原因是GitHub返回的302 Location指向objects.githubusercontent.com而我没有做任何处理浏览器跟着跳到原始CDN一旦这个域名访问不稳定就会卡在跳转链路上。解决方案就是我前面写的proxy_redirect加独立release.mirror.example.comserver块。这里要特别提醒proxy_redirect改写的不仅是Location响应头如果页面JS内部通过API获取下载URL你还需要在API响应体的JSON里做字符串替换。这一层最容易被忽略建议搭完Release加速后找一个真实项目点一次下载按钮验证全链路。5.4 一个值得养成的习惯把配置纳入版本管理镜像站的配置会不断演进今天加一个域名明天调一个缓存策略后天又发现某个子域名需要单独限流。如果不把配置管起来你会陷入“改坏了不知道哪里改坏了”的泥潭。我现在的做法是把整个/etc/nginx/做成一个git仓库用Ansible管理所有服务器状态。每次修改配置先提交到仓库再发布到服务器。这样任何一次变更都有记录出了问题可以直接对比上一版配置。这套流程听起来重但搭建一个镜像站之后你大概率会发现它的有效期长达一两年期间配置会被反复调整版本管理投入的成本早就赚回来了。我个人在实际操作中的体会是镜像站能不能长期稳定跑下去比拼的往往不是Nginx配置有多花哨而是缓存、限流、监控这三件事有没有做到位。缓存决定你的带宽成本限流决定你安不安全监控决定你能否在用户抱怨之前发现问题。如果你也要搭建议先从最朴素的Web主站反代起步跑通之后再逐步加入raw、codeload和Release加速最后做Git仓库镜像。不要一上来就想四五个域名全部一次配齐——分层迭代、先跑通再优化才是这套体系最稳妥的落地方式。