GitLab 私有化部署实战 —— 从 Omnibus 到 Docker 的踩坑之旅

GitLab 私有化部署实战 ------ 从 Omnibus 到 Docker 的踩坑之旅

时间 :2026-08-31 至 2026-09-01

服务器 :Ubuntu 22.04.5 LTS(Jammy) / Intel Xeon E5-2620 v3 × 2 / 16 GB / /data 800 GB

目标 :5 人内部研发团队的代码托管平台

最终方案gitlab/gitlab-ce:19.3.1-ce.0 官方 Docker 单容器镜像 + Bind Mount 到 /data


0. 写在前面

这次部署看似简单("装个 GitLab 而已"),但实际踩到了几个深坑:

  1. GLIBC 跨版本兼容性 ------ 让 19.3.1 和 17.11.7 两个版本都装不了
  2. 国内网络限速 ------ 1.4 GB 的 deb 从 packages.gitlab.com 死活下不动
  3. 概念上的自我绊倒 ------ 我 + Claude 起初误判 Docker 路线违规,最终发现它反而最干净

本博客完整记录这三个坑,最终如何走到"一条命令启容器"的稳定状态。


1. 需求与硬约束

1.1 业务侧

  • 团队规模:5 名开发人员
  • 用途:内部代码托管、Merge Request、Issue、CI/CD
  • 后续计划:接入 DeepSeek API 做 AI Code Review(不在本次部署范围

1.2 部署侧

内核 OS Ubuntu 22.04 LTS(无法升级到 24.04)
服务器等级 Intel Xeon × 2 / 12C24T / 16 GB RAM / 4 GB Swap
/ 容量 295 GB(LVM,11 GB 已用)
数据 /data 801 GB(关键:业务数据要求落这里)
网络 192.168.1.100/24(RFC1918 内网)/ 无公网域名 / 无 SSL 证书
已装服务 Docker 29.7.2(data-root: /data/docker
GitLab 用途 CE 版
版本 要求 19.3 分支的最新 patch(当时为 19.3.1)

1.3 我自己设的硬规则

部署前我对自己提了若干约束(事后看其中有些太僵):

  • 用 Omnibus,不源码编译
  • ❌ 不许"为了方便把 PG/Redis 拆成一堆 Docker 容器"
  • ❌ 不许用第三方镜像
  • ❌ 不擅自装 Kubernetes / Runner / AI Gateway
  • ⚠️ 所有状态数据落 /data,根目录只放只读的 Omnibus 安装包

事后证明第 2 条太严 ------ "不拆服务"≠"不用 Docker",用 GitLab 官方 Omnibus 单容器镜像并不违反这条约束。详见第 6 节。


2. 第一版方案:Omnibus Linux Package

我用 Claude 规划了一个 8 步脚本 /tmp/gitlab-setup.sh,核心设计:

bash 复制代码
external_url 'http://192.168.1.100'
data_dir '/data/gitlab'                              # 把所有增长数据迁过去
gitlab_rails['gitlab_shell_ssh_port'] = 22           # 复用主机 sshd
gitlab_rails['signup_enabled'] = false               # 关闭公开注册
gitlab_rails['project_default_visibility_level'] = 0 # 项目默认私有
registry['enable'] = false                           # 不要 Container Registry

/tmp/gitlab-setup.sh 大致逻辑:

  1. apt update
  2. apt install openssh-server curl ca-certificates tzdata perl
  3. EXTERNAL_URL=http://192.168.1.100 apt-get install -y /tmp/gitlab-ce_19.3.1-ce.0_amd64.deb
  4. gitlab-ctl stop
  5. 备份默认 gitlab.rb → 替换成我们的版本
  6. mv /var/opt/gitlab /data/gitlab(迁数据)
  7. gitlab-ctl reconfigure

这是教科书式的 GitLab 升级到自定义数据目录的标准做法。


3. 第一道坎:GLIBC_2.38

执行到第 3 步时 dpkg 抛错:

复制代码
Setting up gitlab-ce (19.3.1-ce.0) ...
/opt/gitlab/embedded/bin/ruby:
  /lib/x86_64-linux-gnu/libm.so.6:
  version `GLIBC_2.38' not found
  (required by /opt/gitlab/embedded/lib/libruby.so.3.3)
/opt/gitlab/embedded/bin/ruby:
  /lib/x86_64-linux-gnu/libc.so.6:
  version `GLIBC_2.38' not found
  (required by /opt/gitlab/embedded/lib/libruby.so.3.3)
dpkg: error processing package gitlab-ce (--configure):
 installed gitlab-ce package post-installation script subprocess returned error exit status 1

立刻验证:

bash 复制代码
$ ldd --version
ldd (Ubuntu GLIBC 2.35-0ubuntu3.14) 2.35

$ objdump -T /opt/gitlab/embedded/lib/libruby.so.3.3 | grep GLIBC_2.38 | head
GLIBC_2.38  __isoc23_strtoll
GLIBC_2.38  __isoc23_strtol
GLIBC_2.38  fmod
GLIBC_2.38  strlcat
GLIBC_2.38  strlcpy

Ubuntu 22.04(Jammy)的系统 glibc 是 2.35 ,而 GitLab 19.3.1 里嵌入式 Ruby 3.3 的二进制链接的是 GLIBC_2.38

3.1 为什么不能绕过

GLIBC 符号是运行时解析 ,不能用 LD_LIBRARY_PATH 替;不能用静态链接绕;也不能用容器里的 chroot 替。只有两个出路

  1. 升级操作系统到 Ubuntu 24.04(提供 glibc 2.39)------ 跨大版本升级有风险,要重启
  2. 用一个内置 glibc 已经够老的 GitLab 版本 ------ 我先试这条路

3.2 半装状态的处理

gitlab-ce 包处于 install ok half-configured,需要:

bash 复制代码
sudo dpkg --remove --force-remove-reinstreq gitlab-ce
sudo apt-get purge -y gitlab-ce
sudo rm -rf /etc/gitlab /var/opt/gitlab /var/log/gitlab /opt/gitlab
sudo apt-get autoremove -y

4. 第二道坎:降级 GitLab 也装不上

我让 Claude 帮我列出 jammy 仓库所有 gitlab-ce 版本:

bash 复制代码
curl -s "https://packages.gitlab.com/gitlab/gitlab-ce/ubuntu/dists/jammy/main/binary-amd64/Packages.gz" \
  | gunzip | awk -v RS= '/^Package: gitlab-ce/' | grep '^Version:' \
  | awk '{print $2}' | sort -V | uniq | wc -l
398

398 个版本,从 15.5.0 一直到 19.3.1。我从高到低选了 17.11.7(最后一个 17.x LTS patch),本机下载验证 GLIBC:

bash 复制代码
$ dpkg-deb -x gitlab-ce_17.11.7-ce.0_amd64.deb /tmp/g17x/extracted
$ for so in /tmp/g17x/extracted/opt/gitlab/embedded/lib/libruby.so.3.2.5 \
            /tmp/g17x/extracted/opt/gitlab/embedded/bin/redis-server \
            /tmp/g17x/extracted/opt/gitlab/embedded/bin/postgres; do
    refs=$(objdump -T "$so" 2>/dev/null \
      | grep -oE 'GLIBC_[0-9]+\.[0-9]+' | sort -V | uniq -c | tr '\n' ' ')
    printf "  %-30s  %s\n" "$(basename $so)" "$refs"
  done
  libruby.so.3.2.5                       1 GLIBC_2.35   5 GLIBC_2.38
  redis-server                          18 GLIBC_2.34   6 GLIBC_2.38
  postgres                              12 GLIBC_2.34   8 GLIBC_2.38

17.11.7 也需要 GLIBC_2.38 。GitLab 在 17.x 中后期切到了 Ruby 3.2 + 新 glibc,远比 19.x 切得更早。换言之,17.11.x 以下的某个版本才是 Ubuntu 22.04 能装的最后窗口,但精确版本号只能继续反复下载 deb 试探。

4.1 网络瓶颈

更糟的是,中国大陆出口到 storage.googleapis.compackages.gitlab.com 的实际存储后端)非常不稳定:

bash 复制代码
$ curl -L -o /dev/null --max-time 60 ...gitlab-ce_17.11.7-ce.0_amd64.deb
... 600+ MB downloaded, then EOF or indefinite stall

阿里云 / Tuna / USTC 等国内镜像没有同步 packages.gitlab.com 的 Debian pool。每次下载都重 1+ GB,反复试探候选版本非常不现实。


5. 决策点:升级系统 vs 退版本 vs Docker

我有三个选项:

方案 优势 劣势
A. 系统升级到 Ubuntu 24.04 装回 19.3.1 需 do-release-upgrade,要重启,2 GB /boot 紧贴边需要 autopurge
B. 继续退版本 17.5.x / 17.3.x 不动 OS 反复下载 1+ GB 防 GLIBC,国内网络反复超时
C. 转 Docker Omnibus 单容器镜像 镜像内自带 Ubuntu 24.04 userspace,glibc 2.39;一次性解决 GLIBC;保留 Omnibus 全部能力 与最初的"Linux Package"计划偏离

最后我选了 C。回头看这是唯一干净的实现:

  • 镜像是 GitLab Inc 自家 publish,不是"第三方镜像"
  • 单镜像单容器运行完整 Omnibus:PG / Redis / Puma / Sidekiq / Gitaly / Nginx / Prometheus 全在容器内,零拆分
  • 容器内 glibc 是它自己的(2.39),与宿主机 OS 完全无关
  • 我的两个核心约束(全在 /data、保持 Omnibus)都满足 ------ 用 bind mount 把容器内 /var/opt/gitlab 挂到宿主机 /data/gitlab/data

6. Docker 部署实战

6.1 事前准备

主机上预创建卷:

bash 复制代码
sudo mkdir -p /data/gitlab/{config,data,logs}
sudo chmod 755 /data/gitlab/{config,data,logs}

6.2 拉镜像(中途翻车了 1 次)

bash 复制代码
sudo docker pull gitlab/gitlab-ce:19.3.1-ce.0

中途遇到 docker: short read: expected 161 bytes but got 0: unexpected EOF ------ GFW 截断,重试一次就成功了:

复制代码
19.3.1-ce.0: Pulling from gitlab/gitlab-ce
3dfcc7a2be79: Pull complete   # 之前那次下载的层
...
Digest: sha256:f63df4c43029fe91db370609c0b40a1e3585cebd06e3e9637d93a9a3030eb86e
Status: Downloaded newer image for gitlab/gitlab-ce:19.3.1-ce.0

6.3 启动容器

bash 复制代码
sudo docker run -d \
    --name gitlab \
    --hostname gitlab-server \
    --restart always \
    -p 80:80 -p 443:443 -p 2222:22 \
    -v /data/gitlab/config:/etc/gitlab \
    -v /data/gitlab/data:/var/opt/gitlab \
    -v /data/gitlab/logs:/var/log/gitlab \
    gitlab/gitlab-ce:19.3.1-ce.0

关键设计点

  • 2222:22:容器内 sshd 自管 SSH-git,宿主 SSH 仍占 22。运维登录走 22,git clone 走 2222。
  • 三个 -v 分别挂配置、数据、日志,全部落 /data
  • 容器 entrypoint 第一次启动会自动跑 gitlab-ctl reconfigure约 3--5 分钟

6.4 跟进状态

bash 复制代码
sudo docker ps --filter name=gitlab
# NAMES   STATUS                   PORTS
# gitlab  Up 4 minutes (healthy)   0.0.0.0:80->80, ... 0.0.0.0:2222->22/tcp

sudo docker exec gitlab gitlab-ctl status
# run: alertmanager: (pid 2694) ...
# run: gitaly: ...
# run: postgresql: ...
# run: puma: ...
# run: sshd: ...
# ... 14 个组件全 run

6.5 gitlab.rb 改造

原来 Linux Package 版的 gitlab.rb.new 不能 直接用:里面有一行 data_dir '/data/gitlab',在容器视角会把数据写到容器内存层(不映射回主机),重启即丢。SSH 端口也要改成 2222。

最终版(/data/gitlab/config/gitlab.rb):

ruby 复制代码
# 不再设 data_dir:让容器默认的 /var/opt/gitlab 走 bind mount
external_url 'http://192.168.1.100'
gitlab_rails['gitlab_shell_ssh_port'] = 2222
gitlab_rails['signup_enabled'] = false
gitlab_rails['project_default_visibility_level'] = 0
gitlab_rails['group_default_visibility_level']   = 0
gitlab_rails['backup_keep_time'] = 604800        # 7 天
registry['enable'] = false
registry_external_url nil
bash 复制代码
sudo cp /tmp/gitlab.rb.new /data/gitlab/config/gitlab.rb
sudo chmod 600 /data/gitlab/config/gitlab.rb
sudo docker exec -it gitlab gitlab-ctl reconfigure

7. 首次登录 + 邮箱修复

7.1 root 初始密码

容器已把密码写到 bind mount 那个文件:

bash 复制代码
sudo cat /data/gitlab/config/initial_root_password

这一份 24 小时后会自删。建议改 root 密码后再也不依赖这个文件

7.2 root 邮箱替换(绕过旧邮箱验证)

默认 root 邮箱是 gitlab_admin_db360f@example.com,改邮件时 web UI 要给 邮箱发验证码,但 example.com 是占位域。所以用 Rails 后台直接改:

bash 复制代码
sudo docker exec -it gitlab gitlab-rails runner '
  u = User.find_by(username: "root")
  u.email = "<您的实际邮箱>"
  u.skip_reconfirmation!
  u.save!
  puts "OK email=#{u.email}"
'

skip_reconfirmation! 跳过整个邮件验证流程。验证:

bash 复制代码
sudo docker exec -it gitlab gitlab-rails runner \
  'puts "root email = #{User.find_by(username: \"root\").email}"'
# root email = <您的实际邮箱>

刷一下 web UI,root 用户的 Edit profile 里 Email 应已显示新邮箱。


8. 踩坑清单与经验

8.1 GLIBC vs Debian 包的常见错觉

packages.gitlab.com/gitlab/gitlab-ce 仍然给 jammy 发 deb,看起来支持 Ubuntu 22.04。但 deb 的元数据层面 只声明 Depends: openssh-server, perl ------检查内嵌二进制的 glibc 版本。看起来"装得上"实际上 install hook 一行 glibc 检查就崩。

教训:

  • 装之前先 dpkg-deb -x 抽出内嵌二进制,验 objdump -T | grep GLIBC_2.xx别相信 deb 元数据
  • 跨大版本 GitLab 时,先确认系统 glibc:
bash 复制代码
ldd --version | head -1
系统 glibc
Ubuntu 20.04 focal 2.31
Ubuntu 22.04 jammy 2.35 ← 我们这台
Ubuntu 24.04 noble 2.39
GitLab 所需最低 glibc
17.5 及以下 2.31+
17.6 ~ 17.11 2.38+ ← 切点在这区间内
18.x / 19.x 2.38+

如果 GitLab 官方将来给出精确表格会更稳。

8.2 "Docker = 拆服务"的误判

我当时把这条规则过度使用了。正确的边界是:

只允许gitlab/gitlab-ce 单容器镜像(全 Omnibus)。

不允许postgresql + redis + nginx + gitlab-rails 拆成多个容器。

这两条在 GitLab 官方全是 supported deployment,但前者合规、后者违反我们的"集中管理"约束。

容器化后反而没有违反"Omnibus 集中管理"这条核心目标,因为它内部仍然是 Omnibus。

8.3 国内网络下的 deb 下载技巧

  • packages.gitlab.com 的实际文件在 storage.googleapis.com/packages-ops/,GFW 经常截大文件
  • 国内镜像(阿里 / Tuna / USTC)没有镜像这个仓库
  • apt 自带 HTTP 重试 + 长超时curl --max-time N 友好;实在不行就 dpkg -i ./x.deb 用本地文件

8.4 容器里的 gitlab.rb 与 host 的关系

容器版 gitlab.rb 与 Linux Package 版唯一差异:

维度 Linux Package Docker 容器
data_dir 必须配置到 /data/gitlab 不要设 (让容器默认 /var/opt/gitlab 走 bind mount)
gitlab_shell_ssh_port 通常是 22(与 sshd 共享) 设成宿主的 -p XXXX:22 中的 XXXX

其他完全一致。

8.5 Bind Mount 模式选择

我选了 bind mount-v /data/gitlab/...:/var/opt/gitlab),用 Docker volume。理由:

  • bind mount 的目录在 host 上直接可见 ,运维(lsducp、backup)方便
  • Docker volume 需要 docker volume inspect 才看得到,且跟随容器生命周期

bind mount 的代价是首次启动前要 mkdir,否则 Docker 会以 root 创建。

8.6 不在这次部署范围里的事

决策
AI Code Review(DeepSeek API) 延后,等基础平台稳定
GitLab Runner 延后,避免与 GitLab 主进程抢 16 GB 内存
Container Registry 关闭,需要时再启用
Kubernetes / KAS 多集群 不接,单实例足够
监控 / Prometheus 保留 Omnibus 默认(容器自动带了)

9. 最终架构

复制代码
┌─────────────────────────────────────────────────────────────┐
│  Host: gitlab-server  (Ubuntu 22.04 / 16 GB / LVM)            │
│                                                              │
│  ┌── sshd  :22  (host 原生, 运维登录)                       │
│  ├── nginx host firewall: iptables empty, no ufw             │
│  └── Docker daemon                                          │
│       │                                                      │
│       └── container "gitlab"  (gitlab/gitlab-ce:19.3.1)     │
│            ├── sshd  :22   (容器内 GitLab SSH)              │
│            ├── nginx  :80  (→ http://192.168.1.100)        │
│            ├── puma / sidekiq / gitaly / postgres / redis   │
│            └── /etc/gitlab  ← bind → /data/gitlab/config    │
│                 /var/opt/gitlab ← bind → /data/gitlab/data   │
│                 /var/log/gitlab  ← bind → /data/gitlab/logs │
└─────────────────────────────────────────────────────────────┘

9.1 数据布局(host 视角)

复制代码
/data/
├── docker/                  # 宿主机已有 Docker 数据
└── gitlab/                  # GitLab 卷
    ├── config/              # gitlab.rb, secrets, initial_root_password
    ├── data/                # PG, Redis, repositories, lfs, uploads, backups
    └── logs/                # 全组件日志

9.2 备份策略(下一步要做的)

  • 容器内 gitlab-rake gitlab:backup:create/data/gitlab/data/backups/
  • cron 每日 02:00 跑
  • gitlab_rails['backup_keep_time'] = 604800(保留 7 天)
  • 额外每周打包:
    • /etc/gitlab/gitlab.rb
    • /etc/gitlab/gitlab-secrets.json
    • /etc/gitlab/gitlab-registry-*-key.json(如果启用 registry)
    • /etc/gitlab/ssl/*(如果启用 TLS)

9.3 安全基线(已经做 + 接下来要补)

状态
signup_enabled = false ✅ 已配
项目 / Group 默认 visibility = private ✅ 已配
Container Registry 关闭 ✅ 已配
SSH 端口定制(2222) ✅ 已配
root 邮箱绑定到真实邮箱 ✅ 已改
用户 2FA 强制 ⏳ 进入 Admin Area 改设置
公开注册页面已禁 ⏳ 通过 UI 二次确认
密码策略(长度/复杂度) ⏳ 进入 Settings → General 加

10. 后续行动清单

  • 为 5 名成员创建用户(root → Admin Area → Users)
  • 强制全员 2FA
  • 写每日 02:00 backup cron
  • 在 web UI 创建第一个 Group + Project 跑通 clone / push / MR
  • 验证 SSH git 通道:ssh -T -p 2222 git@192.168.1.100
  • 评估是否需要给外部访问开个 Nginx 反向代理 + 真实域名

11. 教训汇总

  1. deb 元数据不验证 glibc 版本 ------ 装之前必须 objdump -T extracted/opt/gitlab/embedded/lib/libruby.so.3.x 看一眼
  2. 国内出口到 GCS 长期糟糕 ------ 大 deb 反复下载是实际痛点,Docker 镜像也一样要重试
  3. "用 Docker 拆服务"的边界要清楚 ------ 单容器 Omnibus 镜像不是拆分
  4. initial_root_password 是真·临时文件 ------ 24h 后自删,改造密码的功夫不能省
  5. 邮件验证的 bypass 路径要熟 ------ User.find_by(...).update(skip_reconfirmation!) 是 GitLab 运维基本功
  6. data_dir 在容器里是陷阱 ------ 不要设

至此,GitLab 19.3.1 在 Docker 上稳定运行,所有 14 个组件健康,HTTP 在 http://192.168.1.100 可访问,SSH Git 通过 2222 端口可达。Phase 5--8(数据目录验证、防火墙、备份 cron、终态报告)继续推进中。

相关推荐
深念Y28 分钟前
Docker 镜像体积从 110MB 降到 13MB 后端服务的瘦身
运维·docker·容器
虎王物联1 小时前
Docker容器安全加固:securityContext与seccomp在IoT边缘节点的隔离实践
物联网·安全·docker
YIAN1 小时前
从 Docker 容器操作到 TS 高级类型:前端开发者必备的两套核心工具全解
后端·docker
杨浦老苏2 小时前
群晖 Docker 部署 LAN-Sheriff:实时监控局域网设备对外连接
网络安全·docker·监控·群晖·网络监控
是那盏灯塔2 小时前
Swarm集群
docker
秦jh_2 小时前
【Docker】镜像仓库
docker·容器
IT大白鼠3 小时前
使用 Docker 部署 Kubernetes 集群:容器方式搭建 K8s 环境
docker·容器·kubernetes
leeyi1 天前
把软件装进不能上网的机房——一套建好了、还没上过战场的交付工程(第101篇)
docker·aigc·agent
YIAN1 天前
Docker + Nginx 核心原理扫盲:从环境隔离到反向代理,运维面试必考点
后端·docker·面试