1. 先想清楚:自建 GitLab 仓库到底解决什么问题
很多人第一次听到"在 GitLab 上建一个自己的仓库",脑子里浮现的是一串命令行,其实真正的分水岭在动手之前:你到底是在官方托管站点上点一个 New Project,还是在自己的一台机器上把整套 GitLab 服务跑起来。这两件事的技术含量差了整整一个数量级,踩坑位置也完全不一样。我做过后一种,也帮别人收拾过前者留下的烂摊子,所以这篇东西会偏向自建场景,顺带把托管场景里那些容易忽略的按钮选项讲透。
仓库这个词在开发日常里其实被混用了。Git 仓库指的是一个带版本历史的代码目录;GitLab 仓库是托管平台上的一个项目实体,里面装着 Git 仓库、议题、合并请求、流水线、制品等一堆附属物;而 Docker 仓库、Maven 仓库指的是存储构建产物的地方。这三者在真实项目里经常被一起提到,配置的时候也容易串味。这篇内容的主线是第二类,也就是 GitLab 项目仓库的建立,同时会在后面专门拿出一节,把制品仓库和代码仓库的边界讲清楚,免得你把该放 Nexus 的东西硬塞进 Git 里。
适合读这篇的人大致有三类。一类是团队里被指派"搞个内网代码托管"的人,手里有一台服务器和一点 Docker 基础,但从没装过 GitLab;一类是刚进公司、需要在新环境里建自己的仓库并推第一版代码的新人,卡在 SSH 密钥或者权限上;还有一类是已经用着 GitLab,但仓库越建越乱、分支命名五花八门、备份从来没做过的人。三类人的关注点不同,我会尽量把每一步都拆到能照着敲的程度,同时把"为什么要这么做"讲明白。
一个基础判断先摆在前面:如果你的仓库里只有公开的学习代码,托管服务完全够用,注册账号、点几下鼠标、推上去就完事,自建纯属给自己找活干。真正需要自建的情况通常是这几条中的一条或多条:代码不能出内网、团队规模大到需要统一账号体系、需要跟内部的其他系统做深度集成、或者网络出口不稳定导致拉取经常失败。判断清楚了再往下走,能省掉大量返工。
1.1 托管服务与自建部署的取舍逻辑
托管服务最大的优势不是免费,而是你不用管升级、不用管磁盘、不用管证书过期。它的代价在于可定制性受限,以及数据物理位置不在自己手里。自建则完全反过来:所有东西都在你掌控之中,但运维责任也一并落到你头上——备份、升级、容量、安全补丁,全是你的活。
我在做选型的时候习惯列一张责任清单,把"谁负责什么"写清楚。这张表看起来很笨,但它能有效阻止那种"装完了就没人管"的经典局面。
| 维度 | 使用托管服务 | 自建部署 |
|---|---|---|
| 初始成本 | 几乎为零 | 一台服务器加半天到两天时间 |
| 持续运维 | 平台方承担 | 自己承担升级、备份、监控 |
| 数据位置 | 平台机房 | 自己的机房或云主机 |
| 定制能力 | 受平台功能限制 | 可改配置、可接内部系统 |
| 网络依赖 | 依赖公网质量 | 内网访问,速度快且稳定 |
| 适合规模 | 个人、小团队 | 中大型团队、合规要求高的场景 |
实际决策中还有一个容易被忽略的变量:人数增长曲线。三五个人用托管很舒服,三十个人开始出现权限管理需求,三百个人的时候账号生命周期管理、审计日志、单点登录这些事会变成硬需求,而这些恰好是自建方案的强项。所以如果你的团队在一年内可能翻几倍,自建的早期投入是划算的。
1.2 三种部署形态的适用场景对照
确定自建之后,装法也有讲究,主流就三条路:容器化部署、系统包直接安装、离线环境的手工部署。它们不是新旧关系,而是适配不同约束条件。
容器化部署是我最推荐的入门方式。镜像封装好了所有依赖,升级就是换个 tag 重启,回滚同理,配置文件集中在一个 gitlab.rb 里,出问题好定位。它的短板在于对宿主机内核版本有要求,且容器与宿主机的资源共享关系需要理解,比如内存限制设得太死会导致服务被系统直接干掉。
系统包安装(也就是官方提供的 Omnibus 包)在物理机上更常见,好处是跟系统服务管理集成得自然,开机自启、日志轮转这些不用额外操心。坏处是升级路径有时候需要按版本逐个跳,跨大版本升级容易出岔子。
离线部署主要出现在内网隔离环境。做法是提前把安装包和所有依赖下载好,用本地源的方式安装,过程中不能有联网动作。这种场景下最容易翻车的地方是时间同步和证书,装完之后 clone 一直报错,查半天发现是服务器时间偏差太大导致令牌校验失败。
三种方式的共同点是:GitLab 是个相当吃资源的常驻服务,无论怎么装,预留的资源都要给足,后面会具体算。
1.3 账号、分组、权限的前置规划
我见过太多团队把 GitLab 装好之后,所有人用 root 账号干活,仓库全建在 root 名下。等到需要交接或者有人离职的时候,才发现根本理不清哪些仓库跟谁有关。这个问题在项目启动的第一天解决成本几乎为零,拖到半年后就要动大手术。
推荐的起点是这样:root 账号只用于管理,日常开发另建个人账号,然后在顶层建一个或几个 Group,项目全部挂在 Group 下而不是个人命名空间下。这样做的好处很直接——人员变动时只需要调整组成员,仓库本身的所有权不用动,URL 也不会因为用户名变化而失效。
权限层次上,GitLab 预设了 Guest、Reporter、Developer、Maintainer、Owner 五档。给权限的原则是够用就好:只读代码的人给 Reporter,日常提交的给 Developer,负责发布和分支保护的给 Maintainer,Owner 一般只留给团队负责人。这个划分听着啰嗦,但它能防住"手滑删掉主分支"这类事故。
Group 还有一层嵌套的能力,可以按部门建父组、按项目建子组,权限在父组层面统一授一次,子组自动继承。团队规模超过二十人的时候,这套结构能省掉大量重复配置。
2. 服务器准备与 GitLab 部署实操
服务器这一关是自建路上淘汰率最高的环节,绝大多数失败案例都源于资源给得不够。GitLab 是个 Rails 应用加一堆后台服务(PostgreSQL、Redis、Gitaly、Sidekiq、Nginx)的组合体,启动完成后常驻内存占用就不低,编译资源、跑流水线的时候还会再往上冲。
2.1 资源规格怎么算才不踩坑
内存是最关键的一项。官方给出的最低门槛是 4GB,但那是"能起来"的标准,不是"能用"的标准。我自己的经验值是这样:纯代码托管、十几个人用,8GB 内存是舒适线;如果要在上面跑 CI 流水线,尤其是构建 Docker 镜像,建议直接上 16GB。另外 Swap 一定要开,2GB 到 4GB 都行,它能在内存峰值时保命,避免 OOM Killer 把 PostgreSQL 直接杀掉。
CPU 方面,2 核能跑,但页面响应会明显迟钝;4 核是性价比较高的选择。GitLab 的很多操作是单线程瓶颈,堆更多核不一定线性提升,4 到 8 核之间比较合理。
磁盘要算得细一点。系统加 GitLab 本体大约占 10GB 到 15GB;代码仓库本身的体积取决于项目,纯文本代码通常很小,但如果仓库里混进了二进制文件、设计稿、数据集,体积会迅速膨胀。我的算法是:预留 50GB 起步,然后按"代码总量乘以 3"来估算实际占用,因为 Git 是完整保存历史的,加上 GitLab 的制品、日志、备份,实际占用通常是原始代码的好几倍。另外强烈建议上 SSD,GitLab 的随机读写非常密集,机械盘上的体验会让人怀疑人生。
还有一项容易忘:备份空间。备份文件默认放在数据目录下,如果不做清理,日积月累能把磁盘吃干净。建议把备份直接往外部存储或者另一台机器上推。
2.2 Docker 方式部署 GitLab 完整流程
下面这套流程我用了很多次,稳定可靠。前提是服务器上已经装好 Docker 和 Docker Compose,并且宿主机的 80、443、22 端口没有被占用(22 端口如果被 SSH 占用,就把 GitLab 的 SSH 映射到别的端口,比如 2222)。
先建目录,把配置、日志、数据三块分开挂载,这样升级的时候直接换容器就行,数据不受影响:
sudo mkdir -p /srv/gitlab/config /srv/gitlab/logs /srv/gitlab/data然后用 docker compose 管理,比一长串 docker run 好维护得多:
services: gitlab: image: gitlab/gitlab-ce:17.4.1-ce.0 container_name: gitlab restart: always hostname: git.example.com environment: TZ: Asia/Shanghai GITLAB_OMNIBUS_CONFIG: | external_url 'https://git.example.com' gitlab_rails['gitlab_shell_ssh_port'] = 2222 gitlab_rails['time_zone'] = 'Asia/Shanghai' nginx['redirect_http_to_https'] = true letsencrypt['enable'] = false puma['worker_processes'] = 2 postgresql['shared_buffers'] = '1GB' ports: - "80:80" - "443:443" - "2222:22" volumes: - /srv/gitlab/config:/etc/gitlab - /srv/gitlab/logs:/var/log/gitlab - /srv/gitlab/data:/var/opt/gitlab shm_size: "256m"这里有几个参数值得单独说明。external_url决定 GitLab 生成的所有链接和 clone 地址,必须在第一次启动前就定好,中途改会带来一堆麻烦。gitlab_shell_ssh_port是给页面显示的 SSH 地址打补丁的,如果不设,页面上给出的 clone 地址会带着容器的 22 端口,而宿主机的 22 被 SSH 占着,用户照着复制就会连不上。shm_size关系到 PostgreSQL 的共享内存,默认 64MB 偏小,设成 256m 能减少一些偶发的数据库报错。puma['worker_processes']控制应用进程数,内存吃紧的机器上把它调小能省下几百 MB。
启动命令很简单:
docker compose up -d docker compose logs -f gitlab启动过程会持续三五分钟,期间 Nginx、PostgreSQL、Gitaly 依次就绪。看到日志里出现 "gitlab Reconfigured" 之类的字样,基本就算完成了。这个过程千万别中途Ctrl+C,重新配置会被打断,留下半成品状态。
2.3 首次登录与管理员初始化
服务起来之后,第一件事是拿初始密码。GitLab 会在首次启动时生成一个随机 root 密码,写在容器内的文件里:
docker exec -it gitlab grep 'Password:' /etc/gitlab/initial_root_password这个文件有个坑:它会在 24 小时后被自动删除。所以拿到密码第一件事就是登录改掉,改完之后把新密码存进团队的密码管理工具,而不是留在某个聊天记录里。
登录之后建议立刻做几件事。第一,关掉公开注册。管理后台的"设置 - 通用 - 注册限制"里可以关闭开放注册,改为管理员审核或仅邀请,这一步能挡掉大量扫描注册的机器人。第二,配置发信服务。GitLab 默认不发邮件,导致密码找回、通知、合并请求提醒全部失效,体验会差一大截;配好 SMTP 之后这些功能才完整。第三,调整默认的项目可见性。默认是 Private 还是 Internal 要根据团队习惯定,企业内部一般选 Private,避免有人手滑把内部代码建成公开项目。
还有一个界面上的细节:进入管理后台之后,先看一眼"概览"里的组件状态,确认各个服务都是绿色。如果有服务显示异常,多半是资源不够或者端口冲突,这时候排查比用起来之后再排查容易得多。
2.4 让 clone 地址显示域名而不是机器 ID
这个问题的表现形式是:项目页面上显示的 clone 地址形如http://a1b2c3d4e5f6/group/project.git,中间那串是容器 ID 或者主机名,别人复制去用根本连不上。
根因在于 GitLab 不知道自己对外的正式地址是什么。它生成 URL 时优先使用配置里的external_url,如果这个值没设或者设成了主机名,页面就会把内部标识暴露出来。解决办法就是在部署时把external_url写成完整的对外域名,比如https://git.example.com,改完之后执行:
docker exec -it gitlab gitlab-ctl reconfigure如果前面挂了反向代理,还要多做两步。一是确认代理转发时带上了正确的主机头,二是在 GitLab 配置里打开对代理头的信任:
nginx['listen_port'] = 80 nginx['listen_https'] = false nginx['proxy_set_header'] = { "Host" => "$http_host", "X-Forwarded-Proto" => "https", "X-Forwarded-Ssl" => "on" } gitlab_rails['trusted_proxies'] = ['172.16.0.0/12']改完同样要 reconfigure。做完这几步,页面上的 clone 地址就会稳定显示成你的域名,HTTP 和 SSH 两种方式都会同步更新。
3. 创建你的第一个仓库并完成代码推送
服务跑起来、管理员账号能用之后,建仓库这件事本身只要一分钟,但里面有几个选项会长期影响使用体验,值得逐个看。
3.1 页面上新建仓库时那几个选项逐个拆
点新建项目之后,GitLab 会问你是从空白建、从模板建,还是导入已有仓库。空白项目自不必说;模板适合快速起一个带 README 和基础流水线配置的项目;导入适合从别的平台迁移,后面单独讲。
接着是命名空间的选择,这一步是重点。选择个人命名空间还是某个 Group,决定了仓库的 URL 前缀和权限继承关系。前面提过,团队项目一律挂 Group 下,个人试验性质的可以放自己名下。
项目路径(Project slug)会自动根据名称生成,建议手动确认一下,因为路径一旦确定就很难改,改了之后所有本地仓库的 remote 地址都会失效。命名习惯上我推荐用小写加连字符,比如order-service,避免空格、大写和特殊字符,可以减少跨系统集成时的转义麻烦。
可见性三档是 Private、Internal、Public。Internal 的含义是"任何登录用户可见",在企业内网环境里这是个容易被误用的选项——你以为它是私有的,实际上全公司只要有账号就能看到。真正需要限制就选 Private,然后在项目成员里逐个加人。
初始化选项里有一个"用 README 初始化仓库"。如果你本地已经有一个带历史的项目要推上去,就别勾,否则远端先有一个提交,本地推的时候会因为历史不一致被拒。反过来,如果你想直接在页面上传几个文件,那勾上更省事。
3.2 SSH 密钥配置与排错
用 SSH 方式访问仓库比 HTTPS 舒服得多,不用反复输账号密码,也不用折腾令牌。配置过程本身很简单,坑都在细节里。
先生成密钥对,推荐用 ed25519,比 RSA 更短更安全:
ssh-keygen -t ed25519 -C "your.name@example.com"一路回车,默认落在~/.ssh/id_ed25519和.pub两个文件。然后把.pub里的内容整段复制,粘到 GitLab 的"用户设置 - SSH 密钥"里。注意是 pub 那个,不是私钥,这一点每年都有无数人搞错。
端口不是 22 的情况下(比如我们前面映射到 2222),需要在~/.ssh/config里加一段:
Host git.example.com HostName git.example.com Port 2222 User git IdentityFile ~/.ssh/id_ed25519 IdentitiesOnly yes配完先用这条命令验证:
ssh -T git@git.example.com返回一句带你用户名的欢迎语就说明通了。如果报Permission denied (publickey),按这个顺序查:密钥有没有粘错、ssh-add -l看 agent 里有没有加载、文件权限是不是 600、config 里的 Host 别名有没有和实际使用的一致。我遇到最多的情况是服务器换了新密钥但本地 agent 里还缓存着旧的那把,ssh-add -D清一下再重新加就好。
3.3 本地仓库从零推到远端
远端仓库建好、SSH 通了之后,本地这段流程就是标准动作。假设你本地已经有一个写好的项目目录:
cd your-project git init git add . git commit -m "chore: 初始化项目" git branch -M main git remote add origin git@git.example.com:group/your-project.git git push -u origin main这里有几个细节值得说。git branch -M main是把默认分支名统一成 main,过去默认是 master,现在两边混用的团队很多,统一一下能省掉后续的混乱。-u参数把本地 main 和远端 main 关联起来,之后直接git push就行。
如果你本地已经有一段时间的提交历史,推送前先看一眼git log,确认没有把不该提交的东西写进去。文件体积也很关键,超过 100MB 的单个文件在推送时会被拒绝,而且一旦进了历史,即使后来删掉,仓库体积也不会自动缩小,清理起来相当麻烦。初始化的时候就把.gitignore写对,比事后补救轻松十倍。
从别的地方复制过来的项目,还可能带着旧仓库的.git目录。这种情况要么直接复用它的历史,要么删掉.git重新 init。想保留部分历史又想去掉某些敏感提交,就得上git filter-repo这类工具,属于进阶操作,不在第一次建仓的范围内。
3.4 第一次推送失败的高频原因
第一次推送失败太正常了,我把遇到过的原因整理成一张表,按出现频率排序:
| 报错信息关键字 | 常见原因 | 处理办法 |
|---|---|---|
| Permission denied (publickey) | 公钥没上传或 agent 没加载 | 重新粘贴公钥,ssh-add加载私钥 |
| remote: HTTP Basic: Access denied | HTTPS 方式用了账号密码 | 改用访问令牌或个人访问令牌 |
| ! [rejected] main -> main (fetch first) | 远端已有提交,历史不一致 | 先git pull --rebase再推 |
| fatal: remote origin already exists | 重复添加远端 | git remote set-url origin 新地址 |
| RPC failed; HTTP 413 | 反代请求体限制过小 | 调大代理的 body 尺寸或改用 SSH |
| Please make sure you have the correct access rights | 项目路径写错或没权限 | 核对 URL,确认自己是项目成员 |
| login failed. check api token or gitlab version | 令牌权限不足或版本不兼容 | 重新生成带 api 范围的令牌 |
最后那条尤其值得展开。这个报错常见于 IDE 插件或者第三方工具连接 GitLab 的场景,本质是工具在调 API 的时候带了无效令牌,或者令牌缺少必要的 scope(比如只有 read_repository 却要执行写操作)。排查方法是先确认令牌还在有效期内、再确认勾选的权限范围够用、最后确认工具版本对 API v4 的支持没问题。老版本的插件有时候会用已经废弃的接口,这种情况只能升级工具。
还有一个环境相关的坑:某些网络环境对 HTTP 大请求做了限制,推送一个稍大的仓库就会中断。这种时候用 SSH 协议往往能绕开,因为限制通常施加在 HTTP 层。判断方法很简单,同一个小仓库用 HTTPS 能推、用 HTTPS 推大仓库失败、换 SSH 成功,基本就能确认。
4. 分支模型、合并请求与团队协作规范
仓库建起来只是开始,真正决定长期可维护性的是分支怎么切、代码怎么合。这一块没有唯一正确答案,但有几个组合在实践中反复被证明有效。
4.1 一个够用又不折腾的分支模型
小团队最忌讳上重型流程。我的建议是从一个极简模型起步,随着团队扩大再逐步加规则。
主干分支只有一个,叫 main,始终保持随时可发布的状態。功能开发在 feature 分支上做,命名用feature/简短描述的形式,比如feature/order-export。修复线上问题用hotfix/前缀,发布准备用release/前缀。前缀的作用是让分支列表一眼能看出用途,尤其在分支数量上百之后,这个习惯能救命。
分支的生命周期要短。一个功能分支从创建到合并最好控制在一周以内,超过两周的分支会开始积累冲突,合并成本急剧上升。我在实践中会定期清理已经合并的远端分支,GitLab 支持在合并请求里勾选"合并后删除源分支",打开这个开关能自动完成大部分清理工作。
命名上还有一个细节:用连字符而不是下划线或空格。因为分支名会出现在 URL 里,也会出现在命令行里,连字符在所有这些环境里都不需要转义。
4.2 保护分支与合并请求的必填项
main 分支一定要设为受保护分支,这是整个协作规范里性价比最高的一条规则。保护之后,普通开发者不能直接推送,只能通过合并请求提交,从机制上杜绝了误操作。
保护分支的配置项里有几个开关值得细看。"允许合并"决定谁能点合并按钮,一般设为 Maintainer;"允许推送"建议关掉,改成"不允许任何人直接推送",包括管理员也走合并请求流程,这样审计记录才完整。"允许强制推送"必须关闭,否则保护形同虚设。
合并请求本身也有必填项可以配置。我一般会打开三条:至少一个人批准、所有讨论必须解决、流水线必须通过。第一条防止自己批自己,第二条防止带着未解决的评审意见合并,第三条防止把编译不过的代码合进主干。
合并方式上有三种选择:普通合并会保留完整历史,合并提交会多一个节点;快进合并保持线性历史,看着干净但丢失了分支信息;压缩合并把整个分支的提交压成一个,适合那种提交信息很随意的场景。我们团队的默认是压缩合并,因为大家本地提交往往很零碎,压成一个语义完整的提交后再进主干,历史读起来舒服很多。
4.3 标签、里程碑与仓库内文档
标签(Tag)用于标记发布节点,配合语义化版本号,比如v1.2.0。打标签的时候建议附上注释,用git tag -a v1.2.0 -m "发布说明"的形式,这样标签对象里会保存打标签的人和说明,比轻量标签信息更完整。
里程碑适合管理有明确时间点的版本目标,把相关的议题和合并请求挂上去,进度一目了然。这个功能很多人没用起来,其实对"这个版本要做哪些事"这种问题,它给出的答案比翻聊天记录可靠得多。
仓库内的文档从第一天就建。README 说清楚项目是干什么的、怎么跑起来;CONTRIBUTING 说清楚分支怎么起、提交信息怎么写;CHANGELOG 记录每次发布的变更。这三份文档加起来可能就几百字,但它能省掉后来者大量的问人时间。写文档这件事有个规律:越早写越省事,因为项目还小的时候你脑子里什么都清楚,写起来很快;等半年后你自己都记不清某个配置为什么那样设了。
5. 仓库日常维护:备份、迁移与安全加固
仓库不是建完就完事的,它跟所有有状态的服务一样,需要定期照看。这一章讲的三件事——备份、迁移、升级——我在实际项目里都遇到过因为没做而导致严重后果的情况。
5.1 备份策略与恢复演练
GitLab 的备份分两部分,很多人只知道第一部分。第一部分是数据备份,用这条命令:
docker exec -it gitlab gitlab-backup create生成的备份包默认在/var/opt/gitlab/backups下,里面包含数据库、仓库、制品等。第二部分是配置文件,也就是/etc/gitlab/gitlab.rb和/etc/gitlab/gitlab-secrets.json。这两个文件必须单独备份,因为密钥文件里存着数据库加密密钥、CI 变量密钥这些内容,缺了它,数据恢复了也解不开。
恢复演练比备份本身更重要。我见过备份脚本跑了两年从没验证过,真出事的时候才发现备份包是空的——原因是备份任务和某个维护窗口撞车,进程被中断了。演练的频率建议每季度一次,在测试环境上实际恢复一遍,确认能起来、能登录、仓库内容完整。
备份的存放位置也有讲究。默认路径在数据目录里,磁盘一旦写满,备份和线上数据一起遭殃。建议改到独立磁盘或者远端存储:
gitlab_rails['backup_path'] = "/mnt/backup/gitlab" gitlab_rails['backup_keep_time'] = 604800backup_keep_time单位是秒,604800 就是保留七天。加上定时任务自动清理,能避免备份把自己的磁盘撑爆。
5.2 从外部仓库迁移项目
迁移的需求很常见,比如团队从别的平台整体搬迁过来。GitLab 提供了按 URL 导入的功能,在新建项目时选择"导入项目",填入源仓库地址和凭证,它会自动把代码、议题、合并请求一起拉过来。
实际做的时候有几个坑。一是源平台的 API 速率限制,仓库多的时候要分批导入,不然会被限流中断。二是议题和评论里的用户映射,源平台的用户在新平台可能不存在,导入后会显示成一串匿名标识,需要事后手工对应。三是大仓库的导入非常慢,一个几 GB 的仓库可能要跑几个小时,中途不要刷新页面。
如果源平台不支持标准导入接口,退而求其次的做法是纯 Git 迁移:在源端git clone --mirror,然后在新端推送。这样能完整保留所有分支和标签,但议题、合并请求这些元数据就带不走了,只适合纯代码迁移的场景。
还有一种情况是从自建的旧服务迁移过来,旧服务可能在另一台机器上。做法是先停掉旧服务的写入,做一次全量同步,然后切换 DNS 或者修改客户端的 remote 地址。切换前一定要在所有开发者的机器上确认一遍 remote 地址已经更新,否则会有人在旧地址上继续推代码,造成分叉。
5.3 版本升级与安全补丁的跟进节奏
GitLab 的版本节奏很快,每个月一个小版本,这带来一个现实问题:不升级会积累安全风险,升级太频繁又会消耗运维精力。
我的做法是制定一个固定的升级窗口,比如每季度一次,跟随当前稳定分支的小版本前进。升级前必做三件事:完整备份、查阅官方的升级路径说明、在测试环境先跑一遍。升级路径特别重要,跨大版本有时候需要按顺序逐级升,直接跳过去会导致数据库迁移失败。
小版本的补丁发布通常包含安全修复,这类更新建议及时跟进。关注官方的安全公告渠道,看到涉及自己使用版本的通知就评估影响范围。评估维度包括:漏洞是否需要认证才能触发、是否需要特定配置才会命中、内部网络是否已经隔离了外部访问。综合判断之后再决定是立即升级还是等下一个窗口。
升级本身的操作很简单,容器方式就是改 compose 文件里的镜像 tag,然后docker compose up -d:
docker compose pull docker compose up -d docker exec -it gitlab gitlab-ctl reconfigure重新配置的过程可能持续几分钟,期间服务不可用,最好安排在低峰期。跑完之后检查一遍组件状态和日志,确认没有报错再收工。
5.4 容量治理与历史仓库瘦身
仓库体积是慢慢长起来的,等到磁盘告警的时候通常已经麻烦了。日常可以关注这几项占用:Git 仓库本体、CI 产生的制品、容器镜像缓存、日志文件。
制品是最容易失控的一块。流水线每次运行都可能产生编译产物、测试报告、打包文件,默认保留时间很长。在项目设置里可以配置制品和流水线的过期时间,比如保留最近 30 天,超过的自动清理。对于已经积累的大量历史制品,可以用清理命令批量处理:
docker exec -it gitlab gitlab-rake gitlab:cleanup:orphan_job_artifact_files日志方面,GitLab 的日志文件增长也不容小觑,尤其是访问日志。在配置里开启日志轮转和保留天数限制,能省下不少空间。
至于 Git 仓库本体的瘦身,属于比较重的操作。如果历史里混进了大文件,常规做法是用git filter-repo重写历史,然后强制推送。这个操作会改变所有提交的哈希,所有协作者都必须重新克隆,所以一定要提前通知到位,并且选在大家都不提交的时间段做。做完之后在服务端触发一次垃圾回收,空间才会真正释放:
docker exec -it gitlab gitlab-rake gitlab:housekeeping6. 把 CI/CD 和镜像仓库串起来
代码托管只是 GitLab 的一半价值,另一半在于它内置的流水线能力。仓库建好之后,很自然的下一步就是让提交自动触发构建和部署。
6.1 Runner 注册的两种方式
流水线要跑起来,得有 Runner 执行任务。Runner 可以装在项目级别、组级别,也可以作为整个实例的共享 Runner。我的建议是:通用能力(比如构建基础镜像)用共享 Runner,特殊环境(比如需要访问内部网络的部署任务)用项目专属 Runner。
容器方式装 Runner 很方便:
docker run -d --name gitlab-runner --restart always \ -v /srv/gitlab-runner/config:/etc/gitlab-runner \ -v /var/run/docker.sock:/var/run/docker.sock \ gitlab/gitlab-runner:v17.4.0注册命令需要从 GitLab 界面上拿注册令牌,路径在"管理后台 - CI/CD - Runner"或者具体项目的设置里:
docker exec -it gitlab-runner gitlab-runner register \ --url https://git.example.com \ --token glrt-xxxxxxxxxxxx \ --executor docker \ --docker-image alpine:latest \ --description "docker-runner"--executor docker表示每个任务跑在一个临时容器里,隔离性好,环境干净。代价是每次都要拉镜像,首次执行会慢一些。如果宿主机上有现成的构建环境,也可以选 shell 执行器,速度更快但隔离性差,容易受宿主机环境影响。
注册完成后,在界面上能看到 Runner 处于在线状态,标签(tag)可以用来精确调度任务。给 Runner 打标签是个好习惯,比如docker-builder、deploy-prod,在流水线里指定标签,避免任务被随机调度到不具备条件的机器上。
6.2 构建镜像并推送到私有镜像仓库
下面这份配置是我在多个项目里用过的模板,作用是构建镜像并推送到内部镜像仓库:
stages: - build - deploy variables: IMAGE_TAG: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA build_image: stage: build image: docker:24.0 services: - docker:24.0-dind variables: DOCKER_TLS_CERTDIR: "/certs" script: - docker login -u "$HARBOR_USER" -p "$HARBOR_PASS" registry.example.com - docker build -t registry.example.com/team/app:$CI_COMMIT_SHORT_SHA . - docker push registry.example.com/team/app:$CI_COMMIT_SHORT_SHA only: - main - tags deploy_prod: stage: deploy image: bitnami/kubectl:latest script: - kubectl set image deployment/app app=registry.example.com/team/app:$CI_COMMIT_SHORT_SHA when: manual only: - main几点说明。$HARBOR_USER和$HARBOR_PASS是放在项目 CI/CD 变量里的,不要写进文件。变量可以设置为"受保护",这样就只有受保护分支的流水线才能读到,安全性更好。DOCKER_TLS_CERTDIR是 docker-in-docker 的必要配置,不设的话客户端和服务端通信会失败。when: manual让部署任务变成手动触发,给线上变更留一道确认环节。
用提交的短哈希作为镜像标签,好处是每个构建产物唯一且可追溯,出问题的时候能精确定位到某次提交。同时可以再打一个latest标签方便本地测试,但生产环境请始终用具体标签,不要用 latest。
6.3 依赖仓库与制品仓库的分工
这一节解决一个常见混淆:代码仓库和制品仓库到底各管什么。
Git 擅长管文本,它对每一行变更都做增量存储和差异计算,这是它的强项。但二进制文件、依赖包、构建产物属于大块头且几乎每次都在整体变化,放进 Git 会让仓库体积迅速膨胀,而且拉取速度直线下降。所以这些东西应该放在制品仓库里,Git 仓库里只保留引用它们的配置文件。
常见组合是这样的:源码放在 GitLab;编译出的二进制包、Docker 镜像放在私有制品仓库(如 Nexus、Harbor 等);依赖的三方库通过私服代理缓存,比如 Maven 项目在settings.xml里配置镜像,把中央仓库的请求转发到内部私服,既加速又可控:
<mirror> <id>internal-repo</id> <mirrorOf>central</mirrorOf> <name>internal mirror</name> <url>https://repo.example.com/repository/maven-public/</url> </mirror>私服还可以配置多个上游仓库的代理顺序,当某个上游不可达时自动回退到下一个,这对构建稳定性帮助很大。类似的机制在其他生态里也存在,比如前端包管理器的私有源配置、鸿蒙生态的包仓库配置,思路完全一致:内部缓存优先,外部兜底。
需要提醒的是,不要把制品仓库的地址和凭证硬编码进代码。放在环境变量或者 CI 变量里,本地开发用一份独立的配置,避免开发者的个人配置影响到流水线。
7. 常见问题速查与踩坑记录
前面几章零散提了不少坑,这一章把高频问题集中整理一遍,方便出问题时按图索骥。
7.1 连接与认证类问题
认证问题占了日常求助的一大半。除了前面表格里列的那些,还有几个场景值得单独说。
页面能打开但 clone 一直超时,通常是端口没通。GitLab 网页走 443,SSH 走另一个端口,很多防火墙只开了 443,导致 HTTPS 能拉、SSH 拉不动。排查顺序是先在本地telnet 域名 端口看通不通,再看服务器防火墙规则,最后确认容器端口映射是否正确。
还有一种情况是 HTTPS 方式推送时反复要求输入密码。这通常意味着服务端开启了双重认证,或者必须用访问令牌代替密码。正确做法是在"用户设置 - 访问令牌"里生成一个带write_repository范围的令牌,然后在提示密码时粘贴这个令牌。
浏览器层面也可能出问题。如果登录后莫名其妙被踢出、或者页面报 CSRF 错误,先检查系统时间是否准确,时间偏差过大时令牌校验会失败。这个问题在虚拟机和容器环境里特别常见。
7.2 部署与性能类问题
页面加载慢、推送超时、流水线卡住,这些多半跟资源有关。按这个顺序排查:先用docker stats看容器内存和 CPU 使用,再用free -h看宿主机整体情况,最后看 GitLab 自身的组件状态。
内存不足最典型的表现是 GitLab 运行一段时间后某个服务自动退出,日志里有 OOM 相关记录。这种情况下要么加内存,要么调小puma的进程数、sidekiq的并发数。调小并发会牺牲一些响应速度,但能换来稳定性,取舍看实际负载。
磁盘满的表现更直接,页面报 500,日志里一堆写入失败。这时候可以先做一次清理:清掉过期的流水线制品、清理日志、删除无用的大型附件,能快速腾出空间。长期方案还是要把备份和制品往外挪。
流水线卡住大部分情况是 Runner 的问题。先确认 Runner 在线,再看它的标签和任务的标签是否匹配,最后看 Runner 所在机器的资源是否被占满。如果用的是 docker 执行器,还要注意并发任务数,默认配置下同一个 Runner 可能同时跑多个任务,资源竞争会导致全部变慢。
7.3 一些没人写在文档里的经验
最后这部分是我自己踩出来的,官方文档里不会写,但实际很有用。
养成先建空仓库再推代码的习惯。很多人喜欢在页面上勾选"用 README 初始化",然后本地推的时候遇到历史冲突,再花时间解决。先建空仓库,本地推首次提交,流程最顺。
.gitignore要一次写全。我见过把node_modules、构建产物、本地配置文件全提交进去的项目,仓库体积几十兆,克隆要等半天。建仓的第一件事就是根据项目技术栈选一份完整的忽略规则,宁多勿少。
提交信息写清楚。这条听起来像废话,但在需要回溯问题的时候,一句"fix bug"和一句"修复订单导出时金额字段精度丢失"的价值差距是巨大的。团队里可以在合并请求模板里加一条提醒,慢慢就形成习惯了。
给仓库加个描述和头像。项目多了之后,列表页里一堆名字相似的仓库,有个描述和图标能省很多辨认时间。这个成本极低,收益却很持续。
定期看一眼管理后台的审计日志。异常登录、权限变更、大量克隆,这些行为在日志里都有记录。平时不看,出事的时候连追溯的线索都没有。设置一个每周花五分钟扫一遍的习惯,性价比很高。
我个人在这件事上最大的体会是:仓库的规范程度几乎完全取决于建仓那一个小时的决策。命名、分组、权限、忽略规则、分支保护,这些在项目刚开始时设置只需要几分钟,等项目跑起来再补,就要协调所有人配合,成本高得离谱。所以如果你正准备建自己的第一个 GitLab 仓库,不妨在点下"创建"之前多花十分钟,把命名空间、可见性、保护规则这三件事想清楚,后面会省掉非常多的麻烦。