尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

从零搭建GitHub镜像站:Gitea同步原理与实战指南

从零搭建GitHub镜像站:Gitea同步原理与实战指南 GitHub镜像站这四个字在代码托管和开源协作圈子里一直是个高频需求。所谓镜像就是把你关心的GitHub仓库复制到自己的服务器上保存一份内容一致的副本并提供Web查看和克隆的入口。这件事能解决的问题很具体团队协作时不希望所有人都直接依赖外部网络拉代码开源项目维护者需要给用户提供区域化下载入口个人开发者想给GitHub仓库做异地备份防止误删或账号异常。无论你是哪种角色一套可复现的镜像站搭建流程都能省掉后面大量手工操作。这篇内容会给你一条完整路径先梳理镜像站到底要镜像什么再讲清楚Git镜像的核心机制然后给一套基于Gitea的实操步骤最后是我实际踩坑后的排查清单。纯脚本方案、自托管平台方案、静态资产镜像方案都会涉及你可以根据自己的团队规模和需求选一种直接抄。1. 动手前先把需求想清楚镜像站到底在镜像什么1.1 三种常见形态仓库镜像、资产镜像、网页镜像很多人一想到镜像站第一反应就是把GitHub仓库复制一份。但实际落地时镜像通常包含三层内容缺一层都不完整。第一层是Git仓库本身也就是.git目录里的全部对象和引用。这一层承载了所有代码提交历史、分支、标签是镜像的核心。用户从你的镜像站克隆项目时能拿到和GitHub上一致的完整历史。第二层是Release资产。很多开源项目会在GitHub的Releases页面发布二进制安装包、压缩包、签名文件。代码仓库镜像同步了这些附件不会跟着过来。如果你的镜像站是给用户做软件分发的必须单独写脚本拉取这些资产。第三层是项目的文档站点或静态页面。GitHub Pages托管的文档、README中引用的图片等都属于外部资源。这层要不要镜像取决于你是否需要离线访问完整的项目信息。理解了这三层你才能判断自己需要的镜像站到底在镜像什么。只做代码备份的人仓库镜像就够了做内部分发或公共镜像的人Release资产和文档站点很可能是刚需。1.2 选型决策Gitea、GitLab还是纯脚本同步确定了镜像内容后往下走就是选实现方案。我见过不少团队在这步来回折腾其实就三条路各有利弊。方案一是纯脚本同步也就是在服务器上执行git clone --mirror再配合cron定期更新。优点是极其轻量、没有额外服务依赖一台1核2G的机器都能跑缺点是只能提供git clone/push没有Web界面团队成员看个仓库文件列表都得用命令行。方案二是自托管Git服务我用得最多的是GiteaGitLab也可以。这类平台本身提供镜像仓库功能你把上游GitHub仓库地址填进去平台会按设定周期自动同步。优点是开箱即用有Web界面、权限模型、Webhook员工可以用浏览器浏览代码也能通过SSH或HTTPS正常克隆缺点是相比脚本方案多一个需要维护的服务。方案三是云平台的重定向或反代缓存方案本质不是镜像只是把请求转发到GitHub我不建议作为长期方案。真正镜像站的词义里数据是要落到你自己服务器上的。选型有一个很实用的判断方式如果你需要镜像的仓库少于10个且使用者都是命令行玩家脚本方案足够如果使用者在10人以上或者希望非技术同事也能浏览代码直接上Gitea。1.3 同步频率与存储预算怎么定镜像站不像缓存CDN那样需要一个全局准入协议它的同步频率完全由你定义。我见到最常见的三种档位每6小时同步一次覆盖绝大多数活跃项目适合团队日常使用每天同步一次适合低频更新的项目每次上游有push事件就同步这个Gitea的Webhook支持GitHub的webhook可以推到你的镜像服务器。存储预算经常被人忽略。一个GitHub仓库的.git裸仓库体积通常是仓库文件的1.5到2倍如果有LFS对象会更大。我建议至少预留仓库体积乘以2的空间再乘以仓库数量然后把这个结果再乘1.5作为缓冲。原因后面在GC部分会解释。这里有个重要的提醒同步不是拷贝工作区文件而是同步Git对象数据库。镜像站会保留上游仓库的全部历史所以从克隆那一刻起你的存储占用基本就和上游仓库的.git体积持平这是镜像站的天然属性不要再幻想只拷最新代码能省多少空间了。2. 镜像的核心机制Git裸仓库与引用同步2.1 裸仓库是什么和普通工作区的区别如果你用git clone拉过项目看到的是一份工作区文件加隐藏的.git目录。这个.git里保存了所有提交对象、树对象、blob对象和引用。裸仓库bare repository则只有这部分没有检出的工作区文件。镜像站为什么要用裸仓库因为你不需要看到文件树你只需要一份完整的Git数据库让任何人在任何时间克隆时都能得到相同的历史。你可以理解成普通仓库是半成品展示间裸仓库是生产线上的原料库。镜像的本质是原料库的复制而不是展示间的复制。创建裸仓库最简单的方式是git clone --bare https://github.com/example/example.git也可以先手动初始化再添加远端git init --bare example.git cd example.git git remote add origin https://github.com/example/example.git git fetch --all新手容易在这步犯一个错把裸仓库当成普通工作区直接往里git add文件。裸仓库没有工作区的概念它也没有索引和待提交状态正常用户不应该直接在里面改文件。镜像站需要的就是这个原始状态改文件反而会污染镜像。2.2 git remote update 与 refs 机制完成了首次克隆镜像同步就很简洁了。对裸仓库执行git remote update --pruneGit会与上游远端通信拉取新的对象并更新本地的分支和标签引用。--prune参数很关键。它表示删除本地已经不存在于远端的引用。举个例子上游仓库删掉了一个dev/test分支如果你不加--prune这个分支引用会永远留在你的镜像仓库里用户从你这里看到的分支会比上游多这是镜像失真的常见原因之一。引用refs是Git把40位commit哈希映射为可读名称的机制包括refs/heads/*分支、refs/tags/*标签、refs/remotes/*远程跟踪。镜像站同步的本质就是保证这些refs和上游完全一致。真正产线上我会写一个很薄的同步脚本#!/usr/bin/env bash set -euo pipefail MIRROR_DIR/data/mirrors/example.git UPSTREAM_URLhttps://github.com/example/example.git cd $MIRROR_DIR git remote set-url origin $UPSTREAM_URL git remote update --prune git gc --auto --prunenowgit gc --auto --prunenow可以顺手清理掉悬空对象避免存储膨胀。关于GC后文会展开。2.3 LFS 对象和 Release 资产也要纳入镜像范围GitHub的大文件存储LFS是一个容易漏的点。仓库的.git只保存LFS指针文件真正的大文件在GitHub的LFS存储服务里。做镜像时如果你只同步了仓库用户克隆看到的是指针拉取LFS内容时仍然会去访问GitHub。这显然不符合镜像的初衷。处理方式是同步完成后在裸仓库里执行git lfs fetch --all这个命令会把远端所有的LFS对象都拉到本地。注意你的存储预算会因此显著膨胀一个50MB的模型文件历史版本多LFS对象可能几十GB。运营镜像站之前一定要先看一下上游仓库有没有使用LFS使用规模多大否则镜像做到一半磁盘就满了。Release资产同理。你可以在同步脚本里加上一段调用GitHub的API获取最新release的资产列表并下载curl -sL https://api.github.com/repos/example/example/releases/latest | jq -r .assets[].browser_download_url | while read -r url; do curl -sL $url -o /data/releases/$(basename $url) done这个写法很粗糙生产环境建议加上版本对比、文件校验、断点下载。我后面实操章节会给出一个更完整的版本。2.4 仓库完整性的验证方法镜像同步完了怎么知道它和上游一致相信它应该一致是不够的。Git自身有校验机制你可以用git fsck验证仓库完整性git fsck --fullfsck会遍历对象数据库检查SHA-1哈希一致性、对象依赖关系。如果输出没有error级别的内容说明仓库对象是完整的。这相当于对镜像做了一遍体检。想进一步验证分支引用是否和上游一致可以对比一下refsgit ls-remote origin /tmp/upstream.refs git show-ref /tmp/local.refs diff /tmp/upstream.refs /tmp/local.refs两边的refs一致的话镜像站在引用层面就是完全同步的。我通常会在每个同步周期后跑一遍这个检查输出结果写到日志文件里一旦diff有内容说明同步过程出问题了。这些方法不挑平台脚本方案和Gitea底层逻辑都适用只是Gitea把底层逻辑封装了。3. 实操记录用 Gitea 搭一个可用的 GitHub 镜像站3.1 环境准备服务器、Docker、域名我在这部分给出一套可以直接落地的方案。服务器配置一个中等活跃度的开源项目镜像2核4G就够用如果仓库数量超过50个或者有多个大仓库带LFS建议4核8G带宽按并发克隆人数估算。系统我以Ubuntu 22.04 LTS为例。第一步安装Dockersudo apt update sudo apt install -y docker.io docker-compose-v2 sudo systemctl enable --now docker sudo usermod -aG docker $USER退出SSH再登录用户所属组生效。域名建议用二级域名比如git.example.com。如果你暂时没有域名也可以用IP访问但后面配HTTPS时证书就是个大麻烦正规部署不建议跳过域名这步。目录规划上统一把持久化数据放在/opt/gitea下面sudo mkdir -p /opt/gitea/{data,logs}3.2 安装 GiteaDocker Compose 方式在/opt/gitea下创建docker-compose.ymlservices: gitea: image: gitea/gitea:latest container_name: gitea restart: always environment: - USER_UID1000 - USER_GID1000 - GITEA__database__DB_TYPEsqlite3 - GITEA__server__DOMAINgit.example.com - GITEA__server__SSH_DOMAINgit.example.com - GITEA__server__ROOT_URLhttps://git.example.com/ volumes: - /opt/gitea/data:/data - /etc/timezone:/etc/timezone:ro - /etc/localtime:/etc/localtime:ro ports: - 127.0.0.1:3000:3000 - 127.0.0.1:22:22端口我这里故意绑定到了127.0.0.1原因很直接Gitea本身不对外直接暴露统一由后面的Nginx接入这样SSH、HTTP的入口各自干净也不容易被外部扫描到未配置防护的服务端口。启动cd /opt/gitea docker compose up -d首次打开http://服务器IP:3000进入安装向导。这里要留意几个配置项安装向导里的应用名称可以写成内网代码镜像库站点根URL必须填你最终对外域名比如https://git.example.com/否则后面页面里生成的克隆地址全是错的。3.3 新建镜像仓库迁移向导里的关键选项装完Gitea后最核心的一步是把GitHub仓库镜像进来。用管理员账号登录点击右上角选择新建迁移仓库。这一步的术语在Gitea里叫迁移Migration下拉框会列出Gitea、GitHub、GitLab等来源。选GitHub后填写目标仓库地址再填一个访问令牌可选。如果仓库本身是公开的不填令牌也能同步填了令牌可以绕过GitHub的API速率限制仓库数量多时建议带一个只读令牌。有几个选项我要特别说明镜像仓库必须勾选。勾选后Gitea才会按计划自动拉取更新不勾选它只是一次性迁移。此仓库将是镜像仓库字样出现后同页面会有同步间隔字段我一般设6小时。如果你希望更实时还可以在GitHub仓库设置页面配置Webhook把push事件推到Gitea的Webhook地址实现push触发同步。私有选项看你的需求。如果镜像站对外开放公开即可如果只给团队用建议一律设为私有。创建完成后仓库页面顶部会显示从 https://github.com/example/example 镜像的标识右上角有一个立即同步按钮。创建后第一次同步会立刻触发后面按间隔调度。Gitea的镜像功能底层其实就是git remote update但它封装了调度、状态展示和权限管理。这意味着事件触发的同步和人肉点按钮的同步底层都不需要你碰命令行。只是要注意立即同步按钮只同步仓库引用LFS对象默认不走想让LFS也镜像需要额外配置LFS支持并定期在容器数据目录里执行git lfs fetch --all。事实上Gitea的镜像仓库目前对LFS的同步支持是通过它自己的LFS服务转发真正的上游LFS数据全量落地要自己想办法这是一个比较隐蔽的坑我放在第4章细说。3.4 定时同步不是只有 CronGitea 的调度原理很多教程让你额外写cron定时跑同步脚本但在Gitea里其实不需要。Gitea内置了一个定时任务系统由配置文件的[mirror]段控制[mirror] DEFAULT_INTERVAL 6h如果你在安装向导里没有配置默认值一般是8小时。想改的话编辑/opt/gitea/data/gitea/conf/app.ini在[mirror]下面加两行然后重启容器docker compose restart gitea这个内置调度器的优势是它直接感知仓库状态每个镜像仓库独立记录上次同步时间不像cron那样需要你为每个仓库都写一遍同步命令。维护成本低很多。我仍然建议保留一个兜底的外部cron作用是检查Gitea的健康状态而不是去拉代码。比如0 */2 * * * curl -fsS http://127.0.0.1:3000/api/v1/healthz /dev/null || docker compose -f /opt/gitea/docker-compose.yml restart gitea这个cron做的事情是API健康检查失败就自动重启容器。这个兜底逻辑在镜像站无人值守时非常有用。3.5 Nginx 反代与 HTTPSGitea作为应用层跑在本机3000端口Nginx负责对外。安装Nginxsudo apt install -y nginx创建配置文件/etc/nginx/sites-available/git.example.comserver { listen 80; server_name git.example.com; 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; proxy_read_timeout 300s; } }client_max_body_size上到1G是有原因的Git大仓库的HTTP push协议会把多个对象打包传上来体积轻松超过默认的1M。如果你忽略这一项用户从你的镜像站clone时会有概率在传输中途收到413错误。然后启用站点并申请证书sudo ln -s /etc/nginx/sites-available/git.example.com /etc/nginx/sites-enabled/ sudo nginx -t sudo systemctl reload nginx sudo apt install -y certbot python3-certbot-nginx sudo certbot --nginx -d git.example.comcertbot自动帮你改Nginx配置并加上证书续期任务。证书下来后访问https://git.example.comGitea页面正常显示克隆地址也会因为ROOT_URL配置而自动切换为HTTPS。到这里一个基础的Gitea GitHub镜像站已经能用了。用户可以通过浏览器浏览代码也可以通过git clone https://git.example.com/you/example.git拉取镜像仓库。跟直接从GitHub克隆相比数据路径更可控团队协作时的带宽和速度也会稳定不少。3.6 扩展Release 资产和文档站的镜像脚本前面反复提到Release资产这里给一个接近生产可用的脚本。它做的事情是从GitHub拉取下在所有release的资产文件对比本地版本只下载新增或变化的部分并生成SHA256校验文件。#!/usr/bin/env bash set -euo pipefail REPOexample/example DEST/data/releases GITHUB_APIhttps://api.github.com/repos/${REPO}/releases mkdir -p $DEST function fetch_assets() { local tag$1 local tag_dir$DEST/$tag mkdir -p $tag_dir curl -sL ${GITHUB_API}/tags/${tag} | jq -r .assets[]?.browser_download_url | while read -r url; do [ -z $url ] continue file$tag_dir/$(basename $url) if [ -f $file ] [ -f $file.sha256 ] echo $(cat $file.sha256) $file | sha256sum -c /dev/null 21; then echo skip $file continue fi echo download $url curl -sL $url -o $file sha256sum $file $file.sha256 done } for tag in $(curl -sL ${GITHUB_API} | jq -r .[]?.tag_name | head -20); do fetch_assets $tag done脚本里加了SHA256校验文件好处有两个一是后续脚本再次运行时能快速判断文件是否完整不完整自动重下二是给消费端提供校验条件用户下载后可以验证文件完整性。文档站镜像就更简单了很多项目用GitHub Pages本质上就是一组静态文件可以用wget递归拉取wget --mirror \ --convert-links \ --adjust-extension \ --page-requisites \ -e robotsoff \ -P /data/sites/example-docs \ https://example.github.io/example/注意--adjust-extension选项它会把没有扩展名的页面存成.html避免本地打开时MIME类型识别错误。文档站镜像频率不用太高一天一次足够。4. 常见问题排查与避坑实录4.1 镜像同步失败的几个高频原因我在实际维护镜像站的过程中遇到过不少看起来没毛病但就是不更新的情况。这里列几个常见原因可以直接对照排查。第一是GitHub API限流。Gitea或脚本往GitHub拉数据如果仓库多或频率高匿名请求会撞上60次/小时的上限。同步失败的信息通常类似403 rate limit exceeded。解决办法是在Gitea的迁移仓库配置里填一个GitHub的访问令牌或者让脚本在请求时带上Authorization头。第二是上游仓库迁移或删除导致地址404。镜像站不会自动判断仓库没了它只会反复同步失败。我会定期review镜像列表把长期失败的仓库标记出来手动确认上游状态后再决定是修复地址还是下线该镜像。第三是同步超时。跨区域访问GitHub时网络链路波动会造成连接中断。默认的同步超时对大型仓库来说不够宽裕我建议在Gitea配置里把[mirror]的SYNC_TIMEOUT调大一些脚本方案里则可以在git命令前设置GIT_HTTP_LOW_SPEED_LIMIT和GIT_HTTP_LOW_SPEED_TIME这类环境变量让Git对低速连接更宽容。4.2 仓库巨大、同步超时怎么办有些项目仓库动辄几GB初次同步可能要跑几个小时。第一次建镜像时经常有人以为卡死了其实是在慢慢拉对象。解决方案有几个层面。第一个层面是断点续传。Git本身就支持增量拉取你不需要重新clone只需要让clone完成。中途中断就重新执行一次git fetch --all已经下载的对象会跳过。第二个层面是切换协议。HTTPS在大文件场景下通常不太稳定如果服务器能通过SSH端口访问GitHub用SSH协议同步往往更稳。同步地址从https://github.com/example/example.git改成gitgithub.com:example/example.git然后在服务器上把GitHub的部署公钥配好。第三个层面是拆分。如果上游仓库实在太大一部分是历史里的大二进制文件可以在镜像站里选择浅克隆git clone --mirror --depth 1 https://github.com/example/example.git浅镜像只保留最近一次提交体积大幅度缩小。代价是用户克隆后看不到完整历史因此只能作为内部快速分发的折中方案不要对外宣称是完整镜像。完整历史还是得靠全量同步慢慢拉。4.3 存储膨胀与GC策略镜像站跑久了存储一定会涨。原因有几个上游历史新增的对象、LFS对象、Gitea或脚本同步过程中的冗余对象。Git的GC机制是增量式的git gc --auto会在对象数量超过阈值时自动运行。如果你想主动压缩可以执行git gc --aggressive --prunenow--aggressive会重新打包所有对象体积能明显下降但很耗CPU只建议在仓库体积异常膨胀时跑。生产环境更推荐定期执行普通gcgit gc --auto还需要告诉Git哪些对象是不能清理的。镜像站要想完整保存仓库所有refs都要保持。我见过有同学为了省空间删了老标签结果用户克隆时缺tag浪费了大量排查时间。记住修剪镜像站对象之前先想清楚你对可见历史的定义不要为了省几GB空间制造数据丢失事故。LFS对象的清理更敏感。日常开发场景里git lfs prune会删除本地的临时LFS对象但镜像站恰恰不适用这个命令因为镜像站需要保留全量LFS对象。准确的说法是镜像站只需要拉取全部LFS对象正常情况下不会产生需要prune的临时对象。如果你在镜像场景用了git lfs prune那很有可能会把不该删的对象删掉。除非你明确知道自己在做什么否则不要在镜像仓库目录里碰它。4.4 权限模型镜像站要不要开放注册这个问题运营者迟早会遇到。镜像站如果对外开放注册任何人都能注册账号并在上面创建仓库磁盘会被恶意塞满还会带来代码托管责任风险。我已经不止一次看到有人搭完镜像站后没关注册过一个月一看服务器被当成了免费网盘。我的建议是公开只读写入需申请。Gitea的管理面板里可以关闭自助注册团队成员的账号由管理员创建或通过限定邮箱后缀注册。公开用户不需要登录也能浏览和克隆仓库这样既能对外提供镜像服务又避免了资源滥用的风险。具体配置在管理后台站点管理-配置管理里把禁用自助注册勾上。如果镜像站对外开放我还会启用仅允许本实例用户查看组织这类收紧的可见性配置从入口减少被爬虫扫到并滥用的情况。4.5 如何验证镜像和上游保持一致最后一个经常被忽略的问题是同步完成后到底该信谁代码托管平台不会主动通知你镜像已滞后只能自己去验证。验证分三个层次。第一层看Gitea仓库页面的最后同步时间这个时间如果一直在更新至少说明同步任务在执行。第二层用git ls-remote对比上游和镜像的refs我在第2章给过对比脚本。第三层直接clone下来做文件级对比在镜像仓库clone后对当前HEAD跑git diff --stat看有没有差异。最直接的做法是写一个巡检脚本定期对每个仓库做第二层对比发现差异就告警。自动化告警可以用Gitea的Webhook直接推到团队群。这块做完你的镜像站才算真正处于被监控状态而不是看起来在跑、实际上早已失真。5. 一点后续体会和镜像站的更多玩法整个项目做下来我个人最大的体会是镜像站表面上是复制仓库的问题实际上是仓储运营的问题。你真正要管理的是引用同步、对象存储、权限边界和持续验证GitHub只是你的一个权威数据源。理解了这一点后面扩展什么都很顺手。举例说你完全可以在这套架构上叠加一个每日归档任务把镜像站的裸仓库打包成bundle文件推到对象存储或者另一台异地服务器实现仓库级灾备。Git本身支持git bundle create /backup/example.gitbundle --all这个bundle文件就是一个完整的可克隆仓库可以随时恢复。我用它给团队做每周离线归档实用度很高。最后再分享一个小经验镜像站域名和资料名称尽量带上明显的镜像标识比如仓库描述里写上此仓库为上游项目的镜像副本页面顶部也有Gitea自带的镜像标记。这样不仅对用户负责也能避免有人误以为你的镜像站是官方仓库出了争议反而麻烦。镜像这种玩法干净清晰的标识比功能堆叠更能守住底线。
返回列表