ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

用Nginx搭建GitHub镜像站:缓存配置与仓库同步实践

2026/10/6 14:02:19 拓冰建站 浏览量
用Nginx搭建GitHub镜像站:缓存配置与仓库同步实践 帮团队搭一个 GitHub 镜像站听起来是一句话的事真从零开始做的时候涉及域名、证书、回源、缓存、大文件下载、仓库同步、访问日志分析……每一个环节都有坑。这篇文章把我这次搭建 GitHub 镜像站的完整过程和选型思路整理出来适合正在调研镜像站方案的读者也适合已经搭了一版但总感觉不好用的人。先说清楚我到底做了什么我在一台独立服务器上用 Nginx 做了一个 Web 层中转网关把 GitHub 的页面、仓库、release 下载和部分 API 请求通过镜像域名对外提供服务同时配合 Git 侧的裸仓库同步策略让团队在内部网络环境下可以快速拉取常用开源项目。整个过程花了两天第一版只用了半天剩下的时间全在补缓存和踩重定向的坑。1. 为什么自建镜像站先想清楚适合不适合1.1 三个真正需要镜像站的场景镜像站不是一个“装上去就能用”的东西它本质上是用你的带宽和服务器资源去换访问体验。所以先判断场景对不对。第一个常见场景是研发团队内部网络。假设你们团队有几十个人每个人入职第一件事都是git clone那十几个固定仓库每个仓库几百 MB 到几个 GB。重复从公网拉取既浪费时间也占用出口带宽。搭一个镜像站之后clone 请求走本地网络几十个人的体验都能拉齐。第二个场景是项目本身对外发布开源软件或固件。你维护的项目如果经常被下载可以做一个自己的镜像入口把 release 文件缓存到离用户更近的节点降低原始托管站点压力。很多大型开源项目都会维护官方镜像列表就是这个思路。第三个场景是离线环境或者受限网络。有些机房只能访问白名单域名而 GitHub 不在名单里。这种情况下你可以在有公网访问权限的跳板机上搭一个镜像站把常用仓库同步到内网再让内网服务器从这个镜像站拉取。这种做法在企业离线开发环境里非常常见。1.2 镜像站的功能边界它不是什么都能做这条一定要写在最前面因为很多人对镜像站有误解。GitHub 镜像站能解决的是“读”的问题浏览公开仓库页面、下载 release 包、git clone公开仓库、拉取 raw 文件。这些都是常规镜像工具可以覆盖的。镜像站不能解决的是“写”的问题。你不能通过镜像站给别人的仓库提 issue、不能发起 pull request、不能把本地 commit 推送到镜像地址上。因为镜像站的本质是一个缓存的端点不是 GitHub 本身。还有些团队想用镜像站绕过身份认证访问私有仓库这条路不要走。私有仓库的授权逻辑很复杂涉及签名、token、多种认证头强行做出来不仅容易泄露内部凭证而且任何一次 GitHub 侧接口变化都会让你的镜像站直接崩掉。我在搭建之前特意把需求列成了表格确认我们到底需要镜像站做什么需求是否需要镜像站实现方式快速 clone 公开仓库需要Git 仓库同步 本地 clone 服务下载 release 压缩包需要静态文件缓存 回源浏览仓库页面可选Web 层中转 页面缓存浏览 raw 文件可选静态文件回源缓存推送代码、提 PR、发 issue不需要无法通过镜像站实现私有仓库访问不建议尽量走原始站点2. 架构拆解从一个 clone 请求看镜像站要做什么2.1 一条请求经过的完整链路很多人以为镜像站就是“把网址换一下请求转发过去”。实际上一次最简单的git clone https://mirror.example.com/platform/repo.git背后要经过至少四层处理。第一层是 DNS 和证书。用户访问镜像域名DNS 解析到你的服务器服务器启用 HTTPS必须给这个镜像域名签发证书。如果这一步没做好用户会看到证书告警直接劝退。第二层是 Web 网关。Nginx 接收请求之后要把mirror.example.com/platform/repo.git这个 URL 改写回 GitHub 的原始地址github.com/platform/repo.git然后带着正确的 Host 头去访问 GitHub。这一步最关键的是 Host 头。很多镜像站的坑都是因为抓到了 GitHub 的 IP但没有带原始 HostGitHub 返回了默认的 404 页面。第三层是重定向处理。GitHub 很多资源不是一次性返回的。页面会跳转到登录、release 文件会跳转到objects.githubusercontent.com或者release-assets.githubusercontent.comraw 文件会跳转到raw.githubusercontent.com。你的镜像站必须把这些跳转也“接住”否则用户点一下下载按钮浏览器就直连 GitHub 去了镜像站根本没有起作用。第四层是缓存层。镜像站建起来之后最怕的是每次请求都回源。如果每个 release 文件都回源下载你的出口带宽很快被打满而且 GitHub 侧也不会给你好脸色。所以必须设计多级缓存。2.2 两种主流的镜像站形态我把它抽象成两种路线你可以根据自己的场景选。第一种叫“流量中转型”适合需要保持 URL 结构和 GitHub 一致的情况。用户在浏览器里访问mirror.example.com/git/git看到的是和 GitHub 几乎一样的页面点击 clone 下载时请求都经过你的服务器。这种形态的好处是兼容性好不需要用户改 URL 习惯坏处是页面结构经常变你需要持续维护路由规则。第二种叫“仓库同步型”适合最终目的是稳定 clone 的场景。你不去做 Web 页面中转只是用定时任务把 GitHub 上的仓库完整同步到本地磁盘然后通过 Git 协议或者 HTTPS 对内网提供服务。这种形态简单可靠但用户看不到 README 的渲染页面只能 clone 代码。我最终的方案是两者结合用 Nginx 做轻量 Web 中转同时用裸仓库同步解决核心仓库的 clone 性能。GitHub 的页面结构虽然会变但我只中转必要的几个路径前缀把维护成本控制在最低。3. 动手之前的容量设计与证书准备3.1 磁盘容量、带宽和并发怎么算容量设计是镜像站最容易估算错的部分。Git 仓库本身有膨胀速度release 文件更是体积大户。第一次规划时我按 30 个仓库、每个仓库平均 Git 历史 2GB 来算觉得 120GB 磁盘足够。实际上很多仓库裸仓库体积是工作区文件的好几倍尤其是带有大量二进制历史的大厂项目。再加上 release 缓存我最终给系统盘和数据盘分别做了规划系统盘 50GB数据盘直接上了 500GB并且留了软链接方便后续扩盘。带宽方面我的计算方法比较简单平均 release 文件大小 × 日预期下载次数 ÷ 86400 秒 × 8 最低出网带宽。比如平均 release 包 300MB一天下载 200 次平均带宽约 5.5Mbps但这个数字没有计算峰值。我按峰值加 5 倍余量直接购买了 100Mbps 出口带宽的机器。如果预算有限至少要保证 30Mbps 以上否则突发流量一来所有用户都会感觉卡死。并发数不需要太高。团队内部使用Nginx 的 worker_processes 设置成 CPU 核数每个 worker 连接数调到 4096足够支撑几百人同时拉取。如果是对外公开的镜像站就要考虑使用 CDN 做一层前置缓存不然单机很容易被打到资源耗尽。3.2 证书与域名镜像站的可信度基础镜像站如果证书不规范会被 Git 客户端直接拒绝。我强烈建议不要用自签名证书因为团队里每个人都要手动把证书加进信任列表换电脑还要再做一次非常痛苦。正规做法是申请一个泛域名证书或者为镜像域名单独签一个证书。如果你用的是自己的域名Lets Encrypt 免费证书足够了。签发命令很简单sudo apt install certbot sudo certbot certonly --standalone -d mirror.example.com --email adminexample.com --agree-tos --no-eff-email如果服务器上已经跑了 Nginx要先把 80 端口暂时空出来或者用--nginx插件自动修改配置。我实际操作时直接用--webroot方式签发避免中断服务。证书签发之后配置 Nginx 时记得把证书链完整写进去。Lets Encrypt 的证书文件是fullchain.pem和privkey.pem两个文件不要只配公钥不配链。很多人第一次配置后浏览器提示“证书链不完整”就是因为只写了 cert 而没写 chain。还有一个容易踩的坑令牌和签名文件的验证路径。GitHub 在 Webhook 或者认证流程中会请求特定路径但镜像站做中转时经常把这些路径也缓存了导致回调验证失败。所以在证书和路由设计阶段就要明确哪些路径绝对不缓存。4. Nginx 网关配置把 GitHub 流量转回本地4.1 最小可用的中转配置这一段直接给配置。我没有把镜像站做成全量站点只处理四个重要路径片段仓库页面、raw 文件、release 下载和 API 访问。map $http_user_agent $ignore_cache { default 0; ~*git 1; } server { listen 443 ssl http2; server_name mirror.example.com; ssl_certificate /etc/letsencrypt/live/mirror.example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/mirror.example.com/privkey.pem; gzip on; gzip_types text/css application/json application/javascript; # GitHub 仓库页面 location / { proxy_pass https://github.com; proxy_ssl_server_name on; proxy_set_header Host github.com; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; # 关键不要把 git clone 请求缓存 if ($ignore_cache) { proxy_no_cache 1; proxy_cache_bypass 1; } # 缓存页面静态资源 proxy_cache github_pages; proxy_cache_valid 200 5m; proxy_cache_key $host$uri$is_args$args; } # raw 文件 location /raw/ { rewrite ^/raw/(.*)$ /$1 break; proxy_pass https://raw.githubusercontent.com; proxy_ssl_server_name on; proxy_set_header Host raw.githubusercontent.com; proxy_cache github_raw; proxy_cache_valid 200 24h; } # release / codeload 下载 location /download/ { rewrite ^/download/(.*)$ /$1 break; proxy_pass https://codeload.github.com; proxy_ssl_server_name on; proxy_set_header Host codeload.github.com; proxy_cache github_download; proxy_cache_valid 200 30d; } }这个配置有两点要注意。第一Git 客户端请求的 User-Agent 通常包含 “git/”我用 map 判断出来之后强制跳过缓存。因为 git 的 fetch 请求里带了很多动态参数缓存会让仓库信息过期。第二raw 和 download 两个路径我都做了前缀跳转让用户访问https://mirror.example.com/raw/owner/repo/main/README.md时能对应到 raw.githubusercontent.com 的原始 URL。4.2 处理跳转最容易忽略的重定向链路上面配置里最容易被忽略的就是 redirect 和 cookie。GitHub 的 release 下载过程是一个两步跳转。你点击下载按钮首先请求https://github.com/owner/repo/releases/download/v1.0.0/file.zipGitHub 返回 302 跳转到https://objects.githubusercontent.com/github-production-release-asset-...浏览器再跟随跳转下载。如果镜像站只中转第一跳不处理第二跳用户下载时表面上蹦出来一个 github.com 的地址实际上最终请求又被弹回远程镜像站等于白搭。解决思路有两个。一个是给 Nginx 开启proxy_intercept_errors并手动改写 Location 头把 302 响应的 Location 替换成镜像域名对应的路径。另一个是我选择的做法不接管 302而是让浏览器直接跟随跳转同时利用 DNS 和网络策略保证objects.githubusercontent.com在团队内网里可以访问。说实话要做到完全接管所有资源的跳转非常复杂因为 GitHub 的 CDN 域名不止一个。我更推荐的做法是镜像站主要负责仓库页面和 codeload 下载而最终落到对象存储的大文件在缓存层处理。用 Nginx 处理大文件缓存需要额外增加一个 upstream 指向objects.githubusercontent.comlocation /github-production-release-asset- { proxy_pass https://objects.githubusercontent.com; proxy_ssl_server_name on; proxy_set_header Host objects.githubusercontent.com; proxy_cache github_release_cache; proxy_cache_valid 200 30d; proxy_max_temp_file_size 0; }如果你不做这一层那么最终下载行为还是会发生从镜像站跳转到第三方域名的情况。对内部使用来说没问题但如果你的目标场景是面向外网发布固件就最好把这一层做完整。4.3 大文件下载的断点续传与缓存细节GitHub 的 release 包动辄几百 MB断点续传能力不可少。Nginx 默认支持Range请求但前提是缓存模块不能自己把响应体完整吞掉。我实测过如果开启proxy_cache且没有设置proxy_max_temp_file_size 0Nginx 会把文件先写满临时文件再决定是否缓存大文件下载时第一发请求会非常慢。推荐的配置是在大文件 location 里加上proxy_max_temp_file_size 0; proxy_buffering off; sendfile on; sendfile_max_chunk 1m;proxy_max_temp_file_size 0的意思是不限制临时文件大小让 Nginx 一边写缓存一边响应客户端避免用户等太久。sendfile_max_chunk 1m限制单次 sendfile 的数据量避免一个下载请求长期占用 worker 进程影响其他请求响应。断点续传验证也要做。镜像站上线之前我用curl -I确认响应头里带Accept-Ranges: bytes然后用 git clone 大仓库时 CtrlC 中断再git fetch续传确保不会出现文件损坏。5. 缓存策略与 Git 仓库同步让镜像真正省带宽5.1 Web 层的缓存分层镜像站的核心价值是“同样的内容只回源一次”。缓存建设比中转配置更见功力。我把缓存分成了三层。第一层是 HTML 页面缓存时间 5 分钟。GitHub 仓库首页的 README、文件列表、提交历史更新频率不高5 分钟短缓存足够且不会让用户看到过于过期的数据。第二层是静态资源比如 CSS、图片、raw 文件缓存时间 24 小时。第三层是 release 文件和 git 打包文件缓存 30 天。因为 release 文件一旦发布基本不可变缓存期越长越好。在 Nginx 配置里缓存目录要提前建好mkdir -p /data/nginx_cache/github_pages mkdir -p /data/nginx_cache/github_raw mkdir -p /data/nginx_cache/github_release然后在 http 块里定义缓存区proxy_cache_path /data/nginx_cache/github_pages keys_zonegithub_pages:100m max_size10g inactive1d use_temp_pathon; proxy_cache_path /data/nginx_cache/github_raw keys_zonegithub_raw:100m max_size50g inactive7d use_temp_pathon; proxy_cache_path /data/nginx_cache/github_release keys_zonegithub_release:1g max_size300g inactive30d use_temp_pathon;keys_zone里的 100m 是缓存索引的内存大小max_size是磁盘上最多放多少缓存。这些数值要结合你磁盘实际规划来写不是写得越大越好。缓存命中率怎么看直接在 Nginx 的响应头里加一段自定义字段add_header X-Cache-Status $upstream_cache_status;然后访问镜像站页面用浏览器开发者工具看X-Cache-Status字段。HIT表示命中MISS表示回源了EXPIRED表示缓存过期重新回源。上线前我用这个字段做了多轮验证确保常见资源的命中率在 90% 以上。5.2 Git 仓库同步从裸仓库到内网服务如果你连的是常用仓库我强烈建议你也做一层 Git 裸仓库同步而不是仰赖 Web 缓存。Web 缓存处理git clone也能生效因为 clone 本质上是在下载一个打包文件。但对于频繁更新的仓库缓存很容易过期。而裸仓库同步是 Git 对 Git 的复制更加原生和高效。裸仓库同步的做法很简单。第一次拉取时使用--mirror参数表示复制远端的所有分支、标签、refs 到本地mkdir -p /data/git_mirror cd /data/git_mirror git clone --mirror https://github.com/owner/repo.git之后通过 cron 定期更新cd /data/git_mirror/repo.git git remote update --prune每次更新之后不需要额外做 ISO 打包因为裸仓库本身就是一个标准 Git 仓库。内网用户可以直接用git clone http://mirror.example.com:8080/owner/repo.git从这台服务器上拉取。注意这里我没有继续用 Nginx 上面的 443 端口而是在同一台机器上跑了 Gitea 实例做仓库浏览和 clone 服务。Gitea 本身就带“仓库镜像”功能可以在后台添加一个镜像仓库填写 GitHub 地址它会自动同步。这个方案比手动 crontab 更省心的地方在于同步状态可以在 Web 页面里看到失败时有重试机制而且用户浏览 README 和文件列表的体验比裸目录好得多。我最终的生产环境就是 Gitea 定时同步 Nginx Web 缓存三层结构。普通用户从镜像站主页搜索仓库点进去看 README再点 clone整个流程不会直接感受到 GitLab 和 GitHub 的差异。5.3 同步频率怎么定同步间隔不要一刀切。活跃仓库每 5 分钟同步一次没问题但如果你有几十个仓库频繁同步会占用大量网络和磁盘 IO。我按仓库活跃度把同步间隔分成三档仓库类型同步间隔策略核心依赖、每周有 release10 分钟全量 mirror 更新常用工具、偶尔 commit1 小时更新时只拉取较小增量冷门存档仓库每天每天凌晨同步一次git remote update本来只会拉取增量所以 10 分钟一次不会太消耗带宽。真正消耗资源的是 Gitea 在每次同步后重新扫描仓库和更新文件索引。仓库数量多时建议把同步任务错峰执行。6. 上线后的故障排查与运维细节6.1 三类高频故障证书、回源和重定向镜像站上线第一天我特意盯了几个常见故障点。这里直接把排查链路写出来。证书告警是最多用户的反馈。排查时先openssl s_client -connect mirror.example.com:443检查证书链然后确认 Nginx 配置里的证书路径没有写错。如果浏览器提示“连接不是私密连接”大概率是证书没包含中间证书。使用 Lets Encrypt 时直接用fullchain.pem就不会有这个问题但如果你从第三方证书商买证书一定要把中间证书和 domain 证书拼成一个文件再配置。回源失败的症状是页面部分内容能显示但 git clone 报 404或者下载文件时直接超时。先用基本 curl 测试curl -I https://mirror.example.com/owner/repo curl -I -L https://mirror.example.com/owner/repo/releases/download/v1.0.0/file.zip看看返回码是 404、301 还是 502。如果curl -I看到 301/302 但-L跟随之后到了原始域名说明重定向链路没有处理好。此时去查 Nginx 日志里有没有对应路径的 proxy 配置。GitHub 的 release 下载路径在迁移 CDN 之后换过几次如果你的镜像站是半年前配的定期做一次真实下载测试很重要。重定向问题的另一个症状是循环跳转。用户访问仓库主页浏览器疯狂刷新。这种情况通常是 Nginx 把 GitHub 返回的重定向又重写回了 GitHub造成死循环。排查时打开浏览器开发者工具看网络请求的重定向列表哪一步的地址不对就去改哪条 rewrite 规则。6.2 日志、监控与磁盘预警镜像站的日志价值极高。我几轮调优全靠日志。Nginx 的访问日志在默认配置里是不带缓存状态和上游响应时间的。建议先自定义日志格式log_format mirror_log $remote_addr [$time_local] $request $status $body_bytes_sent $request_time $http_user_agent $upstream_cache_status $upstream_response_time;把$upstream_cache_status和$upstream_response_time加进去之后你能直接看到哪些请求命中缓存、哪些请求回源耗时超过 2 秒。我以这个日志为依据把那些回源耗时长、命中率低的路径单独加了更长生命周期。磁盘预警一定不能省。Git 裸仓库、缓存目录、日志文件三者的增长速度都很快。我给服务器加了一个简单的磁盘监控脚本每天检查/data分区的使用率超过 80% 就告警。脚本逻辑不复杂核心就是用df -h /data配合 crontab但能省掉很多半夜爬起来的麻烦。#!/bin/bash USAGE$(df /data | awk NR2 {print $5} | tr -d %) if [ $USAGE -gt 80 ]; then echo $(date) /data usage ${USAGE}% | mail -s Mirror Disk Warning adminexample.com fi最后给一个很实用的建议刚开始搭不要追求全站镜像。把团队最常用的十几个仓库同步做好、release 缓存调好比你搭建一个看似包罗万象但处处失效的巨型站点有价值得多。镜像站维护的核心不是“转发”而是“记住”。只缓存你真正需要的内容控制好同步频率这个站才能真正省带宽、提高拉取体验。