GitLab的备份、恢复和迁移,说简单很简单,无非一条命令打个tar包,恢复时restore一下;说麻烦也真的麻烦。我见过太多团队数据没丢时信誓旦旦,真到服务器故障或者换机器那天,才发现备份文件是残的、版本不匹配、密钥丢了、恢复完首页能开但项目全是404。GitLab这东西,日常用着像个黑盒子,代码一推一拉没什么感觉,可它的内部结构比普通应用复杂得多——PostgreSQL数据库、裸仓库、LFS对象、容器镜像、CI/CD流水线记录,还有一堆上传附件全揉在一起,任何一个环节断掉,恢复出来的都是一具残缺的骨架。
这篇文章不打算讲那种“照着官方文档敲三条命令”的流水账,而是把我自己从备份规划、定时任务、恢复演练到跨机器迁移整条链路踩过的坑和验证过的方法摊开说。无论你是刚用Omnibus包装好GitLab的小团队,还是已经在生产环境跑了两三年的运维,只要涉及“别让代码数据死掉”这件事,这篇都能给你一些直接能用的东西。
1. 先把GitLab的数据构成盘清楚
1.1 除了仓库,还有哪些东西隐藏在备份包里
很多人以为GitLab备份就是“把代码仓库打个包”,这是最大的误解。GitLab的完整数据至少包括这么几类:
- PostgreSQL数据库:存项目元数据、Issue、Merge Request、用户、权限、Webhook、CI/CD变量,几乎一切“非仓库内容”都在这套数据库里。
- Git仓库数据:默认在
/var/opt/gitlab/git-data/repositories,按命名空间和项目名组织。 - 上传的文件:头像、附件、设计稿、导入的归档文件等,默认在
/var/opt/gitlab/uploads。 - LFS对象:大文件存储,路径在
/var/opt/gitlab/gitlab-lfs。 - Container Registry镜像:如果开了GitLab自带的镜像仓库组件,这个也占大量空间。
- CI/CD流水线记录:构建日志、Artifacts,在
/var/opt/gitlab/gitlab-rails/shared/artifacts。 - 配置文件:
/etc/gitlab/gitlab.rb和/etc/gitlab/gitlab-secrets.json,这两个文件极其关键,但不在备份tar包里面。
官方那条gitlab-backup create命令,默认会把数据库、仓库、上传文件、LFS、容器镜像等打包进一个带时间戳的tar包,路径通常在/var/opt/gitlab/backups。这么说吧,它打包的是“运行数据”,但配置类和密钥类文件是单独躺在/etc/gitlab下面的,恢复时如果不同时还原这两个文件,就会出现“数据回来了,但服务起不来”或者“Runner失联、2FA失效”这类诡异问题。
1.2 备份迁移前必须想清楚的三个原则
在做任何备份、恢复、迁移动作之前,我建议先在脑子里过一遍这三个原则,它们是后面所有操作的底层逻辑。
第一,版本一致性原则。GitLab备份包和恢复目标实例的主流大版本必须一致,至少主版本号要匹配。比如你用14.10备份出来的包,非要恢复到16.x,官方文档说只能沿大版本逐步升级恢复,直接跨版本恢复基本就是自找麻烦。迁移时最稳妥的办法是先让新旧两台机器跑完全相同的版本,再动备份包。
第二,先备份再操作原则。升级、迁移、恢复实验,任何动作之前先做一次全新备份,并且确认备份文件完整、可读。遇到过太多人拿着三天前的备份想恢复,结果发现那天的备份因为磁盘满了只打了半包。
第三,恢复验证原则。备份的最终目的是“恢复出来能用”,不是“生成了一个文件”。所以每次备份完之后,至少要在测试环境做一次恢复演练,确认项目能打开、代码能clone、Issue页面能渲染、CI能正常触发。这个原则看上去多此一举,但真能在关键时刻救你一回。
2. GitLab备份实操:那条命令背后的完整机制
2.1 备份命令的版本差异与标准用法
GitLab的备份命令在不同版本里有个名字变化。老版本用的是gitlab-rake gitlab:backup:create,新版本(大概从12.x之后)推荐用gitlab-backup create,本质是同一个Rake任务的封装。我自己惯用的完整命令是:
sudo gitlab-backup create STRATEGY=copy GZIP_RSYNCABLE=yes这里解释一下两个参数为什么值得加:
STRATEGY=copy:GitLab备份时默认会直接读取当前数据目录,生产环境仓库正在被访问时,直接打tar容易出现文件状态不一致的情况。这个参数会让GitLab先把数据复制到临时目录,再基于副本打包,虽然会多占一份磁盘空间,但保的是数据一致性,我认为这笔投入值得。GZIP_RSYNCABLE=yes:让生成的tar包内部用rsyncable方式压缩,这样未来用rsync做增量同步、断点续传时能省很多事。
执行完后,在/var/opt/gitlab/backups下会生成类似1700000000_2023_11_04_16.10.5_gitlab_backup.tar的文件。前面那串数字是时间戳,中间的版本号是备份时GitLab的版本。这个版本号特别重要,恢复时一定要对着它。
2.2 配置文件单独备份,这件事决定你能不能恢复
gitlab-backup create不会备份/etc/gitlab/gitlab.rb和/etc/gitlab/gitlab-secrets.json,这是个很容易被忽略但后果严重的点。
gitlab.rb是GitLab的一切配置,external_url、数据库连接、邮件服务、LDAP、备份路径全在里面。gitlab-secrets.json则存着各种加密密钥——数据库加密密钥、OTP密钥、CI/CD Runner的共享密钥、第三方集成的密钥。恢复备份时如果这个文件和新环境不匹配,会出现这些现象:
- CI/CD Runner连接失败,提示
403或者Failed to authorize。 - 启用了2FA的用户全部无法验证。
- 导入导出项目时提示加密密钥不一致。
- “记住我”的Session失效。
所以我的习惯是每次备份完数据的同时,立刻把这两个文件单独拷贝走:
sudo cp /etc/gitlab/gitlab.rb /var/opt/gitlab/backups/gitlab.rb.$(date +%F) sudo cp /etc/gitlab/gitlab-secrets.json /var/opt/gitlab/backups/gitlab-secrets.json.$(date +%F)如果备份目录在本地磁盘,我还会用scp或rsync把它们同步到另一台机器,因为这两份文件体积很小,与其躺在本地等磁盘一起坏掉,不如放异地。
2.3 定时备份脚本与保留策略
手工备份只能算“有备份”,真正扛事的是自动化。我这边用了最简单的cron方案,核心思路是:数据备份每天凌晨执行,配置文件同步执行,备份文件保留7天,超过的自动清理。
脚本大致长这样:
#!/bin/bash BACKUP_DIR=/var/opt/gitlab/backups KEEP_DAYS=7 # 记录日志 echo "===== GitLab Backup Start: $(date) =====" >> /var/log/gitlab-backup.log # 数据备份 /usr/bin/gitlab-backup create STRATEGY=copy GZIP_RSYNCABLE=yes >> /var/log/gitlab-backup.log 2>&1 # 配置文件备份 sudo cp /etc/gitlab/gitlab.rb $BACKUP_DIR/gitlab.rb.$(date +%Y%m%d) sudo cp /etc/gitlab/gitlab-secrets.json $BACKUP_DIR/gitlab-secrets.json.$(date +%Y%m%d) # 清理超过7天的备份 find $BACKUP_DIR -name "*_gitlab_backup.tar" -mtime +$KEEP_DAYS -delete find $BACKUP_DIR -name "gitlab.rb.*" -mtime +$KEEP_DAYS -delete find $BACKUP_DIR -name "gitlab-secrets.json.*" -mtime +$KEEP_DAYS -delete echo "===== GitLab Backup End: $(date) =====" >> /var/log/gitlab-backup.logcrontab配置:
0 2 * * * /usr/local/bin/gitlab_backup.sh选凌晨两点是因为这个时段团队基本不提交代码,仓库读写和数据库压力都小,备份过程中对线上服务的影响降到最低。之前我贪省事放在中午跑,结果刚好赶上开发集中合并代码,备份过程中仓库一直在变,最后打出来的包虽然能恢复,但恢复完总有那么几个分支的状态跟最新代码不一致,这才把时间改到凌晨。
保留7天这件事也要说一句。GitLab官方推荐保留时间和你的实际风险承受能力挂钩。比如你每天备份一次,保留7天,最坏情况下只能恢复到7天前的数据,那一个周的提交和Issue记录全部丢失。如果团队对数据丢失容忍度低,建议保留到14天甚至30天,代价只是磁盘空间。备份文件一般压缩后占原始数据的30%到60%左右,按仓库大小先估算一下,再决定保留周期。
2.4 备份的“物资”检查:文件大小与完整性初判
备份完成后,不要看一眼文件存在就走了。我在脚本后面加了一段自动检查,判断新生成的备份tar包大小是否小于某个阈值,比如小于500M就报警。因为如果仓库很多但备份包只有几M,说明大概率漏了什么。
更靠谱的完整性验证是直接tar -tf:
tar -tf /var/opt/gitlab/backups/1700000000_2023_11_04_16.10.5_gitlab_backup.tar | head -50能正常列出包内文件列表,至少说明压缩包没损坏。但真正保险的验证还是我说的“恢复演练”。
3. 从备份恢复:严格按顺序执行
3.1 恢复前的版本匹配与停机准备
恢复备份这件事最大的一个前置条件:目标机器的GitLab主版本必须和备份包版本号一致。我恢复前通常先看版本:
cat /opt/gitlab/version然后看备份包文件名后的版本号,两者一致才动手。不一致的话,要么升级目标机器,要么找一份匹配版本的备份包。
版本确认后,恢复操作需要在维护窗口执行,意味着要停掉GitLab的关键服务。官方推荐停的是Puma(应用服务器)和Sidekiq(后台任务队列):
sudo gitlab-ctl stop puma sudo gitlab-ctl stop sidekiq这两个一停,GitLab基本就不对外提供正常写入了,避免恢复过程中再有新数据写进来污染备份状态。但注意,如果机器上还开了其他组件,比如Gitaly或者PostgreSQL,它们可以暂时继续跑,恢复工具会自动处理。
3.2 恢复命令与非交互模式的坑
恢复命令本身不长:
sudo gitlab-backup restore BACKUP=1700000000_2023_11_04_16.10.5BACKUP=参数填的是备份tar包文件名中间的时间戳和版本号部分,不带_gitlab_backup.tar后缀。这里有个坑:命令执行到一半会问你是否确认要恢复,因为它会覆盖当前数据库和仓库目录,一确认就不可逆。脚本化或无人值守时,要加上FORCE=yes:
sudo gitlab-backup restore BACKUP=1700000000_2023_11_04_16.10.5 FORCE=yes恢复完成后,第一时间还原配置文件。提前把gitlab.rb和gitlab-secrets.json拷贝回/etc/gitlab/,然后执行:
sudo gitlab-ctl reconfigure sudo gitlab-ctl restart如果不做这一步,可能出现服务起不来或者Runner密钥对不上的问题。我自己就吃过一次亏:当时只恢复了数据tar包,忘了secrets文件,界面能登录,但CI Runner全部失联,最后排查了一下午才发现是密钥文件不匹配。
3.3 恢复之后怎么验证才算真的成功
恢复完成后不要急着对外宣布“已恢复”。我有一套固定的验收清单,挨个过一遍:
- GitLab首页能打开,登录没报错。
- 随机挑几个项目,看README、Issue、Merge Request页面是否正常。
- 挑一个大仓库执行
git clone,确认仓库内容完整。 - 检查Webhook列表,确认URL没有丢失。
- 去Admin区域看Runner是否在线。
- 看后台日志:
sudo gitlab-ctl status sudo gitlab-rake gitlab:check RAILS_ENV=productiongitlab:check这个命令会做一整套自检,包括数据库连接、Redis状态、Sidekiq进程、仓库目录权限等。虽然它不一定能检测出所有业务数据问题,但至少能排除环境层面的故障。跑完没有红色错误,我才会把维护窗口关掉。
4. GitLab迁移方案:同版本迁移、跨版本迁移与容器化迁移
4.1 同版本迁移:最稳的方案是“备份-恢复-校验”
同版本迁移场景很常见:旧机器磁盘快满了,或者公司要换服务器,但GitLab版本不变。这种迁移痛苦最小,因为不需要考虑版本兼容问题。
操作链路清晰明确:
- 旧机器执行一次完整备份,同步备份配置文件。
- 新机器安装同一版本的GitLab Omnibus包。
- 把备份tar包和两个配置文件的副本拷贝到新机器。
- 新机器停掉Puma和Sidekiq,把配置文件放回
/etc/gitlab/。 - 执行
sudo gitlab-backup restore BACKUP=xxx FORCE=yes。 - 执行
sudo gitlab-ctl reconfigure和sudo gitlab-ctl restart。 - 修改DNS解析,或者把旧机器的流量切到新机器。
- 按3.3的验收清单逐项验证。
这条链路最关键的一步其实在步骤7。我建议先切一半流量或者先让部分开发手动访问新地址测试,确认无问题后再改正式DNS的TTL,等确认一切稳定后,再下线旧机器。
4.2 跨版本迁移:内核是“阶梯式升级再迁移”
如果说同版本迁移是换车不换发动机,那跨版本迁移就是连发动机带底盘一起换,最容易翻车。
GitLab官方规定,备份包只能恢复到备份时的同主版本环境,或者比该版本更高的后续版本。但实际操作中,比如你手上是13.x的备份,想恢复到16.x的新实例,直接恢复会报错或者行为异常。我踩过一次之后学乖了,老老实实按阶梯升级:
- 假设旧环境是13.4,先升级到13.12最新版,再升到14.x,再到15.x,最后到16.x。
- 每升一个大版本前做一次全量备份,升级完验证一次基本功能。
跨版本迁移时,gitlab-secrets.json的兼容性也会变化,新版本升级过程通常会自己调整密钥文件格式,只要别手动覆盖、别混版本复制旧文件,一般问题不大。
另外,跨版本后数据库会做迁移,时间长短取决于数据量。百万级Issue和几十G仓库的实例,数据库迁移可能要跑一两个小时,运维窗口要留够,别把时间卡得太死。
4.3 Docker方式部署的GitLab迁移
如果GitLab是用Docker跑的,备份和迁移的姿势有些不一样。
容器内执行备份:
docker exec -t gitlab gitlab-backup create STRATEGY=copy备份文件会生成在容器内/var/opt/gitlab/backups,而这个目录通常挂载到了宿主机的某个路径上,比如:
docker run -d \ --name gitlab \ -v /srv/gitlab/data:/var/opt/gitlab \ -v /srv/gitlab/config:/etc/gitlab \ gitlab/gitlab-ce:latest这种情况下备份出来的tar包直接落在宿主机的/srv/gitlab/data/backups里。迁移到新机器时,有两种做法:
- 方法一(推荐):在新容器里执行
docker exec -t gitlab gitlab-backup restore BACKUP=xxx,同时把srv/gitlab/config下的配置和secrets一起拷贝过去。 - 方法二:直接把整个
/srv/gitlab挂载目录原样拷到新机器,再把容器跑起来。这个方法看起来省事,但要求宿主机文件权限、数据目录结构完全一致,处理不好容易出现权限错乱。除非整体系统盘级别克隆,否则我更推荐备份-恢复的方式。
4.4 迁移后最容易忽略的几个收尾项
代码搬过去了,页面能开了,不代表迁移收尾完成。这几件事我每次迁移后都会单独处理:
- 修改external_url:如果新机器的域名或IP变了,
/etc/gitlab/gitlab.rb里的external_url要改,否则仓库的clone地址、CI回调用地址全是旧的。 - 通知团队更新remote地址:所有开发本地
git remote指向旧服务器,迁移后要统一通知,给一份新地址的替换命令。 - CI/CD和Webhook里的回调地址:项目配置里如果有GitLab URL写死的地方,要同步检查。
- HTTPS证书:新机器的SSL证书如果不在GitLab处理范围,要确认Nginx层或GitLab内置的HTTPS配置正常。
5. 常见问题与排查技巧实录
5.1 运维中高频故障速查表
我把日常备份、恢复、迁移中遇到的高频问题整理成一个速查表,后面详细展开说:
| 现象 | 常见原因 | 处理方向 |
|---|---|---|
备份命令报Permission denied | 备份目录权限不对 | 检查/var/opt/gitlab/backups属主是否为git用户 |
| 备份文件生成但只有几M | 某个组件本身没数据,或备份中断 | 用tar -tf检查包内目录结构 |
| 恢复后项目打开404 | 数据库里元数据丢失,或仓库目录没恢复完整 | 检查git-data目录,确认namespace路径 |
| 恢复后登录页面500 | gitlab-secrets.json不匹配 | 重新拷贝旧环境的secrets文件 |
| Runner全部离线 | Runner注册token变更 | 在Admin区域重新注册Runner |
| Webhook请求失败 | external_url或回调地址没改 | 修改gitlab.rb后reconfigure |
5.2 恢复时“项目数据存在但页面404”的经典场景
这个问题的原因通常不在仓库本身,而在于数据库里的项目记录和仓库目录对不上。GitLab判断一个项目是否存在,先查数据库,再关联仓库目录。如果恢复时数据库没有完整恢复,或者仓库目录的namespace路径和数据库记录对不上,就会表现成“仓库文件明明在,但网页打开404”。
排查入口是看生产日志:
sudo tail -f /var/log/gitlab/gitlab-rails/production_json.log里面有详细的项目查找和异常栈。如果日志提示找不到仓库路径,说明仓库目录有问题;如果日志提示数据库查询为空,说明数据库恢复不完整。对症下药,别一上来就怀疑是权限问题。
5.3 备份文件损坏的提前预警
有一次我例行检查备份包,发现某天的tar包能tar -tf列出文件,但到真正恢复演练时发现里头有一个项目的sidecar文件CRC不对。后来再也不敢只看文件大小,而是每个月抽一次备份包,找一台闲置机器完整做一次恢复演练。演练时间成本不低,但对比真出事时全团队干瞪眼,这半小时太值了。
如果备份包在拷贝到异地时发生损坏,tar -tf会直接报错,这种还算好发现的。最怕的是备份时数据正在写入导致逻辑层面的不一致,比如某个仓库当时正在执行GC,仓库目录刚好被打包了一半。这也是我坚持加STRATEGY=copy参数的原因——让备份工具先做一次内部快照复制,尽量避免逻辑不一致。
5.4 Jenkins、API Token等集成连接失败
GitLab和Jenkins等工具集成时,经常会遇到连接失败,错误提示类似Failed to authenticate或login failed。这种问题在恢复和迁移后尤其高发。
常见原因有三个:
- GitLab的API token是绑定用户、绑定应用的,恢复后的数据库如果回滚到了token创建之前的时间点,token自然失效。
gitlab-secrets.json不匹配导致加密上下文变化。- external_url改了,Jenkins那边配置的GitLab URL还是旧地址。
处理方式很直接:在GitLab里重新生成API token,到Jenkins里更新凭据,顺便把GitLab的URL配置改成新地址。
6. 备份体系之外的个人建议
6.1 把备份和“对象存储”结合,别让备份和主数据死在同一台机器
很多团队把备份文件和GitLab放在同一台服务器的同一块磁盘上,这在我看来约等于没备份。磁盘整块挂了的时候,主数据和备份一起陪葬。更合理的做法是配置GitLab的备份路径到NAS或对象存储挂载点,或者备份完立刻用rsync、rclone推送到异地存储。
我现在就是双重策略:本地备份保留7天,另有一份备份通过rclone定时同步到远端对象存储,保留30天。异地那份在出大事时才会用,平时基本不碰,但心里踏实很多。
6.2 定期演练恢复,最好把流程文档化
我知道“演练恢复”这件事,一听就是“有时间再说”的事。但恰恰是没演练过,真到灾难发生时才知道有多少前置步骤被忽略。我的习惯是每季度做一次完整演练,把恢复流程写成文档,标注每一步大概耗时、常见报错怎么处理,这样即使换人操作也能照着走。
演练时甚至可以把网络切断一部分,模拟极端情况,比如只能拿到距今一周的备份包、配置文件和secrets全部丢失,检验团队的降级应对能力。这种演练做多了,遇到真实事故时就不会手忙脚乱。
6.3 最后分享一个让我少走弯路的小习惯
每次备份完后,我都会顺手看一眼/var/log/gitlab-backup.log的最后几行,确认是否有done或者错误信息。很多备份工具的坑在于,退出码为0并不代表备份完全成功——某个子组件可能悄悄失败了但整体流程还是正常结束。我看日志不是看它有没有报错,而是看它是否明确写了哪些组件被备份、哪些被跳过。
遇到跳过项我会单独处理,绝不带着疑问把备份当作有效备份保留。因为一次备份记录为“部分成功”,会让后面所有的恢复方案都建立在一个不完整的基础上,这比没有备份更危险。