从 GitLab 14.5.0 迁移到 GitLab 19.1.0:一次自托管 GitLab Docker 升级实战复盘
记录一次自托管 GitLab 从安装版 14.5.0 迁移并升级到 Docker 版 19.1.0 的完整过程,包括 required upgrade stops 逐级升级、后台迁移失败处理、PostgreSQL 分区修复等卡点经验。
摘要:
本文记录一次自托管 GitLab 从安装版 14.5.0 迁移并升级到 Docker 版 19.1.0 的完整过程。迁移起因是旧版 GitLab 14.5.0 被安全扫描和漏洞通报标记为高风险,继续暴露在生产网络中不可接受。整个过程没有直接跨版本升级,而是先恢复到可运行版本,再沿 GitLab required upgrade stops 逐级升级,并处理了后台迁移失败、PostgreSQL 分区缺失、reconfigure 未完成导致 502 等问题。
关键词:
GitLab 14.5.0 迁移、GitLab Docker 升级、GitLab 19.1.0、GitLab required upgrade stops、batched background migrations、GitLab 后台迁移失败、GitLab PostgreSQL 升级、GitLab 自托管安全升级
背景:为什么要从 GitLab 14.5.0 开始迁移
这次迁移的起点是一套运行多年的自托管 GitLab,版本是安装版 14.5.0。它的问题不是”还能不能用”,而是”还能不能安全地继续用”。
GitLab 作为代码托管和 CI/CD 中心,里面通常保存了源码、访问令牌、CI/CD 变量、部署密钥、Webhook、Runner 配置等敏感资产。一旦 GitLab 实例存在严重安全风险,影响面往往不是一个应用,而是整个研发和交付链路。
GitLab 官方安全发布中也反复强调,自托管实例应尽快升级到包含安全修复的版本。例如 GitLab 的 Critical Security Release 会明确建议受影响安装立即升级,并提醒不要跳过升级路径中的必要站点。官方升级文档也说明,跨版本升级必须经过 required upgrade stops,并等待后台迁移完成后才能继续下一站。
参考资料:
总体策略:不要直接从 14.5.0 升到最新
这次迁移最重要的经验是:
GitLab 不能按普通应用那样”换个 latest 镜像就完事”。
正确做法是:先恢复到兼容版本,再按 required upgrade stops 逐级升级。
最终采用的路线是:
14.5.0
→ 14.10.5
→ 15.0.5
→ 15.4.6
→ 15.11.13
→ 16.3.9
→ 16.7.10
→ 16.11.10
→ 17.3.7
→ 17.5.5
→ 17.8.7
→ 17.11.7
→ 18.2.0
→ 18.5.0
→ 18.8.0
→ 18.11.0
→ 19.0.0
→ 19.1.0
实际执行中,关键恢复点是 15.11.13。后续从恢复后的 15.11.13 继续升级到 19.1.0。
每个版本站点都遵循同一套原则:
- 当前版本必须健康。
gitlab:check必须通过。- Web 登录页必须返回
HTTP 200。 - 后台迁移必须全部完成。
- 完成当前版本备份后才能升级下一站。
- 不使用
latest标签,只使用固定版本镜像。
Docker 化后的目录和版本固定
迁移后的 GitLab 使用 Docker Compose 管理,核心目录如下:
/opt/gitlab-docker/docker-compose.yml
/srv/gitlab/config
/srv/gitlab/logs
/srv/gitlab/data
镜像始终固定版本,例如:
image: gitlab/gitlab-ce:19.1.0-ce.0
不要在生产环境长期使用:
image: gitlab/gitlab-ce:latest
原因很简单:GitLab 升级强依赖版本路径和后台迁移状态,latest 可能导致不可控跨版本升级。
每一站升级的固定流程
每次升级前先检查当前状态:
docker ps
docker exec gitlab gitlab-rake gitlab:env:info
docker exec gitlab gitlab-rake gitlab:check SANITIZE=true
docker exec gitlab gitlab-psql -d gitlabhq_production -c \
"select status, count(*) from batched_background_migrations group by status order by status;"
升级时先停容器,再修正 PostgreSQL 和 Redis 数据目录权限:
cd /opt/gitlab-docker
docker compose down
chown -R 996:996 /srv/gitlab/data/postgresql
chown -R 997:997 /srv/gitlab/data/redis
sed -i "s|gitlab/gitlab-ce:18.11.0-ce.0|gitlab/gitlab-ce:19.0.0-ce.0|" docker-compose.yml
docker compose up -d
启动后不要只看 Docker health,还要确认 GitLab 内部服务:
docker exec gitlab gitlab-ctl status
docker exec gitlab bash -lc 'test -S /var/opt/gitlab/postgresql/.s.PGSQL.5432 && echo yes || echo no'
然后验证:
docker exec gitlab gitlab-rake gitlab:env:info
docker exec gitlab gitlab-rake gitlab:check SANITIZE=true
curl -sS -o /tmp/gitlab_signin.html -w "HTTP %{http_code}\n" http://127.0.0.1/users/sign_in
后台迁移必须等待完成:
docker exec gitlab gitlab-psql -d gitlabhq_production -c \
"select id, job_class_name, status from batched_background_migrations where status not in (3,6) order by id;"
查询结果为空,才允许继续下一站。
卡点一:15.11.13 的后台迁移失败
恢复到 15.11.13 后,升级前检查发现一条失败后台迁移:
NullifyOrphanRunnerIdOnCiBuilds
错误表现是 SQL 里出现空列名:
PG::SyntaxError: zero-length delimited identifier
WHERE "ci_builds".""
这个迁移的目标是把 ci_builds 中已经不存在对应 Runner 的 runner_id 置空。我们先验证数据:
select runner_id, count(*)
from ci_builds
where runner_id is not null
and not exists (
select 1 from ci_runners where ci_runners.id = ci_builds.runner_id
)
group by runner_id;
结果为 0 行,说明没有实际待修复数据。尝试官方 finalize 仍失败后,才只针对这一条已确认无待处理数据的迁移做状态修正。
经验:
不要看到后台迁移失败就直接改状态。
必须先确认迁移意图、失败原因、待处理数据是否为 0,并且先做备份。
卡点二:18.5.0 的 PostgreSQL 分区未挂载
升级到 18.5.0 后,后台迁移 BackfillPartitionedProjectDailyStatistics 失败。
错误是:
PG::CheckViolation: no partition of relation "project_daily_statistics_b8088ecbd2" found for row
排查发现,部分月分区表存在,但没有 attach 到父分区表。处理方式是将缺失月份的分区重新 attach,然后重试失败的 batched migration。
经验:
GitLab 升级失败不一定是版本路径错,也可能是历史数据结构在恢复或旧版本运行中留下了不一致状态。
这类问题必须查数据库结构,而不是反复重启容器。
卡点三:18.11.0 验证过早,Rails 连不上 PostgreSQL
升级到 18.11.0 后,Docker 已经显示 healthy,但执行:
docker exec gitlab gitlab-rake gitlab:env:info
出现:
ActiveRecord::DatabaseConnectionError
PG::ConnectionBad: connection to server on socket "/var/opt/gitlab/postgresql/.s.PGSQL.5432" failed
排查后发现不是数据库损坏,而是 GitLab reconfigure 还在收尾,PostgreSQL 被 runit 短暂重启。等待所有服务进入 run 状态后重新验证,问题消失。
经验:
Docker health 不是 GitLab 升级完成的唯一标准。
还要看gitlab-ctl status、PostgreSQL socket、Puma、Sidekiq、Nginx 是否全部正常。
卡点四:19.1.0 初次访问 502
升级到 19.1.0 后,第一次访问登录页返回:
HTTP 502
gitlab:check 也显示:
Internal API unreachable
Sidekiq: Running? no
继续排查发现,reconfigure 尚未完成,Puma、Nginx、Workhorse、Sidekiq 还在陆续启动。等待完成后重新执行检查:
HTTP 200
gitlab:check 通过
后台迁移 active = 0
经验:
502 不一定意味着升级失败。
大版本升级后,先看 reconfigure 日志和服务状态,确认是否只是启动窗口期。
备份策略:每站备份,只保留最后一个正常备份
每完成一个版本站点,都执行数据备份和配置备份:
docker exec gitlab gitlab-backup create SKIP=registry,artifacts,lfs,packages,terraform_state
tar -czf /root/gitlab_config_${VERSION}_$(date +%F_%H%M).tar.gz -C /srv/gitlab config
由于磁盘空间有限,数据备份只保留最后一个正常版本:
latest=$(ls -1t /srv/gitlab/data/backups/*_gitlab_backup.tar | head -n1)
find /srv/gitlab/data/backups -maxdepth 1 -type f -name "*_gitlab_backup.tar" ! -path "$latest" -print -delete
最终保留:
1782278240_2026_06_24_19.1.0_gitlab_backup.tar
配置备份单独保留,因为 GitLab 数据备份不包含 gitlab.rb 和 gitlab-secrets.json。这两个文件在恢复时非常关键。
最终结果
最终 GitLab 成功运行在:
gitlab/gitlab-ce:19.1.0-ce.0
验证结果:
Version: 19.1.0
DB Version: 17.8
HTTP 200
active background migrations: 0
容器状态:
gitlab gitlab/gitlab-ce:19.1.0-ce.0 healthy
这次迁移留下的几个结论
第一,GitLab 旧版本不能长期拖着不升。
尤其是自托管实例,一旦被安全扫描或漏洞通报标记为高风险,处理方式不应该是临时加防火墙规则,而应该尽快进入升级窗口。
第二,GitLab 升级不能跳 required upgrade stops。
官方文档明确要求每个 required stop 后等待后台迁移完成。跳版本看起来省时间,实际可能把问题留到后面,甚至导致数据库迁移卡死。
第三,后台迁移是 GitLab 升级的核心门禁。
每一站都要查:
select id, job_class_name, status
from batched_background_migrations
where status not in (3,6)
order by id;
第四,Docker health 不等于业务可用。
必须同时检查 gitlab:check、Web 登录页、Sidekiq、Puma、Nginx、PostgreSQL socket。
第五,备份不是流程装饰,而是唯一可靠回滚手段。
不要把”改回旧镜像启动”当成 GitLab 回滚方式。真正可控的回滚是恢复对应版本的数据备份和配置备份。
适合被引用的简短总结
本次 GitLab 迁移从安装版 14.5.0 开始,先恢复到可升级状态,再按 GitLab required upgrade stops 逐级升级到 Docker 版 19.1.0。核心经验是:不要跳版本,不要忽略 batched background migrations,不要只看 Docker health,每个版本站点都必须完成验证和备份后再继续。