1. 整体设计思路:为什么非要把数据库拆出来
先说个真实场景。某公司内部用GitLab管代码,一开始图省事,直接用官方镜像一把梭:
docker run -d \ --name gitlab \ --publish 8443:443 \ --publish 8080:80 \ --publish 8022:22 \ --restart always \ gitlab/gitlab-ce:latest数据全靠容器内部存储,所有代码仓库、数据库、配置文件全和容器生命周期绑在一起。跑了大半年,痛点全来了:
一是升级迁移极其痛苦。GitLab小版本升级频繁,每次都要先备份再升级再验证,一旦容器异常,GitLab自带Omnibus的PostgreSQL容易数据不一致,恢复起来费时费力。
二是容器重建成本高。有次服务器重启,Docker daemon异常退出,GitLab容器起不来,吓得赶紧查数据卷,好在当时已经把关键目录挂载出来了,不然代码仓库全没。
三是资源利用率差。GitLab的PostgreSQL和Redis默认与Web进程挤在一个容器里,内存分配互相干扰——GitLab本身很占内存(尤其是跑CI Runner的时候),数据库进程一起来,内存立刻告急。
在实践过程中,分离部署的价值就体现出来了:
- 数据库独立运维:PostgreSQL单独成容器,可以独立升级、独立备份、独立监控,不会因为GitLab应用层故障导致数据库连带挂掉。
- 应用层随意折腾:GitLab容器可以随时销毁重建,只要数据库和仓库数据卷还在,拉起来就是原样。
- 资源精细控制:数据库容器单独设置内存上限,避免和应用进程抢占资源。
- 架构清晰:后续想迁移到云数据库(比如托管版的PostgreSQL),应用层几乎不用改,只需要改连接串和初始化逻辑。
这里要明确一个概念:“数据分离”不等于“数据库分离”。前者是把GitLab的数据目录(/var/opt/gitlab)挂载到宿主机磁盘,后者是把GitLab应用和PostgreSQL数据库运行在两个独立容器中,甚至数据库放在独立主机上。本文讲的是后者——两个容器,各司其职。
2. 环境准备与架构规划
2.1 硬件资源评估:别等OOM了才后悔
GitLab对内存的胃口在代码托管工具里是出了名的。官方要求是4GB内存起步,但实际用下来,跑一个团队级别的GitLab,8GB内存才比较舒服。数据库分离部署之后,资源分配可以更精细一些,建议按下面的底子配置:
| 资源 | 最低配置 | 推荐配置 | 说明 |
|---|---|---|---|
| 内存 | 4GB | 8GB+ | PostgreSQL 2GB,GitLab 4GB,剩余给系统 |
| CPU | 2核 | 4核+ | 代码仓库多的时候,GitLab后台任务很吃CPU |
| 磁盘 | 50GB | 200GB+ SSD | 仓库数据增长很快,慢盘会拖累Git操作体验 |
| SWAP | 2GB | 4GB | 内存峰值时兜底,但别指望靠它扛大流量 |
之前踩过坑,在一台2核4G的云服务器上跑分离部署,Unicorn和Sidekiq两个进程内存一上来,PostgreSQL直接被挤到SWAP,结果就是Git操作超时、页面打开极慢。后来加了内存才解决,实在没法加的话,至少要限制GitLab的并发进程数。
2.2 Docker环境准备
以Ubuntu 22.04为例,Docker安装其实没什么新鲜的,但有几个细节值得留意:
# 更新系统包索引 sudo apt-get update # 安装依赖 sudo apt-get install -y \ ca-certificates \ curl \ gnupg \ lsb-release # 添加Docker官方GPG key sudo mkdir -p /etc/apt/keyrings curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg # 添加软件源 echo \ "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \ $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null # 安装Docker引擎 sudo apt-get update sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin注意:不要图省事省略GPG key导入步骤,否则apt update会报KeyError,很多人在这卡了半天。
装完Docker后,再把当前用户加进docker组,避免每次sudo:
sudo usermod -aG docker $USER newgrp docker顺便把Docker的日志轮转配置好,否则GitLab这种日志大户几个月就能写满磁盘:
{ "log-driver": "json-file", "log-opts": { "max-size": "20m", "max-file": "5" } }写入/etc/docker/daemon.json,重启Docker生效:
sudo systemctl restart docker2.3 目录规划:把关键数据全部落在宿主机
分离部署之前,先规划好目录结构。GitLab有三个关键数据目录:
/etc/gitlab:配置文件/var/opt/gitlab:核心数据,含仓库、数据库数据/var/log/gitlab:运行日志
虽然数据库单独拆出去了,但GitLab自身的仓库数据、备份文件还是要落盘。我的目录规划是:
/data/ ├── gitlab/ │ ├── config/ # GitLab配置文件(挂载到 /etc/gitlab) │ ├── data/ # GitLab数据目录(挂载到 /var/opt/gitlab) │ ├── logs/ # GitLab日志目录(挂载到 /var/log/gitlab) │ └── backup/ # 备份文件目录(挂载到 /var/opt/gitlab/backups) └── postgresql/ └── data/ # PostgreSQL数据目录(挂载到 /var/lib/postgresql/data)为什么要单独建一个backup目录?因为挂载到/var/opt/gitlab/backups之后,备份操作可以直接写到宿主机,不需要进容器找文件,后续做异地备份、定时任务都会方便很多。
目录权限也要注意,PostgreSQL的data目录需要给容器内的postgres用户写入权限。我一般直接用chown -R 999:999(PostgreSQL官方镜像默认UID是999),或者更稳妥的方式是启动容器后用--user参数指定,但新手建议直接用默认UID,省得出权限问题。
2.4 网络规划:固定IP还是端口映射?
GitLab需要暴露三个端口:
- 443:HTTPS访问
- 80:HTTP访问
- 22:SSH Git操作
端口映射方式有两种思路。一种是传统做法,直接把宿主机的端口映射到容器:
-p 8443:443 -p 8080:80 -p 8022:22但这样有个问题:用户访问GitLab时要带端口号,git clone时SSH也要写非标准端口,体验不太好。
另一种做法是用Docker NAT网络+宿主机端口转发。我实际操作中比较推荐创建一个自定义网络,然后用固定IP的方式让容器之间直接通信:
docker network create gitlab-net --subnet=172.20.0.0/16PostgreSQL容器分配固定IP,GitLab容器通过这个IP连接数据库。这样好处是:
- 容器重建后IP不会变(必须显式指定)
- 容器间通信不走端口映射,少一层NAT损耗
- 后续加Redis容器、Runner容器,都可以在同一个网络里自由通信
3. 数据库分离部署完整实操
3.1 版本选型:别用最新的,用最稳的
先明确版本搭配,这里直接说结论:
GitLab版本和PostgreSQL版本是绑定的。每个GitLab版本都有对应的PostgreSQL大版本要求。比如GitLab 16.x对应PostgreSQL 13/14,GitLab 15.x对应PostgreSQL 12/13,等等。
我去查阅了GitLab官方文档中的版本配套关系,整理成下表(基于常见实践的通用建议,具体以官方文档为准):
| GitLab大版本 | 推荐PostgreSQL版本 | 说明 |
|---|---|---|
| GitLab 16.x | PostgreSQL 13 或 14 | 新部署首选14 |
| GitLab 15.x | PostgreSQL 12 或 13 | 12还在维护,13比较稳 |
| GitLab 14.x | PostgreSQL 12 | 老版本可以用,但不推荐新装 |
选PostgreSQL版本时不要选最新的(当前最新的大版本),因为GitLab官方没有完整测试过最新PostgreSQL版本,可能存在兼容性问题。选GitLab大版本默认支持的版本就好,这也是官方Omnibus打包所用的版本。
另外,GitLab版本也建议选最新的CE稳定版,不要选RC版本。直接在Docker Hub上拉最新的gitlab/gitlab-ce:latest标签其实并不推荐,最好锁定一个具体版本,比如gitlab/gitlab-ce:16.11.3-ce.0,这样后续升级时可控性更强。
锁定版本的好处是:生产环境别追新,等出了两三个补丁版本再升,能省去很多因上游bug导致的修复时间。
3.2 部署独立PostgreSQL容器
PostgreSQL容器是这次改造的重头戏。我的做法是直接用官方的postgres:13镜像,不装任何额外工具,保持最小化。
创建数据目录并设置权限:
mkdir -p /data/postgresql/data chown -R 999:999 /data/postgresql/data然后启动PostgreSQL容器:
docker run -d \ --name gitlab-postgresql \ --network gitlab-net \ --ip 172.20.0.10 \ -e POSTGRES_PASSWORD=YourStrongPassword \ -e POSTGRES_DB=gitlabhq_production \ -e POSTGRES_USER=gitlab \ -v /data/postgresql/data:/var/lib/postgresql/data \ --restart always \ --memory 2g \ --cpus 2 \ postgres:13几个环境变量的作用:
POSTGRES_PASSWORD:数据库超级用户密码,这里我们顺手创建了gitlab用户,但实际是默认postgres用户的密码POSTGRES_DB:要创建的默认数据库名。GitLab连接数据库时默认使用的库名是gitlabhq_productionPOSTGRES_USER:默认创建的超级用户名。但实际部署中,我更建议用POSTGRES_USER=postgres,然后在初始化后单独为GitLab创建专用账号,避免GitLab运行时用超级用户权限
说道这里重点来了——GitLab官方对权限要求比较苛刻,它需要操作数据库的用户具备CREATEDB权限(用于创建测试数据库跑迁移),但不建议给超级用户权限。所以正确的做法是:
- 先以
postgres超级用户启动容器 - 容器启动后进入容器,创建GitLab专用用户和数据库
- 启动GitLab时使用这个专用账号
实际操作中,我喜欢在PostgreSQL容器启动后,执行以下初始化脚本(基于我部署时的通用实践):
docker exec -it gitlab-postgresql psql -U postgres -c "CREATE USER gitlab WITH CREATEDB SUPERUSER PASSWORD 'GitLabDBPassword';" docker exec -it gitlab-postgresql psql -U postgres -c "CREATE DATABASE gitlabhq_production OWNER gitlab;"那SUPERUSER要不要加?在GitLab官方文档中,GitLab要求数据库用户必须具备超级用户权限(否则部分迁移脚本无法执行)。实际测试下来,没有SUPERUSER权限时,GitLab初始化过程中会报Permission denied to create extension之类的错误,很多扩展(如pg_trgm)需要超级用户权限安装。所以这里必须加上SUPERUSER权限,但密码一定要设成强密码,仅限内网访问。
PostgreSQL容器启动时,还要注意时区问题。GitLab默认使用UTC时间,如果希望系统时间和本地时间一致,需要在PostgreSQL容器里设置时区:
-e TZ=Asia/Shanghai顺手一起加上,省得后面日志时间对不上。
3.3 部署GitLab容器并连接外部数据库
数据库容器跑起来之后,再来部署GitLab容器。这是整个流程中参数最多的环节。
先创建目录:
mkdir -p /data/gitlab/{config,data,logs,backup} chown -R 1000:1000 /data/gitlab然后启动GitLab容器:
docker run -d \ --name gitlab \ --network gitlab-net \ --ip 172.20.0.11 \ -p 443:443 \ -p 80:80 \ -p 22:22 \ -e GITLAB_OMNIBUS_CONFIG=" external_url 'http://gitlab.example.com'; gitlab_rails['db_adapter'] = 'postgresql'; gitlab_rails['db_encoding'] = 'unicode'; gitlab_rails['db_host'] = '172.20.0.10'; gitlab_rails['db_port'] = '5432'; gitlab_rails['db_username'] = 'gitlab'; gitlab_rails['db_password'] = 'GitLabDBPassword'; gitlab_rails['db_database'] = 'gitlabhq_production'; gitlab_rails['backup_path'] = '/var/opt/gitlab/backups'; postgresql['enable'] = false; redis['enable'] = false; " \ -v /data/gitlab/config:/etc/gitlab \ -v /data/gitlab/data:/var/opt/gitlab \ -v /data/gitlab/logs:/var/log/gitlab \ -v /data/gitlab/backup:/var/opt/gitlab/backups \ --shm-size 256m \ --restart always \ gitlab/gitlab-ce:16.11.3-ce.0这里有几个参数要重点解释,都是踩过坑才知道关键的:
postgresql['enable'] = false和redis['enable'] = false:这是分离部署的开关。GitLab的Omnibus默认会尝试启动自带的PostgreSQL和Redis,必须把这两个关掉,否则容器启动时会因为5432或6379端口冲突报错。
但注意:GitLab依赖Redis,官方Omnibus默认自带Redis,我们这里关掉Redis之后,GitLab内部是不是就无法工作了?这里要说明一下——GitLab的Redis是支撑Sidekiq队列、缓存和会话的核心组件,不能直接关掉。在分离部署实践中,Redis也需要单独部署或让GitLab继续用自带Redis。那上述配置里我写了redis['enable'] = false,这个前提下必须再准备一个独立Redis容器。为了避免范围太大,我们这里先聚焦“数据库分离”,Redis可以继续用GitLab容器内部自带的(删掉那行redis['enable'] = false,或者改为redis['enable'] = true),只把数据库分离出去。如果你要一并做Redis分离部署,那就再起一个Redis容器,设置redis['enable'] = false并配置gitlab_rails['redis_host']等相关参数。
实际上我之前做的一次完整分离包括Redis,但为了一个主题讲透,本文先把数据库分离做干净。若你的GitLab容器保留了内置Redis,那配置里就不要加redis['enable'] = false,也别加gitlab_rails['redis_host']。方向明确,数据库分离即可。
external_url:这是对外访问地址。如果用IP访问,就写http://你的服务器IP,如果有域名就写域名。这个地址写错会导致GitLab页面上的clone地址错误、OAuth回调失败。
--shm-size 256m:这个参数容易被忽略。GitLab的Nginx和Puma会用到/dev/shm,默认64MB可能不够,大文件提交时会出现奇怪的网络错误。加上这个参数能避免很多诡异的间歇性问题。
端口映射:443、80、22这三个端口正常情况下应该都映射出去。如果宿主机22端口已经被SSH占用了,就需要把宿主机的其他端口映射到容器的22(比如-p 8022:22),同时修改GitLab配置里的gitlab_rails['gitlab_shell_ssh_port']。但在专用于GitLab的机器上,我建议直接占22端口,否则团队成员clone仓库时都要写端口号,很烦。
启动命令执行完后,容器不会立刻可用。第一次启动过程中GitLab会做很多初始化工作:
- 生成密钥和证书
- 运行数据库迁移
- 初始化默认配置
这个过程在性能一般的机器上可能要3到5分钟。可以通过日志观察进度:
docker logs -f gitlab看到类似下面这样的日志,说明初始化完成,可以访问了:
GitLab configured successfully!3.4 初始化验证:确认数据库是真的分离了
容器起来后,别急着开心,先做几项验证,确保数据库真的连到了外部PostgreSQL,而不是还在用容器内自带的(虽然理论上关了自带就不会有这个问题,但操作失误时有发生)。
第一步,检查GitLab容器内是否有PostgreSQL进程:
docker exec gitlab ps aux | grep postgres如果返回空,说明GitLab确实没有启动内置PostgreSQL,符合预期。这里加一句,如果你在容器内看到了postgres相关进程,检查一下是不是监控工具或Sidekiq的子进程,因为正常情况下GitLab容器内部是不应该跑独立的PostgreSQL服务的。
第二步,在PostgreSQL容器里确认GitLab创建了哪些数据库:
docker exec gitlab-postgresql psql -U postgres -c "\l"正常会看到gitlabhq_production和gitlabhq_test(测试环境数据库)两个库,其中gitlabhq_test是迁移时创建的临时测试库。如果只看到一个,说明GitLab初始化可能还没跑完,等一会儿再看。
第三步,确认GitLab能正常操作数据库。进入PostgreSQL容器,切换到gitlabhq_production库,随便查一张表:
docker exec gitlab-postgresql psql -U gitlab -d gitlabhq_production -c "\dt"能列出表就说明连接没问题。常见的情况是Connection refused或密码错误,这时候按第五部分的排查思路去看。
第四步,浏览器访问http://服务器IP,设置管理员密码,首次登录后创建项目,上传一个测试文件,用git clone拉取下来,确认读写路径全通。
4. 运维细节:备份、恢复与版本升级
4.1 备份策略:两条腿走路,缺一不可
数据库分离之后,备份策略会变得更清晰,但也要更细心,因为应用和数据库的备份是两个独立动作,时间点要对齐。
GitLab官方提供了一套备份工具,运行在GitLab容器内:
docker exec -t gitlab gitlab-backup create这个命令会备份以下内容:
- Git仓库(裸仓库)
- 数据库快照(通过GitLab的backup工具dump)
- 上传的附件
- CI/CD流水线历史数据
备份文件默认生成在/var/opt/gitlab/backups目录下,也就是我们挂载到宿主机/data/gitlab/backup的目录。文件名格式是1692835200_2023_08_24_16.11.3_gitlab_backup.tar。
但这里有个大坑——GitLab的backup工具在执行时会同时备份它自己认识的那个数据库。数据库分离后,backup工具是不是还会尝试连接我们已经单独部署的PostgreSQL?实际上大家不必担心:GitLab backup会以GitLab配置中指定的数据库连接信息(db_host、db_username等)去连接外部PostgreSQL,然后执行数据库转储。只要gitlab_rails['db_*']配置正确,backup工具就能正常从外部数据库备份数据。不会再去连一个“自带数据库”,因为内置数据库已经被禁用了。
所以备份操作在GitLab容器内一条命令搞定,但PostgreSQL容器自身我们还是要做一层物理备份,原因有两个:
- GitLab的backup工具备份的是逻辑数据,某些情况下(比如表损坏)导出的也可能是不完整的数据
- 数据库容器本身如果损坏,光有逻辑备份还不够,恢复时要先重建数据库环境,再做恢复,耗时更长
所以我的策略是双保险:
- GitLab层:每天执行
gitlab-backup create,生成完整备份tar包 - PostgreSQL层:每天执行
pg_dump或pg_basebackup,生成数据库级备份
PostgreSQL物理备份脚本(写入cron定时任务):
#!/bin/bash # /usr/local/bin/pg_backup.sh DATE=$(date +%Y%m%d_%H%M%S) BACKUP_DIR=/data/backups/postgresql mkdir -p $BACKUP_DIR docker exec gitlab-postgresql pg_dump -U gitlab -d gitlabhq_production -F c -f /tmp/gitlab_${DATE}.dump docker cp gitlab-postgresql:/tmp/gitlab_${DATE}.dump $BACKUP_DIR/ docker exec gitlab-postgresql rm -f /tmp/gitlab_${DATE}.dump # 清理30天以前的备份 find $BACKUP_DIR -name "*.dump" -mtime +30 -delete关于备份时间点:GitLab backup和pg_dump要错开执行,避免两者同时在数据库上跑大查询相互影响。建议GitLab backup在凌晨2点,pg_dump在凌晨4点。
4.2 恢复实操:从零到一完整还原
恢复操作比备份复杂得多,尤其是数据库分离之后,顺序不对就是各种报错。
灾难场景:GitLab服务器系统盘损坏,需要在新机器上恢复服务。
操作步骤如下:
第一步,部署基础环境。重复第二部分的Docker安装和目录创建。
第二步,启动PostgreSQL容器(不要启动GitLab),用备份数据还原:
docker run -d \ --name gitlab-postgresql \ --network gitlab-net \ --ip 172.20.0.10 \ -e POSTGRES_PASSWORD=YourStrongPassword \ -v /data/postgresql/data:/var/lib/postgresql/data \ --restart always \ postgres:13如果/data/postgresql/data里已经有完整数据文件(物理备份方式恢复),那么容器起来数据就在。但如果是用pg_dump的逻辑备份恢复,方法如下:
# 先创建数据库和用户 docker exec -it gitlab-postgresql psql -U postgres -c "CREATE USER gitlab WITH CREATEDB SUPERUSER PASSWORD 'GitLabDBPassword';" docker exec -it gitlab-postgresql psql -U postgres -c "CREATE DATABASE gitlabhq_production OWNER gitlab;" # 从备份文件恢复 docker exec -i gitlab-postgresql psql -U gitlab -d gitlabhq_production < /data/backups/postgresql/gitlab_20240101_030000.dump第三步,启动GitLab容器,但先不要访问。立即执行GitLab备份恢复:
docker exec -it gitlab gitlab-backup restore BACKUP=1692835200_2023_08_24_16.11.3这里需要说明的是,执行gitlab-backup restore时,GitLab要求服务处于停止状态才能恢复。但我们容器方式部署其实不需要太严格,只要保证没有外部请求在写数据即可。实操中我会先把GitLab容器停一下:
docker stop gitlab docker start gitlab # 让容器内部服务都起不来?不对,这样又会自动全部启动这里有个细节:docker stop gitlab会停止容器内的所有进程。再docker start gitlab之后,容器内所有Omnibus服务(包括web、sidekiq等)都会自动启动,执行restore时正好冲突。
正确做法是:
docker exec -it gitlab gitlab-ctl stop puma docker exec -it gitlab gitlab-ctl stop sidekiq docker exec -it gitlab gitlab-backup restore BACKUP=1692835200_2023_08_24_16.11.3 docker exec -it gitlab gitlab-ctl startgitlab-ctl stop puma和stop sidekiq停掉Web服务和后台任务队列,让数据库不再有任何写入。然后恢复备份,最后再启动服务。
恢复完成后,还需要恢复配置文件、密钥等。/etc/gitlab目录里的gitlab-secrets.json尤其重要——如果这个文件丢失,即使数据恢复了,用户密码、2FA验证、CI/CD变量都会失效。所以你必须在平时就把整个/data/gitlab/config目录备份好,最好也放进定时任务里。
4.3 版本升级:小步快跑,禁止跳版本
数据库分离部署之后,GitLab升级容易很多:因为PostgreSQL不用动,只需要升级GitLab容器。
但GitLab升级有个铁律:不能跨大版本跳跃式升级。比如从15.x直接升到16.x是有风险的,官方只保证相邻小版本的兼容性。实际操作中,我建议按以下步骤走:
- 备份当前版本(在GitLab容器内执行
gitlab-backup create) - 修改镜像标签为下一个版本号,重建容器
- 登录页面验证核心功能(登录、创项目、push/pull、跑CI)
- 确认没问题后再继续升下一版
比如升级路径:16.11.3→16.11.4→16.12.0→16.12.1,每个小版本间隔观察几天,确认稳定再升。
数据库分离部署对升级的额外好处是:升级失败的恢复成本极低。只需要把旧的GitLab镜像标签换回来,数据库数据完全不受影响,因为升级前后用的是同一个PostgreSQL。而以前一体化部署时,升级会连数据库结构一起改,回滚时要连数据库一起恢复,风险大增。
5. 常见问题与排查技巧实录
5.1 数据库连接失败:Connection refused
现象:GitLab容器启动后,日志疯狂报Failed to connect to database: Connection refused。
排查步骤:
- 确认PostgreSQL容器是否在运行:
docker ps | grep postgresql - 确认两个容器在同一个网络:
docker network inspect gitlab-net,看看172.20.0.10和172.20.0.11是否都在 - 确认GitLab容器内能ping通数据库容器IP
从实际经验来看,这个错误90%的情况是网络配置不一致导致的:要么PostgreSQL容器没有指定固定IP,导致重启后IP变了但GitLab配置里还写的是旧IP;要么两个容器不在同一个自定义网络里。
所以强烈建议:每个容器启动前,先规划好固定IP,写进启动命令里。我一般把网络规划写成一个笔记,贴在服务器终端附近,或者写进Makefile,防止时间久了忘记。
5.2 密码问题:认证失败
现象:GitLab日志报FATAL: password authentication failed for user "gitlab"。
排查步骤:
- 用同样的账号密码手动连一次数据库,确认密码是否正确:
docker exec -it gitlab-postgresql psql -U gitlab -d gitlabhq_production -h 127.0.0.1- 确认GitLab配置里的密码没有特殊字符被转义错误。比如密码里如果有
$、'、\,在GITLAB_OMNIBUS_CONFIG里转义不当就会被截断或解析错误
这里给个建议:数据库密码要么用纯字母数字,要么用环境变量文件传参。我踩过一次坑,密码里带了个#,YAML解析直接把后面的内容当成注释了,排查了整整一个下午。后来学乖了,数据库密码一律用a-zA-Z0-9,复杂度靠长度保证,比如G1tLab_Db_P@ss这种级别就够用。
5.3 内存虚高:GItLab容器老占4GB+内存
现象:刚部署完,docker stats一看,gitlab容器内存占用4GB+,怀疑是不是分离了个寂寞。
原因分析:GitLab的内存大头主要有:
Puma(Ruby Web服务器)— 默认会fork多个worker
Sidekiq(后台任务队列)— 常驻内存
Prometheus(监控系统)— 系统监控组件
分离部署数据库之后,省掉的是PostgreSQL的内存(约500MB-1GB),但GitLab应用层本身的内存消耗还是很可观的。
优化手段:
在GITLAB_OMNIBUS_CONFIG里加上:
puma['worker_processes'] = 2; puma['min_threads'] = 4; puma['max_threads'] = 8; sidekiq['max_concurrency'] = 5; prometheus_monitoring['enable'] = false;如果不做监控,直接把Prometheus关掉能省200MB内存。Puma的worker数建议等于CPU核心数,线程数看并发量,团队小的话4-8个线程完全够。
另外,PostgreSQL容器也建议加--memory 2g --memory-swap 2g限制,避免它抢占过多宿主内存。
5.4 备份文件太多,磁盘告急
现象:df -h一看磁盘使用率飙升,发现/data/gitlab/backup目录堆满了备份tar包。
原因:GitLab默认不做历史备份清理,每天一个tar包,每个几百MB到几GB,一个月就是几十GB。
解决方案:写清理脚本,或者直接在GitLab里配置自动清理。
我习惯用系统cron来做:
0 2 * * * docker exec -t gitlab gitlab-backup create >> /var/log/gitlab_backup.log 2>&1 30 3 * * * find /data/gitlab/backup -name "*.tar" -mtime +7 -delete保留7天基本上是均衡方案:既能应对“昨天改坏了代码今天想恢复”,又不会囤太多历史备份文件。如果想保留月度备份,可以在每周日额外保留一份,放到另一个目录。
5.5 Docker重启后容器未自动启动
现象:服务器重启后,GitLab和PostgreSQL容器都没起来。
原因:大概率是启动命令里没写--restart always,或者Docker服务本身没设置开机自启。
解决方案:
- 所有容器必须加上
--restart always - 确认Docker服务开机自启:
sudo systemctl enable docker
还要注意:PostgreSQL容器必须比GitLab容器先启动。虽然GitLab有自动重连机制,启动时连不上数据库会等一会儿重试,但最好还是通过docker的depends_on(如果用了docker-compose)或让PostgreSQL先启动来避免没必要的等待。
我之前没有用--restart always参数,结果一次机房断电后所有服务全部手动重启,白白浪费了一个上午。后来把启动命令全部改成带--restart always的版本,运维轻松很多。
5.6 问题排查速查表
| 问题现象 | 可能原因 | 快速排查命令 |
|---|---|---|
| 页面打不开 | Nginx未启动 / 端口被占用 | docker logs gitlab | grep nginx |
| Clone报错 | SSH端口映射错误 | ssh -p 22 git@你的IP测试连通 |
| 登录后500错误 | 数据库连接异常 / secrets文件丢失 | docker logs gitlab | grep ERROR |
| 磁盘暴涨 | 日志未轮转 / 备份未清理 | du -sh /data/gitlab/logs /data/gitlab/backup |
| CI Runner注册失败 | 令牌错误 / 网络不通 | docker exec gitlab gitlab-rails runner "puts Gitlab::CurrentSettings.current_application_settings.runners_registration_token" |
6. 升级到docker-compose管理:换个姿势更省心
如果你觉得上面一长串docker run命令太啰嗦,强烈建议把分离部署迁到docker-compose管理。配置文件即代码,启动一次搞定,不会忘参数。而且升级时只需要改镜像标签,比docker run命令重建容器更清晰。
下面是一个实践过的compose配置,可以直接参考(这个配置同时分离了PostgreSQL,Redis还是继续用GitLab内置的):
version: '3.8' services: postgresql: image: postgres:13 container_name: gitlab-postgresql restart: always networks: gitlab-net: ipv4_address: 172.20.0.10 environment: POSTGRES_PASSWORD: YourStrongPassword POSTGRES_DB: gitlabhq_production POSTGRES_USER: gitlab TZ: Asia/Shanghai volumes: - /data/postgresql/data:/var/lib/postgresql/data mem_limit: 2g cpus: 2 gitlab: image: gitlab/gitlab-ce:16.11.3-ce.0 container_name: gitlab restart: always depends_on: - postgresql networks: gitlab-net: ipv4_address: 172.20.0.11 ports: - "443:443" - "80:80" - "22:22" environment: GITLAB_OMNIBUS_CONFIG: | external_url 'http://gitlab.example.com' gitlab_rails['db_adapter'] = 'postgresql' gitlab_rails['db_encoding'] = 'unicode' gitlab_rails['db_host'] = '172.20.0.10' gitlab_rails['db_port'] = '5432' gitlab_rails['db_username'] = 'gitlab' gitlab_rails['db_password'] = 'GitLabDBPassword' gitlab_rails['db_database'] = 'gitlabhq_production' gitlab_rails['backup_path'] = '/var/opt/gitlab/backups' postgresql['enable'] = false puma['worker_processes'] = 2 puma['min_threads'] = 4 puma['max_threads'] = 8 volumes: - /data/gitlab/config:/etc/gitlab - /data/gitlab/data:/var/opt/gitlab - /data/gitlab/logs:/var/log/gitlab - /data/gitlab/backup:/var/opt/gitlab/backups shm_size: '256m' mem_limit: 4g networks: gitlab-net: driver: bridge ipam: config: - subnet: 172.20.0.0/16使用方式:
docker-compose up -d这个配置我把内存限制加上了,后端数据库2GB,GitLab 4GB,总共6GB,2GB留给系统。如果机器只有8GB内存,刚好够用。
补充一点:mem_limit是docker-compose v2/v3的写法,旧版是mem_limit: 4g也可以,不用纠结太多,能用就行。
7. 我对分离部署的几点实操心得
数据库分离这套方案改造完成也稳定运行一段时间了,总结几个我踩过坑之后才明白的道理:
第一,分离不是目的,可维护才是目的。如果你只有一个人用GitLab,那完全没必要做数据库分离,一体化部署反而更省心。但如果是团队级使用、有明确的备份恢复要求、或者打算后续迁移到云数据库,那分离部署就值得投入。
第二,PostgreSQL容器版本一旦定型,尽量别动。GitLab升级只需要拉新镜像,数据库版本升级相对繁琐(要先备份、再升PG、再验证兼容性),所以刚开始选PG版本时,可以稍微选老一档的稳定版本,这样GitLab升级到更高级版本时,PG版本还能在兼容范围内。
第三,固定IP方案比服务发现更可靠。虽然docker-compose里有服务名可以直接当hostname用,但我们在宿主机上还可能跑监控脚本、备份脚本、其他工具,这些脚本需要稳定访问数据库,固定IP比服务名更方便。这也是为什么我在本文中一路坚持用IP而不是容器名称。
第四,无论怎么折腾,定时备份任务一定要落地。部署形态可以随便换,但“每天有人看到备份成功的日志”才是运维的地基。我把备份脚本写进crontab之后,还加了一个健康检查:如果备份文件连续两天没有更新,钉钉机器人推送告警。这样即使备份命令失败了,也能第一时间发现。
这套架构后续想再加东西也方便:Prometheus监控加一个容器,Runner再加一个容器,都在同一个gitlab-net网络里互通,互不干扰。对我的团队来说,这套办法解决了之前GitLab数据库和应用互相拖累的问题,而且迁移成本极低——只要数据目录和配置文件还在,随时能把服务拉起来。