
国内GitHub镜像站搭建全攻略这篇东西其实想写很久了。镜像站Mirror Site这词在开源圈、运维圈不算新鲜但很多朋友一听“镜像站”脑子里先想到的是“把整个 GitHub 全量同步下来”——我实话实说这个目标既不现实也没必要。我自己在几家公司、实验室里陆续搭过三四套镜像服务从最朴素的 Git 仓库裸同步方案到带 Web 管理界面的轻量平台都折腾过一轮这篇文章就把一套“能用、够稳、不折腾”的完整方案摊开讲。先说清楚它解决什么问题。最常见的场景是这么几个企业内部网络隔离开发机和公网之间带宽有限几十个人同时拉同一个开源项目仓库慢且不说还特别容易超时或者是某些离线环境需要定期获取一批固定的开源仓库做代码审计、依赖构建再就是个人开发者手头有十几个高频使用的仓库希望做一份本地备份顺便让局域网里几台机器共用这份缓存。这篇文章适合三类人看被团队内频繁拉取外部代码仓库搞得带宽告急的运维同学需要在离线或隔离环境里维护代码同步的研发负责人还有单纯想搞一套个人代码缓存服务的技术爱好者。你不需要对 Git 内部原理有多深的理解只要按步骤操作配合文末的排查手册基本能一次跑通。1. 镜像站的整体设计思路与需求拆解1.1 先搞清楚镜像站到底在“镜像”什么很多人把“GitHub 镜像”想复杂了。站在协议层面看Git 仓库的同步本质就是远端引用refs和对象objects的增量复制。一个开源仓库的完整内容在服务端就是.git目录里那一堆对象文件。所以最底层的镜像服务不需要网页界面不需要数据库甚至不需要 Git 客户端之外的任何额外软件——只要能把远端仓库的引用和对象定期抓回来再通过本地 Git 协议、HTTP 静态托管或者 SSH 对外提供读取服务这就已经是一个完全合格的镜像站了。我在第一套方案里就是用一个 Shell 脚本加 crontab 完成的。脚本遍历一份仓库清单逐个执行git clone --mirror做初次全量同步之后用git remote update --prune做增量更新。整个“站点”没有数据库、没有 Web 前端对外就靠 Nginx 把目录暴露成http://mirror.internal/repo.git这种格式。读者直接用git clone http://mirror.internal/xxx.git就能拉代码实际体验和从 GitHub 上拉几乎没差别。但纯静态方案有它的短板没有权限控制任何人只要能访问这台机器就能 clone 全部仓库也没有 Web 界面想看看某个仓库的提交记录、分支情况不方便。于是后期我在另一套系统里换成了 Gitea——它是一个轻量级的 Git 服务端二进制解压就能跑自带 Web UI、用户体系和 Webhook给团队成员分配只读账号很容易。本文后面的完整方案也会以 Gitea 为主线展开因为它的覆盖场景更广适合绝大多数中小团队。1.2 为什么强调“指定仓库清单”而不是“全量同步”GitHub 上的仓库数量级是千万级算上仓库体积和增长速率全量同步的地板就是几十 TB 起步每天增量也是 TB 级别这根本不是普通公司或个人的服务器能扛的。更重要的是绝大多数团队根本用不上那么多仓库。实际操作中我习惯先把仓库清单收进一个纯文本文件按行写仓库地址支持注释。比如# 核心构建依赖 https://github.com/某开源组织/某构建工具.git https://github.com/某开源组织/某解析库.git # 团队高频参考 https://github.com/某组织/某前端框架.git https://github.com/某开发者/某内网穿透组件.git这份清单就是整个镜像站的“配置中心”。新增一个同步对象就往文件里追加一行再跑一遍脚本就行。基于清单的另一个好处是权限边界非常清晰——镜像站只承载你明确声明要同步的仓库不需要为“会不会同步了不该同步的东西”而焦虑。1.3 几种主流方案选型对比在动手之前我把见过的方案和各自的适用场景列个表帮助你自己判断该走哪条路线方案核心组件优点缺点适合场景纯 Git 裸仓库 Nginxgit clone --mirror Nginx最轻量、无外部依赖无权限控制无 Web 界面个人缓存、内网完全可信环境Gitea / Gogs 同步仓库Gitea 同步脚本 Webhook有权限管理和 UI支持 Web 触发同步多一个 Java/Go 服务进程中小团队内部代码分发GitLab 镜像功能GitLab 自带仓库镜像Repository Mirror官方功能同步对用户透明资源占用偏高配置较重中大型公司已有 GitLab 体系对象存储 文件网关阿里云 OSS 自定义同步层容量弹性支持大规模分发架构复杂维护成本高大规模公共镜像服务个人推荐普通人直接走第二条路线Gitea。它单机部署内存占用很低实测 512 MB 内存的空闲机器就能跑得很流畅而且它对“外部仓库同步”这件事支持得比较自然——Gitea 支持从任意公开 Git 地址迁移仓库Mirror 模式。所谓 Mirror 模式就是你在 Gitea 里创建一个“镜像仓库”填上源仓库地址Gitea 会在后台按你设置的间隔自动拉取更新。这套机制天然适合做内部镜像站只是默认的同步间隔最小只能到几分钟一次而对多数场景已经完全够用了。2. 镜像同步的核心原理与关键机制2.1 理解git clone --mirror和普通 clone 的区别先补一个原理性的知识点否则后面脚本里的参数容易让人懵。常规的git clone拿到的是一份“工作副本”有.git目录也有 checkout 出来的工作区文件本地默认分支跟踪远端分支。而git clone --mirror拿到的是一份“裸镜像”没有工作区文件但.git里的一切——所有分支、标签、远端跟踪引用、Git 配置——都和源仓库保持一致。一个容易忽视的细节是--mirror会把远端的所有引用完整映射到本地。举例来说如果源仓库有三十个分支、两百个标签镜像仓库也会有完整的分支和标签。这也就意味着镜像仓库本身几乎就是源仓库在同步时刻的一份“完整快照”。为什么要用--mirror而不是普通 clone因为镜像站要承载团队成员的 clone 操作每个成员 clone 的都是一个完整仓库。如果本地只是一个普通工作副本分支不完整别人 clone 过去一开始就缺信息。所以镜像仓库本身必须是“全量引用”的。增量同步用git remote update --prune。这条命令会向远端仓库发起引用协商只拉取本地缺失的对象。第一次全量可能耗时较长后面基本就是秒级或毫秒级的小同步。定期跑这条命令就能维持镜像的新鲜度。我做测试时一个约 300 MB 的开源仓库在带宽稳定的情况下初次全量大概几分钟之后每五分钟一次的增量同步通常在几秒内完成。2.2 关键细节update-server-info与 HTTP 智能协议如果镜像仓库只打算用 SSH 协议服务update-server-info可以省掉。但只要你想通过 HTTP/HTTPS 让同事git clone http://mirror...这个命令就绕不开。Git 的 HTTP 智能协议依赖仓库内生成的info/refs文件这个文件是服务端向客户端提供引用列表的“索引”。仓库引用更新后如果info/refs没同步刷新客户端拉取时可能会看到过期分支甚至直接报错。所以完整脚本里每次仓库更新后必须执行git --git-dir$repo_dir update-server-info我早期就栽过这个坑同步脚本只跑了remote update没跑update-server-info结果用 Nginx 暴露的 HTTP 地址 clone 分支列表始终是旧的排错排了一个下午。2.3 清理与维护git gc和对象压缩镜像站跑久了仓库体积会缓慢膨胀。因为每次增量同步都会引入新的对象虽然 Git 的松散对象会周期性被自动压缩但自动触发不完全可控——特别是当你手动做强制更新、或者仓库历史里有大量大文件变更时。我建议在同步脚本里给每个仓库安排定期git gc。Gitea 或 GitLab 这类平台自带 GC 计划而纯静态方案必须自己加。实际执行时我习惯每月跑一次git gc --aggressive --prunenow。加--aggressive会做一次更彻底的对象压缩代价是耗时长、CPU 占用高。仓库大的时候最好在凌晨低峰跑避免影响正常拉取。注意不要在团队正集中拉代码的时段跑git gc --aggressive它会把仓库锁住一段时间。更稳妥的做法是只跑git gc --auto让 Git 自己判断是否需要压缩。3. 完整实操从零搭建一套可用的 Git 镜像站3.1 环境准备与基础依赖我以 Ubuntu 22.04 LTS 为例其他 Linux 发行版思路完全一致。需要准备的东西很少一台 Linux 服务器或虚拟机建议至少 2 核 4 GB 内存、50 GB 可用磁盘具体按仓库总量估算一般给仓库预估体积留 1.5 倍余量。一个域名或者直接走 IP内网使用的话用 IP 或内网 DNS 都行。服务器能正常访问外网能访问到 GitHub 官方地址。先把系统基础依赖装好sudo apt update sudo apt install -y git nginx curl wgetGit 版本建议 2.30 以上太老的版本对--mirror和update-server-info的支持不够完善。我实测 2.39 跑得很稳。3.2 安装并初始化 Gitea到 Gitea 官方发布页下载最新版二进制包这里不写具体版本号你操作时选最新的稳定版即可然后放到/usr/local/binsudo wget -O /usr/local/bin/gitea https://dl.gitea.com/gitea/version/gitea-version-linux-amd64 sudo chmod x /usr/local/bin/gitea创建运行用户和目录sudo adduser --system --group --disabled-password --home /var/lib/gitea gitea sudo mkdir -p /var/lib/gitea/{custom,data,log} sudo chown -R gitea:gitea /var/lib/gitea sudo chmod -R 750 /var/lib/gitea用 systemd 托管 Gitea 进程# /etc/systemd/system/gitea.service [Unit] DescriptionGitea (Git with a cup of tea) Afternetwork.target [Service] Usergitea Groupgitea WorkingDirectory/var/lib/gitea/ ExecStart/usr/local/bin/gitea web --config /var/lib/gitea/custom/conf/app.ini Restartalways EnvironmentUSERgitea HOME/var/lib/gitea GITEA_WORK_DIR/var/lib/gitea [Install] WantedBymulti-user.target启动并设为开机自启sudo systemctl daemon-reload sudo systemctl enable --now gitea首次访问http://服务器IP:3000会进入安装引导页。数据库选 SQLite 即可中小团队完全够用仓库根目录设为/var/lib/gitea/data/repositories。安装完用管理员账号登录后台稍后会用到。3.3 用 Gitea 创建“镜像仓库”Gitea 建镜像仓库的路径是右上角“” → “新建迁移” → 选择“Git”迁移方式。填上源仓库的 Git 地址镜像仓库名称和本地路径按自己习惯命名。关键点在“镜像同步”这一栏Gitea 支持两种同步方式——定期同步和 Webhook 触发同步。我个人强烈建议同时打开“同步间隔设置”设成每 15 分钟或每 30 分钟一次这样即使没有外部触发也能保证镜像不会长期滞后。Gitea 的后台同步机制会自动对镜像仓库执行git remote update不需要自己写 crontab。你可以在同一页面配置“当源仓库发生 Webhook 推送时立即同步”。这个后面单独讲因为第一次配置时很多人找不到入口容易忽略。3.4 用脚本批量同步多仓库给纯静态方案或混合方案用如果你手头仓库很多一个个去 Web 界面创建镜像仓库效率太低。我在混合方案里会直接用脚本先把仓库“种”下去再让 Gitea 接管后续同步。完整的同步脚本我放在下面这个脚本适合两类场景一是没有任何 Web 平台时直接靠它加上 Nginx 对外服务二是作为 Gitea 的“初始化工具”批量生成裸仓库后导入。#!/bin/bash # mirror_sync.sh - Git 镜像仓库批量同步脚本 # 用法: ./mirror_sync.sh set -euo pipefail # 配置区 REPO_LIST/opt/mirror/repos.txt # 仓库清单 MIRROR_ROOT/data/git-mirror # 镜像根目录裸仓库都放这里 LOG_FILE/var/log/mirror_sync.log GIT/usr/bin/git mkdir -p $MIRROR_ROOT log() { echo [$(date %Y-%m-%d %H:%M:%S)] $* | tee -a $LOG_FILE } sync_repo() { local repo_url$1 local repo_name repo_name$(basename $repo_url .git) local repo_dir$MIRROR_ROOT/$repo_name.git if [ -d $repo_dir ]; then log 更新仓库: $repo_name $GIT --git-dir$repo_dir remote update --prune || log 警告: $repo_name 增量更新失败尝试强制重建 $GIT --git-dir$repo_dir update-server-info || true $GIT --git-dir$repo_dir gc --auto || true else log 首次克隆: $repo_name $GIT clone --mirror $repo_url $repo_dir $GIT --git-dir$repo_dir update-server-info fi date %Y-%m-%d %H:%M:%S $repo_dir/.last_sync chown -R gitea:gitea $repo_dir } # 逐行读取仓库清单忽略注释和空行 grep -vE ^\s*(#|$) $REPO_LIST | while IFS read -r repo_url; do sync_repo $repo_url done log 本轮镜像同步完成脚本用set -euo pipefail保证出错时及时退出避免一个仓库的失败掩盖其他日志信息。repo_name用basename从 URL 里取出仓库名拼成本地路径。初次同步会走git clone --mirror后续全走增量更新。仓库清单repos.txt示例# 我的核心镜像清单 https://github.com/某团队/某微服务框架.git https://github.com/某团队/某路由库.git https://github.com/某开发者/某命令行工具.git3.5 定时任务与 Webhook 触发脚本方式依赖 crontab。打开 crontab 编辑器crontab -e加入一条每 30 分钟执行一次的规则*/30 * * * * /opt/mirror/mirror_sync.sh /var/log/mirror_sync_cron.log 21如果你希望“源仓库只要一更新镜像就立刻同步”就要用 Git 自带的 Webhook 机制。GitHub 上在仓库的 Settings → Webhooks 里添加一个“镜像站触发地址”Gitea 会暴露一个api/v1/repos/{owner}/{repo}/mirror-sync的 POST 接口。当 GitHub 源仓库发生 push 事件GitHub 会向这个地址发请求Gitea 收到后就立即拉起镜像同步。这个模式的优点是镜像新鲜度极高几乎实时缺点是如果镜像仓库很多或者源仓库 push 很频繁同步任务会排长队。我实际使用的经验是日常开发项目用 Webhook 模式低频维护的老项目统一用定时同步两套并存最合理。3.6 Nginx 反向代理与 HTTPS 配置Gitea 默认监听3000端口直接裸奔不太好。我用 Nginx 做一层反向代理把 80/443 端口转发给 Gitea顺便补上 HTTPS。Nginx 配置核心片段server { listen 80; server_name mirror.example.internal; client_max_body_size 1G; location / { proxy_pass http://127.0.0.1:3000; proxy_set_header Host $host; 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; } }client_max_body_size必须调大否则团队 push 大文件时会被 Nginx 以 413 拒绝。HTTPS 证书直接用内部 CA 或 Lets Encrypt 都行内网环境我一般自签证书并在团队成员机器上提前导入 CA 证书。4. 常见问题与排查技巧实录4.1 同步失败网络临时抽风怎么办镜像站对外提供的是服务依赖的是外网拉取。GitHub 偶尔会抖动git remote update偶发失败很正常。脚本里我已经加了失败前的日志提示但更好的方式是配置重试机制。给脚本加一层简单的重试比如失败后等 30 秒再试最多重试三次。我踩过最典型的坑是网络抖动导致同步失败后镜像停留在旧版本团队拉代码撞见一个“突然回退的提交”排查半天才发现是镜像没跟上而不是代码真的回退了。所以同步失败必须要在日志和告警里体现出来。4.2 仓库体积异常膨胀镜像仓库在git gc --auto没有跑的时候会慢慢变大。几个藏在暗处的原因源仓库历史里有大文件每次同步都会把历史对象完整拷贝。有些仓库会频繁产生大量 tag旧 tag 对象一直留着。Gitea 的定期同步默认不会主动清理远端已经删除的引用除非你在镜像仓库设置里勾选了“同步删除远程分支”。解决办法对体积特别大的仓库单独设置git gc --aggressive --prunenow月度任务同时确认 Gitea 镜像设置里“删除远程分支”选项是开启的。4.3 Gitea 同步间隔最小为什么是分钟级Gitea 的 Cron 任务那套机制里同步间隔最小是一分钟但对大多数场景已经足够。如果某个仓库需要秒级同步我建议直接用外部 Webhook 触发而不是把同步间隔调到一分钟然后频繁扫全量清单——那样极其浪费 I/O。4.4 HTTP clone 时报错repository not found这个报错 90% 的原因是 Nginx 找不到对应的 Git 智能 HTTP 端点。检查两点确认 Gitea 的app.ini中[server]段配置了正确的ROOT_URL要和 Nginx 的对外地址一致。确认仓库所有者、仓库名大小写是否匹配。Git 仓库名对大小写敏感Foo.git和foo.git是两个完全不同路径。4.5 SSH clone 权限问题如果团队通过 SSH 协议克隆需要在 Gitea 后台给成员账号上传 SSH 公钥。常见坑是团队成员本地的 SSH key 没加进 ssh-agent导致反复要求输入密码。另一个坑是 Gitea 默认 SSH 端口是 22如果和服务器系统 SSH 冲突需要改 Gitea 的 SSH 端口并在客户端侧写清楚ssh://githost:端口/owner/repo.git格式。4.6 磁盘空间告警双倍空间陷阱最容易忽略的是“同一份仓库占两份空间”的情况。Gitea 做同步时是先拉取到临时目录再移动到正式仓库目录如果磁盘余量刚好只够仓库体积的 1.1 倍同步过程很可能因为临时文件写满而失败。我建议给镜像站所在分区至少留出所有仓库总大小的 1.5 倍空间预留足够缓冲。5. 一些老手的经验之谈这套镜像站在我手里折腾了三个多月才稳定下来。第一个版本只用了纯脚本加 Nginx脚本逻辑没问题但团队里每个人都来问我“哪个仓库同步了没”这类问题实在烦不胜烦后来才补上 Gitea。第二个版本则踩了 Gitea 的 cron 同步间隔太死的坑最后用 Webhook 补齐了实时性。有几个习惯我认为是镜像站长期稳定运行的分水岭仓库清单必须用版本管理保存每次增删仓库都是一次小发布保留审计记录。同步日志要放到独立文件并做轮转不然跑半年日志就有几个 GB排查问题反而难找关键点。监控不只是看“同步成没成功”更要关注“同步后仓库体积变化是否异常”——很多诡异问题都是从体积异常膨胀开始的。如果你只是给个人电脑做一套本地缓存那纯脚本方案十行命令就能搞定但如果目标是服务一个团队Gitea 加 Webhook 加 Nginx 这套组合值得从头搭一遍。镜像站本质上是一个“耐心工程”基础设施的价值不在搭建的那一下而在之后一年里它稳定、静默、零故障地不断把最新代码送到你手里。