从 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

每个版本站点都遵循同一套原则:

  1. 当前版本必须健康。
  2. gitlab:check 必须通过。
  3. Web 登录页必须返回 HTTP 200
  4. 后台迁移必须全部完成。
  5. 完成当前版本备份后才能升级下一站。
  6. 不使用 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.rbgitlab-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,每个版本站点都必须完成验证和备份后再继续。


参考资料