自建 GitLab 这件事,装起来有多爽,备份没做好翻车时就有多痛。前两篇聊了安装和日常使用,这篇把最后一块拼图补上:备份和恢复。GitLab 不是一个能靠"把整个/var/opt/gitlab目录打个 tar 包"就完事的系统,它背后有一整套 PostgreSQL 数据库、一堆加密密钥、N 个服务进程的数据都交织在一起。我见过有人把整个宿主机云盘快照当成备份,结果真到恢复时发现仓库文件更新丢了、CI 变量全部解密失败,那场面基本等于这几个月的代码白干。
这篇会把 GitLab 备份恢复的底层逻辑、标准操作、自动化和恢复演练完整过一遍,适合所有自己维护 GitLab 实例的开发者、运维和"公司里被迫兼管服务器"的朋友。里面提到的命令我都按当前主流版本的实际行为写,老版本命令差异也会标注,照着执行基本不会踩坑。
1. 自建 GitLab 的备份恢复,到底在备份什么
1.1 先讲一次不算严重但吓出一身冷汗的故障
我遇到过这么个事:某公司内部跑着一台单机 GitLab,数据放在一块数据盘里,看着挺正常。某天早上一到工位,同事说代码推不上去,登录服务器一看,磁盘 IO 错误,文件系统直接变成只读。重启之后部分数据卷损坏,Git 仓库目录里若干对象的校验和不对,仓库直接打不开。
当时所有人第一反应是"我们有云盘快照,怕什么"。等真正去翻快照的时候才发现,快照是前一天晚上打的,当天上午大家推的十几个 commit 全没了。虽然最后靠 git fsck 抢救回来一部分,但有些新仓库因为没有远端备份,损失是实打实的。
这个经历给我的教训是:底层快照不是备份,它只是"同一份数据的另一个时间点副本",一旦数据本身已经损坏,快照同样会跟着坏。GitLab 这种应用,必须做应用层一致性的备份,也就是要同时保证 PostgreSQL 数据库、Git 仓库、配置文件、密钥文件等所有组件处于一个逻辑一致的状态。
1.2 拆解 GitLab 的"家底":配置文件、数据库和密钥
GitLab 自建(Omnibus 安装)的数据分布并不复杂,但分散在好几处。先记住这张表:
| 路径 | 内容 | 丢了会怎样 |
|---|---|---|
/etc/gitlab/gitlab.rb | 主配置,包括域名、端口、备份路径、各种开关 | 配置全部丢失,服务可以重新配,但很费时间 |
/etc/gitlab/gitlab-secrets.json | 密钥文件,存储数据库加密密钥、会话密钥、2FA 种子等 | 最致命,用户密码全部失效,CI 变量无法解密 |
/var/opt/gitlab/git-data | 所有 Git 仓库的裸仓库 | 代码全部丢失,这是最核心的资产 |
/var/opt/gitlab/postgresql/data | PostgreSQL 数据库数据,包含用户、权限、合并请求、CI 记录 | Web 界面数据全部丢失 |
/var/opt/gitlab/gitlab-rails/uploads | 上传的头像、附件、设计文件 | 附件丢失 |
/var/opt/gitlab/gitlab-rails/shared/artifacts | CI/CD 产物、页面制品 | 构建产物丢失,流水线记录不完整 |
/var/opt/gitlab/gitlab-ci/builds | 构建日志 | 日志丢失 |
其中最容易被人忽略的是/etc/gitlab/gitlab-secrets.json。很多人以为备份就是备份数据和配置,其实 GitLab 在数据库里存了很多敏感字段是加密的,加密用的密钥就放在这个 secrets 文件里。你的项目 CI 变量、用户 2FA 种子、Rails 的secret_key_base,全部依赖这个文件。密钥丢了,数据库就算完整恢复,也没法解开这些数据,等于白恢复。
所以我自己对备份的定义很简单:GitLab 的完整备份 = 数据备份(gitlab-backup生成的 tar)+ 配置文件备份(gitlab.rb)+ 密钥文件备份(gitlab-secrets.json),三样缺一不可。
1.3 gitlab-backup 的机制和产物
Omnibus 版本自带的备份命令是gitlab-backup create,在 13.x 之前的版本里则写作gitlab-rake gitlab:backup:create。它内部做的事情是:
- 对 PostgreSQL 执行
pg_dump,导出数据库到一个临时目录; - 整理
/var/opt/gitlab下的仓库、上传文件、构建产物等数据; - 把这些内容打成 tar 包,放到备份目录,默认是
/var/opt/gitlab/backups; - 生成一个
backup_information.yml,记录 backup 的版本、时间、GitLab 版本、Git 版本等关键元数据。
最终产生的文件命名规则类似这样:
1700010000_2025_01_01_17.0.0_gitlab_backup.tar前面是一串时间戳,然后是备份日期和 GitLab 版本。注意,gitlab-backup默认不会把/etc/gitlab下的配置和密钥文件打进 tar,这是官方设计,意思是配置文件要自己单独备份。这个细节是无数人恢复失败的头号原因,后面我会专门展开。
2. 第一次备份该怎么跑:准备、执行、验证
2.1 跑备份前的环境准备和目录规划
第一次执行备份前,先把环境查清楚,不然很容易跑到一半磁盘满了。我的固定步骤是:
# 查看备份目录所在磁盘空间 df -h /var/opt/gitlab/backups # 查看现有数据大小 du -sh /var/opt/gitlab/git-data du -sh /var/opt/gitlab/postgresql/data估算备份文件大概有多大,最稳的办法是预留出数据目录 1.5 倍以上的空间。因为gitlab-backup执行时先落临时文件再打包,高峰占用会明显超过最终 tar 的体积。如果你数据有 200GB,备份目录至少留 300GB 比较好。
其次要确认备份目录跟数据目录不要在同一块物理盘上。这个道理很简单:备份的意义就是在原始数据损坏时能救回来,如果磁盘直接物理故障,备份和数据一起没了,那备份就是白做。稍好一点的方案是把备份目录放到另一块挂载盘,或者直接通过网络备份到另一台机器。
权限方面,gitlab-backup create需要以root或者git用户执行。如果你用普通用户执行,通常会遇到权限报错。最省事的做法是sudo gitlab-backup create。
如果想把备份目录改到别的位置,在/etc/gitlab/gitlab.rb里改:
gitlab_rails['backup_path'] = "/data/gitlab/backups"改完执行sudo gitlab-ctl reconfigure让配置生效。改之前记得备份目录要先建好,并设置成git:git属主:
mkdir -p /data/gitlab/backups chown git:git /data/gitlab/backups2.2 手动备份的标准操作和参数说明
环境准备好后,执行手动备份:
sudo gitlab-backup create STRATEGY=copy CRON=1这里面两个参数我建议第一次就跑上。STRATEGY=copy会用文件系统 copy 的方式处理正在写入的数据库文件,对整个实例的线上影响比默认方式小,特别适合还在被团队使用的生产实例。CRON=1表示非交互模式,避免执行过程中停下来问 "Do you want to continue?" 这类问题,便于以后把它写进定时任务。
如果你的实例数据量很大,还可以考虑加这些参数:
sudo gitlab-backup create STRATEGY=copy CRON=1 SKIP=artifacts,registrySKIP后面可以跟逗号分隔的组件名,比如不备份制品和容器镜像仓库,速度会快不少。但注意,跳过了的东西恢复时也没有,需要自己权衡。
执行完之后,检查备份文件:
ls -lh /var/opt/gitlab/backups/成功的标志有三个:tar 文件时间戳接近当前时间、文件大小不为零、日志结尾出现 "done" 字样。日志末尾如果出现红色或者 ERROR,别管 tar 文件存不存在,直接当作失败处理。
2.3 备份文件里的秘密:看懂备份内容的组成部分
很多人以为备份文件就是个黑盒,其实里面结构很清楚。执行:
tar tf /var/opt/gitlab/backups/1700010000_2025_01_01_17.0.0_gitlab_backup.tar | head -50你会看到类似这样的内容:
backup_information.yml db/ db/database.sql.gz repositories/ repositories/group/ repositories/group/project.git/ uploads/ builds/ artifacts/ pages/这么一看就明白了:db/database.sql.gz是 PostgreSQL 的导出结果,repositories/是裸仓库,uploads/是附件,builds/是构建日志。恢复时,gitlab-backup restore就是读取这个结构,把每个组件还原到对应目录。
还有个关键文件backup_information.yml,恢复工具会读取它来判断备份时的 GitLab 版本、Git 版本等信息。所以请不要手贱去改 tar 包里的文件,改坏了版本信息,恢复工具会直接拒绝工作。
2.4 验证备份有效性的最低成本方法
只备份不验证,等于没备份。这是我反复吃亏得出的结论。有一次定时任务日志显示备份成功,但实际 tar 文件在打包中途因为磁盘满被截断了,日志里报错被好几条无关输出盖住,一直没发现。直到后来要迁移实例,恢复时才发现备份文件根本不完整。
最低成本的验证分两层。第一层是物理验证,每隔一段时间做一次:
gzip -t /var/opt/gitlab/backups/xxx_gitlab_backup.tar如果没有报错说明压缩层是完好的。再执行:
tar tf /var/opt/gitlab/backups/xxx_gitlab_backup.tar > /dev/null它能确认 tar 包内所有文件都能正常读取。但这两步只能验证"文件没坏",不能验证"数据逻辑一致"。真正靠谱的验证是拿备份文件在一台临时 GitLab 实例上实际恢复一次,然后跑gitlab-rake gitlab:check检查完整性。这个成本高一些,但频率不用太勤,一个月或一个季度一次足够。
3. 别让备份变成废纸:自动化、异地和保留策略
3.1 定时备份的几种方案:cron 与日志告警
手动备份只是开始,定时备份才是常态。我见过太多团队说"我们会手动备份",结果一忙起来两三个月都不碰一次。最省心的方式是写进 cron:
0 2 * * * /usr/bin/flock -n /tmp/gitlab-backup.lock /usr/bin/gitlab-backup create STRATEGY=copy CRON=1 >> /var/log/gitlab-backup.log 2>&1这里用flock -n给备份加锁,防止上一个备份还没跑完、下一个任务又开始执行,导致两个备份进程同时写同一个备份目录。加上>> /var/log/gitlab-backup.log 2>&1把输出存到日志,方便事后排查。
还有一些思路是把备份脚本做成 systemd timer,管理和日志都会更规范。但对多数单机自建场景来说,cron 足够可靠。关键在于必须有失败告警,否则写进 cron 也不代表真的在保护你。最简单的做法是让备份脚本在失败时退出码非零,再用外部监控盯住退出状态,或者把日志同步到专门的收集服务里。
3.2 异地备份:rsync 和对象存储
定时的本机备份只解决"数据损坏"问题,不解决"机器没了"的问题。机房断电、服务器被盗、磁盘阵列整体故障,这些情况下本机备份也一起完蛋。所以异地备份是必须的,尤其对代码资产来说。
单机自建最简单的方案是用 rsync 把备份目录同步到另一台机器:
rsync -avz --delete /var/opt/gitlab/backups/ backup-user@backup-host:/data/gitlab-backups/--delete参数确保远程目录跟本地保持一致,本地清理过期备份时,远程也不会堆积。这个命令同样可以写进 cron,在 GitLab 备份完成后半小时执行。
如果公司有对象存储(S3 兼容的私有化部署也很多),GitLab 本身支持直接上传备份到对象存储。在gitlab.rb里配置gitlab_rails['backup_upload_connection']等参数,官方文档写得很详细。但我的建议是:盲目上对象存储之前,先确认你了解备份的上传和下载流程,否则恢复时反而多一道凭据障碍。很多时候 rsync 到一台内网备份机已经足够可靠。
3.3 保留策略:保留几份、怎么清理
备份文件体积会越来越大,不可能无限堆积。GitLab 的备份命令默认只负责生成,不负责清理旧文件。所以你要自己定保留策略。我现在的策略是:
- 每日备份保留最近 14 天;
- 每月 1 号的全量备份单独保留 6 个月;
- 异地备份跟本地保持同样节奏。
清理可以用一行命令:
find /var/opt/gitlab/backups -name "*_gitlab_backup.tar" -mtime +14 -delete但注意,如果每月归档目录不在同一个地方,要单独处理。另外提醒一句,find -mtime依赖的是文件修改时间,备份文件名里有时间戳,判断时最好用时间戳而不是 mtime,避免有人手动 touch 过文件导致误判。
备份加密这件事,没做的人总觉得"内网无所谓"。如果你把备份文件 rsync 到异地或者归档到对象存储,强烈建议加密。最轻量的方式是 gpg 对称加密:
gpg --symmetric --cipher-algo AES256 /var/opt/gitlab/backups/xxx_gitlab_backup.tar执行后会生成.gpg文件,原文件建议删掉。恢复时先解密再放回备份目录。这里有个小坑,gitlab-backup restore识别备份文件靠的是文件名里的时间戳规则,解密出来的文件名必须保持跟原 tar 文件一致,才能被正确识别。
3.4 恢复演练不能只在脑子里做
我见过太多团队把备份脚本写得漂漂亮亮,但从来没有真正恢复过一次。等到版本升级、机器迁移、磁盘损坏这些真出事的时候,才发现恢复流程有一步漏了,或者备份文件本身已经坏了。恢复演练不是可有可无的加分项,它是备份策略的验收环节。
我的做法是每季度找一台 staging 虚拟机,安装跟生产相同版本的 GitLab,从备份服务器拉最新备份文件,完整执行一次恢复,然后跑gitlab-rake gitlab:check。整个过程不算太久,但能提前暴露很多问题,比如新版 GitLab 数据库迁移不兼容、备份文件被加密后忘了密钥、异地备份链路断了好几天没人发现。等真出事再发现这些问题,代价就不是一台虚拟机的事情了。
4. 恢复比备份更需要排练:从零到一完整恢复流程
4.1 恢复前必须对版本、环境、权限做的检查
恢复流程中最核心的一条铁律是:备份版本和待恢复的 GitLab 版本必须保持一致。GitLab 的备份文件包含数据库 dump 和内部结构,跨大版本恢复非常容易因数据库迁移问题卡住。官方文档虽然允许恢复到同版本或以后版本,但我的实践经验是:恢复到完全相同的版本最稳,尤其是小版本号最好也对上。
恢复之前要做三件准备工作。第一,检查备份文件里的backup_information.yml,确认gitlab_version与当前实例版本一致。第二,确认备份文件属主是git:git,否则恢复过程中 GitLab 无法读写,执行:
chown git:git /var/opt/gitlab/backups/xxx_gitlab_backup.tar第三,确认备份文件已经放在当前实例的备份目录里,并且文件名没有被人为改过。
4.2 三种典型恢复场景的命令与步骤
场景一:同一台机器原地恢复。假设实例还在但数据被误删或损坏。官方推荐先停掉会写数据的服务,避免恢复过程中后端进程继续往数据库里写数据:
sudo gitlab-ctl stop puma sudo gitlab-ctl stop sidekiq如果是老版本,puma可能是unicorn,用gitlab-ctl status确认一下实际服务名就好。然后执行恢复:
sudo gitlab-backup restore BACKUP=1700010000_2025_01_01_17.0.0恢复过程会提示输入yes确认,如果你用脚本自动化,需要提前处理交互。恢复完成后执行:
sudo gitlab-ctl reconfigure sudo gitlab-ctl restart sudo gitlab-rake gitlab:check场景二:迁移到新机器。新机器上安装与备份相同版本的 GitLab,先停止服务:
sudo gitlab-ctl stop把旧机器的/etc/gitlab/gitlab.rb和/etc/gitlab/gitlab-secrets.json拷贝到新机器,再执行 reconfigure。然后把备份 tar 文件放到新机器的备份目录,chown 之后执行 restore。特别注意顺序:配置文件和密钥要在恢复数据之前准备好,否则 GitLab 启动时找不到密钥,或者用默认密钥初始化,数据恢复后反而可能解密失败。
场景三:容器化部署的恢复。如果 GitLab 跑在容器里,备份和恢复都在容器内执行:
docker exec -it gitlab gitlab-backup create docker exec -it gitlab gitlab-backup restore BACKUP=1700010000_2025_01_01_17.0.0但容器场景有个额外风险:容器镜像版本和备份版本不一致的概率更高,因为很多人习惯直接docker pull最新镜像。恢复前务必确认镜像版本与备份文件版本一致。
4.3 恢复后必做的验证:登录、仓库、CI 变量、Webhook
恢复完成不等于万事大吉,必须做一轮业务验证。我的固定顺序是:
- 访问 Web 界面,用管理员账号登录,确认页面正常、项目列表数量正确;
- 随便挑一个项目执行
git clone操作,确认 HTTP 和 SSH 两种协议都通; - 打开一个项目的 CI/CD 设置页,确认变量能看到、Runner 列表里没有丢失;
- 检查一个 Webhook 配置,确认外部通知地址还在;
- 找一个开启 2FA 的用户账号实测登录,确认密钥文件恢复有效。
这些验证里最容易翻车的是 CI 变量和 2FA。如果之前没备份gitlab-secrets.json,恢复后 Web 界面可能正常,但 CI 变量会显示乱码或无法解密,团队成员的 2FA 全部失效。这个问题不是数据损坏,是密钥不匹配,只能靠密钥文件来解决。
4.4 把恢复演练做成脚本,而不是手工流程
手工恢复演练最多做一两次,时间长了就容易偷懒。我现在把演练过程写成了一个简单脚本,大致流程是:新起一台空白虚拟机 -> 安装对应版本 GitLab -> 拷贝配置和密钥 -> 从备份服务器拉备份文件 -> 恢复 -> 跑gitlab-check-> 输出报告。
这套脚本虽然不复杂,但每次演练只要半小时就能完成全部动作。相比手动操作,脚本化能保证每次都走同样的步骤,不会有"这次忘了 chown"这种低级失误。如果你团队规模不大,哪怕只是把命令按顺序存在一个文档里,也比每次临时回忆要强得多。
5. 我踩过的坑:这些细节让恢复差点失败
5.1 密钥文件没备份导致用户登录全部失效
有一次做跨机器迁移,我小心翼翼地把gitlab.rb和备份 tar 都拷到了新机器,恢复流程跑完,Web 界面能打开,项目和仓库都回来了。结果当天下午团队同事陆续反馈:密码登录报错、两步验证无法通过、CI 变量显示异常。
排查下来才发现,我在准备材料时把/etc/gitlab/gitlab-secrets.json漏掉了。新机器启动时 GitLab 生成了一套全新密钥,等我把数据库恢复回去,数据库里的加密数据是用旧密钥加密的,自然解不开。
解决方案是立刻把旧机器的gitlab-secrets.json传过去覆盖,然后重启服务。因为密钥文件必须在实例第一次启动前就放好,否则后续再换也会引发不一致。这个坑之后,我把密钥文件直接列为备份材料的第一项,每次先检查它有没有被拷走。
5.2 版本不一致导致数据库迁移卡死
另一次恢复演练,我为了省事在 staging 上装了一个比备份版本新很多的主版本,想着"反正能恢复再升级"。结果gitlab-backup restore执行到一半,数据库迁移脚本直接报错退出,整个 staging 实例处于半初始化状态。查阅日志发现备份文件里的版本是老的,新版本 GitLab 在恢复老数据库 dump 后会自动执行迁移,但迁移过程遇到一个旧数据残留导致约束冲突。
后来我改了策略:恢复演练和生产迁移都严格要求备份版本和安装版本一致,跨版本操作一定是"先恢复到同版本 -> 验证没问题 -> 再正常执行 GitLab 升级"。这个顺序能避免把"恢复失败"和"升级失败"两件事混在一次变更里,排查问题时会轻松非常多。
5.3 恢复后仓库 hook 和 CI 的权限问题
还有一次恢复完成、页面正常、clone 也正常,但用户推代码时收到 500 错误。跑gitlab-rake gitlab:check发现一堆仓库的权限有问题。原因很常见:备份文件是从另一台机器上拉过来的,解压后属主不是git:git,或者两台机器的git用户 UID 不一致。
解决方式比较直接:
chown -R git:git /var/opt/gitlab/git-data chown -R git:git /var/opt/gitlab/gitlab-rails/uploads sudo gitlab-ctl reconfigure恢复完数据后别急着开放提交权限,先用gitlab-rake gitlab:check把权限类问题扫一遍再放人进来。如果公司同时维护多套环境,建议在两台机器上统一git用户 UID,能省很多跨环境拷贝之后的麻烦。
5.4 备份超时和备份文件损坏的排查思路
最后说一个最隐蔽的坑:日志显示备份成功,但备份文件实际已经损坏。当时定时任务每天凌晨跑,某天数据量涨得快,磁盘快满了,备份脚本在打包阶段因为空间不足报错退出。但由于脚本没有做退出码判断,外部监控看到进程"跑过了"就以为没问题,后续的异地 rsync 也一直在同步那个被截断的 tar 文件。
排查这类问题的路径其实很清晰:先看/var/log/gitlab-backup.log的结束状态,确认有没有DONE标记;再执行gzip -t验证文件完整性;顺带用df -h检查磁盘占用。三个动作加起来不到一分钟,却能拦截掉大部分备份失效场景。为了防止再发生,我在备份脚本里加了一步:备份结束后自动执行gzip -t,失败就通过 webhook 推消息告警。
6. 我的固定检查表和最后的建议
把前面所有经验整理下来,我自己现在给团队定的固定动作是这么一组:
- 每天 02:00 执行自动备份,日志输出到独立文件,退出码异常必须告警;
- 每周手动抽查一次备份日志,确认 tar 文件大小和
gzip -t都通过; - 每月把最新备份文件 rsync 到异地备份机,并实际验证异地文件可读;
- 每季度在 staging 环境做一次完整恢复演练,顺手验证密钥文件和配置文件是否齐整。
这套节奏听起来很简单,但真正坚持下来并不容易。备份恢复这个事,难的不是跑通一条命令,而是让"能恢复"变成常态。我在经历过密钥丢失、版本不匹配、备份文件损坏这些事故之后最大的体会是:与其研究各种花哨的高可用方案,不如先把最基础的备份-验证-恢复闭环做扎实。每次备份完顺手在临时环境里恢复一次,虽然占一点时间,但真到翻车那天,你会感谢当时多花的那十分钟。