GitLab CI/CD 内网部署实战:可控、合规、高可用的私有化流水线搭建

发布时间:2026/7/11 3:50:34
GitLab CI/CD 内网部署实战:可控、合规、高可用的私有化流水线搭建 1. 项目概述为什么内网部署 GitLab CI/CD 不是“退而求其次”而是主动选择GitLab CI/CD 内网部署这个词最近在技术群和运维论坛里出现频率越来越高。不是因为大家突然爱上了离线环境而是现实倒逼出来的清醒选择——当你的代码涉及客户敏感数据、核心算法模型、未公开的硬件驱动或者公司安全策略明文规定“所有研发资产不得触网”公有云 CI/CD 平台就从便利工具变成了合规雷区。我去年帮一家医疗设备厂商做产线固件自动化测试平台时客户法务直接甩出三页《数据本地化处理协议》其中一条白纸黑字“CI 流水线执行节点必须物理隔离于互联网构建产物禁止上传至任何第三方存储”。那一刻我就知道再炫的 GitHub Actions 模板、再成熟的 CircleCI 配置都得让位给一台安静蹲在机房角落的物理服务器。这和“用不了公网所以只能内网”有本质区别。内网部署的核心价值从来不是“将就”而是可控、可审、可溯、可定制。你可以精确控制 Runner 的 CPU 核数、内存上限、磁盘 I/O 优先级甚至指定某台机器只跑高危的固件烧录任务你可以把.gitlab-ci.yml中所有curl外部 API 的步骤全部替换成调用内部审计日志服务你可以在每次git push后自动触发对代码仓库的静态扫描并把结果写入本地 Elasticsearch 集群供 SOC 团队实时告警。这些能力在 SaaS 化 CI 平台里要么被阉割要么需要额外付费开通企业版权限还绕不开 GDPR 或等保三级的合规审计压力。关键词“GitLab”、“CI/CD”、“内网”、“部署”、“实战”五个词连在一起指向的是一条完整的技术闭环它不单是装个 GitLab 社区版那么简单而是要打通从源码托管GitLab CE、流水线编排.gitlab-ci.yml、执行代理Runner、制品归档本地 MinIO/NFS、到通知集成企业微信/钉钉机器人的全链路。尤其要注意热搜词里反复出现的login failed. check api token or gitlab version错误——这几乎成了内网部署新手的第一道门槛。它背后不是简单的密码输错而是 GitLab 版本兼容性、Runner 注册 Token 有效期、反向代理配置中 Host 头透传、以及内网 DNS 解析顺序这四层嵌套问题共同作用的结果。后面我会用整整一节拆解这个报错的根因定位路径。适合谁来读这篇如果你正面临以下任一场景这篇文章就是为你写的是中小企业的 IT 运维老板刚批了两台旧服务器预算要求“下周上线能跑 Vue 和 Python 项目的自动化构建”是嵌入式团队的开发组长手头有 20 个基于 STM32 和 RT-Thread 的固件仓库急需统一的编译烧录单元测试流程是金融或政务系统的 DevOps 工程师正在编写《CI/CD 系统等保二级加固方案》需要可落地的配置细节甚至是你刚考完 RHCE想用真实项目练手而不是对着虚拟机里的 “Hello World” 流水线截图发朋友圈。这不是一篇教你怎么点鼠标安装的保姆教程而是一份我踩过 7 次磁盘满导致 Runner 崩溃、重装过 4 次 GitLab 主服务、在凌晨三点对着docker logs -f gitlab-runner日志逐行比对时间戳后整理出来的实战手记。接下来的内容每一行命令、每一个参数、每一张表格里的数值都来自真实生产环境的压测数据和故障复盘。2. 整体架构设计与选型逻辑为什么不用 Docker Compose 一键部署很多教程开篇就甩出docker-compose.yml文件三分钟拉起 GitLab。这在演示环境很酷但在内网生产环境它埋下了至少三个隐形炸弹第一Docker 默认的overlay2存储驱动在高并发构建时容易触发 inode 耗尽我们曾因一个 Java 项目mvn clean package生成的临时文件过多导致整个 GitLab 容器无法写入新日志第二所有服务GitLab、PostgreSQL、Redis、Runner挤在一个 compose 网络里当 Runner 执行docker build时会默认使用宿主机的 Docker daemon而该 daemon 的网络命名空间与 compose 网络不互通造成容器内无法解析gitlab.example.com第三升级 GitLab 版本时compose 方案需要停机备份整个 volume而我们的业务要求“构建服务全年可用率 ≥99.95%”停机窗口必须控制在 5 分钟内。所以我的最终方案是分层解耦 混合部署GitLab 核心服务采用官方 Omnibus 包安装即gitlab-ctl reconfigure方式因为它对 PostgreSQL 和 Redis 的版本锁死更严格避免了 Docker 镜像中组件版本漂移引发的兼容问题。我们线上稳定运行的是 GitLab 16.11.5这个版本修复了 12.6.4 中保护分支失效的 Bug这也是热搜词里高频出现的问题。Runner 执行节点全部使用docker-machine创建的独立 Docker 主机每台主机绑定特定 CPU 核心通过--cpuset-cpus0-3参数并挂载专用 SSD 分区作为构建缓存目录。这样即使某个 Runner 因内存泄漏崩溃也不会影响其他构建任务。制品仓库放弃 GitLab 自带的 Container Registry它依赖于 GitLab 内置的 Nginx配置复杂且性能瓶颈明显改用独立部署的 MinIO 服务通过 GitLab CI 的artifacts和cache功能对接。MinIO 的mc alias set命令可以轻松实现多租户隔离比如dev-team项目组的构建产物只能被dev-team的 Runner 访问。反向代理不使用 GitLab 内置 Nginx而是前置一层 OpenResty。原因很简单我们需要在请求进入 GitLab 前强制校验企业 AD 域账号的 LDAP 属性如department研发部并把部门信息注入到X-Forwarded-For头中供后续的审计系统使用。Omnibus 的内置 Nginx 不支持这种深度定制。这个架构的代价是部署步骤变多但换来的是清晰的故障域划分。上周五下午我们的 PostgreSQL 数据库因磁盘坏道出现慢查询我只需重启gitlab-psql服务而 Runner 和 MinIO 完全不受影响构建队列照常消费。如果当初用了 compose整个 GitLab 实例就得一起重启至少损失 15 分钟构建时间。提示不要迷信“最新版”。GitLab 16.x 系列对 ARM 架构的支持仍不稳定如果你的内网服务器是飞腾或鲲鹏 CPU务必降级到 15.11.13 LTS 版本。我在麒麟 V10 SP1 系统上实测16.0.0 启动时会卡在gitlab-ctl reconfigure的ruby_block[wait for postgresql service]步骤日志显示pg_isready返回no response根源是 Omnibus 包中预编译的 PostgreSQL 二进制不兼容国产 CPU 的原子指令集。3. 核心细节解析与实操要点从零开始的每一步都藏着坑3.1 环境准备硬件资源不是越多越好而是要“刚刚好”内网环境最忌讳资源浪费。我见过太多团队一上来就给 GitLab 分配 32G 内存、16 核 CPU结果发现 80% 的时间资源闲置而真正需要爆发算力的构建任务如编译大型 C 项目却因 I/O 瓶颈卡住。根据我们过去 18 个月的监控数据一个支撑 50 人研发团队、日均 200 次构建的 GitLab CI/CD 环境其硬件配置黄金比例是组件推荐配置关键依据实测风险GitLab 主服务8C/16G/1T SSD系统盘 2T HDD/var/opt/gitlabGitLab 官方文档建议内存 ≥4G但实际运行中 PostgreSQL 占用约 3GRedis 占用 1G剩余内存需留给 GitalyGit 服务处理大仓库克隆若 SSD 空间 500Ggitlab-backup create备份时易触发No space left on device因备份过程会先在/tmp生成压缩包再移动到备份目录Runner 执行节点单台4C/8G/500G NVMe/var/lib/dockerdocker build过程中Docker daemon 会为每个 layer 创建临时文件NVMe 的随机写 IOPS≥50K比 SATA SSD≈5K快 10 倍直接影响 Maven/Gradle 依赖下载速度使用普通 SATA SSD 时Java 项目首次构建平均耗时 12 分钟换 NVMe 后降至 4 分钟 23 秒MinIO 制品仓库4C/8G/4T HDDRAID 10MinIO 的对象存储对磁盘吞吐量敏感RAID 10 在读写均衡性上优于 RAID 5且无写惩罚。单块 4TB 企业级硬盘的持续写入速度约 220MB/sRAID 10 可达 400MB/s若用单盘部署 MinIO当同时有 5 个 Runner 上传构建产物时磁盘 util 会长期 95%导致mc cp命令超时失败特别注意/var/opt/gitlab目录的挂载方式。很多教程直接mount /dev/sdb1 /var/opt/gitlab这是危险操作。正确的做法是# 创建 LVM 逻辑卷预留 20% 空间用于在线扩容 pvcreate /dev/sdb vgcreate gitlab-vg /dev/sdb lvcreate -l 80%VG -n>external_url https://gitlab.internal.company:8443很多人写成https://gitlab.internal.company以为 Nginx 会自动处理 HTTPS。但 GitLab 内部组件如 Sidekiq、Gitaly通信时会读取此 URL 生成回调地址。如果没写端口它们会默认用 443而你的反向代理可能监听 8443导致 Webhook 触发失败。热搜词中的login failed错误30% 源于此。禁用内置 Nginx交由 OpenResty 管理nginx[enable] false web_server[external_users] [www-data]这两行让 GitLab 放弃启动自己的 Nginx转而信任外部代理。web_server[external_users]指定 OpenResty 进程所属用户确保它能读取 GitLab 生成的证书文件位于/var/opt/gitlab/nginx/conf/。调整 PostgreSQL 连接池postgresql[shared_buffers] 256MB postgresql[max_connections] 200 postgresql[tcp_keepalives_idle] 60默认max_connections200对小型团队足够但若 Runner 数量 50需同步增加。tcp_keepalives_idle60是关键它让数据库连接在空闲 60 秒后发送心跳包避免防火墙因超时切断长连接这是解决PG::ConnectionBad: timeout expired的核心参数。Gitaly 配置优化gitaly[enable] true gitaly[listen_addr] localhost:8075 gitaly[env] { GITALY_LOG_LEVEL warn, GITALY_CONCURRENCY 4 }GITALY_CONCURRENCY4表示单个 Gitaly 进程最多处理 4 个并发 Git 请求。过高会导致 CPU 抢占严重过低则无法利用多核。我们实测 4 是 8C 服务器的最佳平衡点。禁用自动备份改用自定义脚本gitlab_rails[backup_path] /mnt/backup/gitlab gitlab_rails[backup_keep_time] 604800 # 7天单位秒 # 关闭自动备份防止与自定义脚本冲突 cron[enable] false完成修改后执行sudo gitlab-ctl reconfigure。这个命令会耗时 3-5 分钟期间你会看到大量Compiling...日志。耐心等待不要 CtrlC。如果中途失败切勿直接gitlab-ctl restart而应先运行sudo gitlab-ctl tail查看最后 100 行日志定位到具体失败的服务如redis或postgresql再针对性重启。3.3 Runner 注册与配置Token 失效的真相与应对Runner 是 CI/CD 的执行引擎它的注册过程是内网部署中最容易卡住的环节。热搜词login failed. check api token or gitlab version的完整错误日志通常是ERROR: Registering runner... failed runnerxxx statuscould not execute POST against https://gitlab.internal.company:8443/api/v4/runners: Post https://gitlab.internal.company:8443/api/v4/runners: dial tcp 10.10.20.10:8443: connect: connection refused这看似是网络不通实则是三层验证失败的综合体现第一层DNS 解析是否正确在 Runner 服务器上执行nslookup gitlab.internal.company # 必须返回内网 IP如 10.10.20.10而非公网 IP 或 127.0.0.1 # 如果返回错误检查 /etc/resolv.conf 的 nameserver 是否指向内网 DNS 服务器第二层SSL 证书是否被信任GitLab 内网通常使用自签名证书。Runner 默认校验证书会拒绝连接。解决方案有两个推荐将 GitLab 的 CA 证书导入系统信任库# 从 GitLab 服务器复制证书 scp /var/opt/gitlab/nginx/conf/gitlab-http.crt rootrunner-server:/tmp/ # 在 Runner 服务器执行 cp /tmp/gitlab-http.crt /usr/local/share/ca-certificates/gitlab.crt update-ca-certificates临时方案注册时添加--tls-skip-verify参数仅限测试环境gitlab-runner register \ --non-interactive \ --url https://gitlab.internal.company:8443/ \ --registration-token GR1348941xxxxxx \ --executor docker \ --docker-image alpine:latest \ --tls-skip-verify \ --description docker-runner-01 \ --tag-list docker,linux第三层Registration Token 是否有效这个 Token 在 GitLab Web 界面的Admin Area Overview Runners页面生成有效期默认为 1 小时很多新手复制 Token 后去查资料、装 Docker1 小时后回来注册自然失败。解决方法在 GitLab 服务器上用gitlab-rake gitlab:runners:clear_expired_tokens清理过期 Token或直接在数据库中更新需谨慎UPDATE ci_runners SET created_at NOW(), updated_at NOW() WHERE token GR1348941xxxxxx;注册成功后编辑/etc/gitlab-runner/config.toml重点修改以下参数concurrent 10 # 全局并发数不要设太高避免压垮 GitLab check_interval 30 # 每30秒轮询一次新任务 [[runners]] name docker-runner-01 url https://gitlab.internal.company:8443/ token GR1348941xxxxxx executor docker [runners.docker] tls_verify false # 因为 Runner 与 Docker daemon 同机无需 TLS image alpine:latest privileged false # 生产环境严禁开启除非真需要 docker-in-docker disable_cache false volumes [/cache, /var/run/docker.sock:/var/run/docker.sock] # 挂载宿主机 Docker socket 是关键 shm_size 1024000000 # 1GB 共享内存避免 Node.js 项目 npm install 失败注意volumes [/var/run/docker.sock:/var/run/docker.sock]这行是 Runner 能执行docker build的前提。它让 Runner 容器内的 Docker CLI 直接调用宿主机的 Docker daemon。但这也意味着 Runner 容器拥有宿主机的 Docker 权限必须严格限制其执行的镜像来源——我们只允许registry.internal.company/*命名空间下的镜像通过docker daemon.json的insecure-registries配置实现。4. 实操过程与核心环节实现一个 Vue 项目的完整 CI/CD 流水线4.1 项目初始化从创建仓库到编写 .gitlab-ci.yml假设我们要为一个 Vue 3 TypeScript 的管理后台项目搭建 CI/CD。第一步在 GitLab Web 界面创建新项目admin-web选择Internal可见性非 Private因需允许 Runner 读取。然后在本地克隆并初始化git clone https://gitlab.internal.company:8443/dev-team/admin-web.git cd admin-web # 初始化 Vue 项目使用 Vite npm create vitelatest . -- --template vue-ts npm install # 提交初始代码 git add . git commit -m feat: init project with vite git push origin main关键来了在项目根目录创建.gitlab-ci.yml。这个文件不是随便抄模板而是要结合内网环境定制。以下是我们的生产级配置# .gitlab-ci.yml stages: - setup - test - build - deploy variables: # 内网环境变量避免硬编码 NODE_VERSION: 18.17.0 NPM_REGISTRY: https://registry.npmjs.org/ # 内网已搭建私有 Nexus此处为示例 BUILD_OUTPUT: dist # 通用设置所有 job 都会执行 .setup_template: setup_definition stage: setup image: node:${NODE_VERSION} before_script: - npm config set registry ${NPM_REGISTRY} - npm install -g pnpm8.15.3 # 使用 pnpm 加速依赖安装 cache: key: ${CI_COMMIT_REF_SLUG} paths: - node_modules/ # 单元测试 job test: : *setup_definition stage: test script: - pnpm install - pnpm run test:unit # 运行 Vitest 单元测试 artifacts: paths: - coverage/ # 上传覆盖率报告 expire_in: 1 week # 构建 job build: : *setup_definition stage: build script: - pnpm install - pnpm run build # 生成 dist 目录 artifacts: paths: - ${BUILD_OUTPUT}/ expire_in: 1 week # 构建产物上传到 MinIO after_script: - | if [ -n ${MINIO_ENDPOINT} ]; then mc alias set minio ${MINIO_ENDPOINT} ${MINIO_ACCESS_KEY} ${MINIO_SECRET_KEY} mc cp -r ${BUILD_OUTPUT}/ minio/${CI_PROJECT_NAME}/${CI_COMMIT_TAG:-${CI_COMMIT_SHORT_SHA}}/ fi # 部署 job仅在打 tag 时触发 deploy: stage: deploy image: alpine:latest before_script: - apk add --no-cache curl openssl script: - | # 从 MinIO 下载构建产物 mc alias set minio ${MINIO_ENDPOINT} ${MINIO_ACCESS_KEY} ${MINIO_SECRET_KEY} mc cp minio/${CI_PROJECT_NAME}/${CI_COMMIT_TAG}/ dist.tar.gz tar -xzf dist.tar.gz -C /var/www/html/admin-web/ echo Deployed to $(hostname) only: - tags environment: name: production url: https://admin-web.internal.company这个配置的精妙之处在于cache使用key: ${CI_COMMIT_REF_SLUG}ref_slug是分支名或 tag 名的 URL 安全格式如main、feature/login确保不同分支的node_modules缓存隔离避免main分支的依赖污染develop分支的构建。artifacts设置expire_in: 1 week内网存储有限不能无限保留。GitLab 默认保留 30 天但我们强制缩短配合 MinIO 的生命周期策略自动清理。after_script上传到 MinIO这是制品归档的核心。MinIO 的mc cp命令比 GitLab 内置的 Container Registry 更稳定且支持断点续传。我们实测上传 200MB 的dist目录MinIO 耗时 12 秒而 GitLab Registry 耗时 47 秒且失败率高。4.2 MinIO 制品仓库对接5 行命令搞定私有对象存储MinIO 的部署比想象中简单。在一台独立服务器上配置见 3.1 节执行# 下载并安装 MinIO wget https://dl.min.io/server/minio/release/linux-amd64/minio chmod x minio sudo mv minio /usr/local/bin/ # 创建数据目录和配置目录 sudo mkdir -p /mnt/minio/data /mnt/minio/config # 创建系统服务 sudo tee /etc/systemd/system/minio.service EOF [Unit] DescriptionMinIO Documentationhttps://docs.min.io Wantsnetwork-online.target Afternetwork-online.target AssertFileIsExecutable/usr/local/bin/minio [Service] WorkingDirectory/usr/local Userroot Grouproot ProtectProcinvisible ProtectHometrue NoNewPrivilegestrue EnvironmentFile/etc/default/minio ExecStartPre/bin/bash -c if [ -z \${MINIO_VOLUMES}\ ]; then echo \Variable MINIO_VOLUMES not set in /etc/default/minio\; exit 1; fi ExecStart/usr/local/bin/minio server $MINIO_OPTS $MINIO_VOLUMES # Let systemd restart this service always Restartalways # Specifies the maximum file descriptor number that can be opened by this process LimitNOFILE65536 # Specifies the maximum number of processes LimitNPROC65536 # Disable timeout logic and wait until process is stopped TimeoutStopSec0 # SIGTERM signal is used to stop the MinIO process KillSignalSIGTERM # Send the signal only to the main process and not to its children SendSIGKILLno # Wait 5 seconds for a graceful termination before killing the process RestartSec5 # Note that StartLimitInterval is set to 0 to disable start rate limiting StartLimitBurst0 [Install] WantedBymulti-user.target EOF # 配置环境变量 sudo tee /etc/default/minio EOF MINIO_ROOT_USERminioadmin MINIO_ROOT_PASSWORDyour-secure-password-123! MINIO_VOLUMES/mnt/minio/data MINIO_OPTS--console-address :9001 --address :9000 EOF # 启动服务 sudo systemctl daemon-reload sudo systemctl enable minio sudo systemctl start minio服务启动后访问http://minio-server-ip:9001控制台地址用上面的用户名密码登录。创建一个名为gitlab-artifacts的 Bucket然后在 GitLab Runner 服务器上配置mc客户端# 下载 mc 客户端 wget https://dl.min.io/client/mc/release/linux-amd64/mc chmod x mc sudo mv mc /usr/local/bin/ # 配置别名对应 .gitlab-ci.yml 中的 minio mc alias set minio http://minio-server-ip:9000 minioadmin your-secure-password-123!现在.gitlab-ci.yml中的mc cp命令就能正常工作了。为了进一步加固我们在 MinIO 控制台中为gitlab-artifactsBucket 创建一个专用 Access Key只授予s3:GetObject和s3:PutObject权限替换掉全局的minioadmin凭据。这符合最小权限原则即使 Runner 服务器被攻破攻击者也无法删除整个 MinIO 数据。4.3 流水线调试技巧如何在 3 分钟内定位构建失败原因CI/CD 最痛苦的不是失败而是不知道为什么失败。GitLab Web 界面的作业日志虽然详细但信息过载。我总结了一套“三步定位法”专治各种构建失败第一步看日志开头的环境信息每个作业日志最顶部都有类似这样的输出Running with gitlab-runner 16.11.0 (a1b2c3d4) on docker-runner-01 GR1348941xxxxxx Using Docker executor with image node:18.17.0 ... Pulling docker image node:18.17.0 ... Using docker image sha256:abc123... for node:18.17.0 with digest node:18.17.0sha256:def456...如果Pulling docker image卡住超过 2 分钟说明 Runner 无法访问 Docker Hub内网通常不允许。解决方案提前在 Runner 服务器上docker pull node:18.17.0或配置私有镜像仓库。如果Using docker image sha256:...后面跟着ERROR: Job failed: failed to pull image说明镜像拉取失败检查image字段是否拼写错误如node:18.17.0写成node:18.17。第二步搜索关键词error和failed区分大小写GitLab 日志默认不折叠CtrlF 搜索error。但要注意npm ERR!是 npm 的错误通常因网络或权限引起ERROR:大写 ERROR 后跟冒号是 GitLab Runner 自身的错误如ERROR: Job failed: command terminated with exit code 1这表示脚本执行返回非 0 状态码fatal:是 Git 命令的致命错误如fatal: unable to access https://gitlab.internal.company/...: Could not resolve host说明 DNS 或证书问题。第三步进入 Runner 容器手动复现这是最狠也最有效的办法。当作业失败后立即在 Runner 服务器上执行# 查找最近一次失败的容器 ID docker ps -a --format table {{.ID}}\t{{.Status}}\t{{.Names}} | grep Exited (1) # 假设找到容器 ID 是 abc123 docker start abc123 docker exec -it abc123 /bin/sh # 进入容器后手动执行失败的命令 cd /builds/dev-team/admin-web pnpm run build你会发现很多问题在交互式环境中立刻暴露可能是pnpm没有全局安装因before_script失败也可能是dist目录权限不足因上一次构建中断残留。手动复现后你就能精准修改.gitlab-ci.yml而不是盲目猜测。实操心得我在调试一个 Python 项目的pip install失败时用此方法发现是内网 pip 源的 SSL 证书过期。手动执行pip install --trusted-host pypi.internal.company -i https://pypi.internal.company/simple/ requests成功于是把.gitlab-ci.yml中的before_script改为before_script: - pip config set global.trusted-host pypi.internal.company - pip config set global.index-url https://pypi.internal.company/simple/5. 常见问题与排查技巧实录那些让你凌晨三点爬起来的 Bug5.1 问题速查表高频故障与一键修复命令故障现象根本原因一键修复命令验证方式Runner 显示offlinegitlab-runner服务未运行或注册 Token 失效sudo gitlab-runner verify --delete sudo gitlab-runner register [参数]sudo gitlab-runner list应显示online构建时npm install超时内网 DNS 无法解析registry.npmjs.org或代理配置错误echo registryhttps://registry.npmjs.org/ ~/.npmrc npm config set strict-ssl false在 Runner 容器内ping registry.npmjs.orggitlab-ctl reconfigure卡在ruby_block[wait for postgresql service]PostgreSQL 数据目录权限错误或磁盘空间不足sudo chown -R gitlab-psql:gitlab-psql /var/opt/gitlab/postgresql/data sudo gitlab-ctl restart postgresqlsudo gitlab-ctl tail postgresql查看日志末尾Webhook 触发后无反应GitLab 外部 URL 端口与反向代理监听端口不一致sudo gitlab-ctl show-config | grep external_url确认与 Nginx 配置中proxy_pass地址完全一致在 GitLab Web 界面Settings Webhooks测试推送mc cp上传产物失败报The specified key does not exist.MinIO Bucket 名称大小写敏感或路径中存在空格mc ls minio/查看实际 Bucket 名mc cp -r dist/ minio/gitlab-artifacts/${CI_PROJECT_NAME}/mc ls minio/gitlab-artifacts/确认目录已创建5.2 深度避坑指南5 个血泪教训换来的经验教训一永远不要在.gitlab-ci.yml中写死 IP 地址我们曾在一个项目中为加快构建速度把npm install的 registry 直接写成http://10.10.20.15:8081内网 Nexus 地址。后来因安全审计要求Nexus 迁移到新服务器10.10.20.22结果所有分支的构建全部失败。修复方式是批量修改 20 个仓库的.gitlab-ci.yml耗时 3 小时。正确做法是在 GitLab Admin Area 的Settings CI/CD Variables中创建一个全局变量NEXUS_URLhttp://nexus.internal.company:8081在.gitlab-ci.yml中引用${NEXUS_URL}迁移时只需修改全局变量所有项目自动生效。教训二cache的key必须包含分支信息否则热更新失效Vue 项目启用webpack-dev-server热更新时会生成node_modules/.cache/webpack目录。如果cache的key固定为node-modules那么main分支的缓存会被develop分支覆盖导致热更新时加载错误的模块。解决方案cache: