内网部署 GitLab CI/CD 实战:离线环境稳定运行指南 1. 项目概述为什么内网部署 GitLab CI/CD 不是“退而求其次”而是刚需落地的第一步你是不是也遇到过这样的场景公司刚立项一个新系统安全团队拍板“所有开发环境必须隔离在内网”运维同事甩来一句“外网 GitLab 不许连自己搭”或者你在做政企、金融、制造业客户的交付项目客户明确要求“代码不能出防火墙CI 流水线必须跑在本地机房”又或者你是个技术负责人正为测试环境频繁被外部网络抖动拖垮而头疼——这时候翻遍全网教程90% 都在讲“怎么用 Docker 跑个 GitLab 社区版”但没人告诉你跑起来只是起点让 CI/CD 在无外网、无公网域名、无 DNS 解析的纯内网里真正稳定跑通构建、测试、部署全流程才是真正的分水岭。这正是“从零搭建 GitLab CI/CD 环境内网部署完整实战”这个标题背后的真实需求。它不是教你怎么点几下鼠标装个 GitLab而是直面内网环境特有的三重硬骨头网络拓扑不可控、服务发现不透明、权限链路不闭环。比如GitLab Runner 启动后连不上 GitLab 实例报错login failed. check api token or gitlab version你查日志发现根本不是 token 错而是 Runner 容器默认用localhost去连宿主机上的 GitLab结果走的是容器内部回环压根没发出去再比如你写好.gitlab-ci.yml流水线卡在git clone步骤提示fatal: unable to access http://gitlab.example.com/group/project.git/: Could not resolve host: gitlab.example.com——问题不在 GitLab而在 Runner 所在节点压根没配内网 DNS或者/etc/hosts漏了一行映射。我带过 7 个不同行业的私有化交付项目从汽车电子产线的嵌入式固件 CI到三甲医院 HIS 系统的 Java 微服务发布再到军工研究所的 FPGA 工程自动化编译所有成功落地的案例第一步都是先搞定内网 CI/CD 的“呼吸权”让 Runner 能稳定认出 GitLabGitLab 能可靠调起 RunnerRunner 能干净拉下代码、执行脚本、上传产物。这不是炫技是工程底线。本文接下来要拆解的就是这条“呼吸链”上每一个咬合齿的实操细节从物理服务器选型建议别一上来就堆 64 核内网小集群 8C16G 更稳到 Docker Compose 编排中network_mode: host和extra_hosts的取舍逻辑从 GitLab 内置 Registry 的证书自签与 Runner 镜像信任配置到.gitlab-ci.yml中image:字段在离线环境下的三种替代方案从 Runner 注册时--executor docker与--executor shell的本质差异到如何用gitlab-runner verify --debug抓出 DNS 解析失败的毫秒级超时证据。所有内容都来自我在某省电力调度中心机房连续蹲点 37 天、重装 12 次 GitLab 实例后记下的操作日志。你可以直接抄作业但更建议你理解每一步背后的“为什么”——因为内网没有错误可以靠重试掩盖只有逻辑清晰才能一次跑通。2. 整体架构设计与核心组件选型逻辑2.1 为什么放弃“一键安装包”坚持 Docker Compose 部署很多新手看到官方文档里curl -s https://packages.gitlab.com/install/repositories/gitlab/gitlab-ce/script.deb.sh | sudo bash这类命令就直接开干结果在内网里栽得最惨。原因很简单包管理器依赖的外部源如packages.gitlab.com在内网不可达且 deb/rpm 包内置的 systemd 服务单元文件对内网多网卡、多路由表的复杂拓扑适配性极差。我见过最典型的故障是GitLab 服务启动后Web 界面能打开但 API 调用全部 502查gitlab-ctl tail nginx日志发现 upstream connect error根源是gitlab.rb里external_url配置的地址被 Nginx 反向代理规则错误地解析成了127.0.0.1:8080而实际 Puma 进程监听的是0.0.0.0:8080但内网防火墙策略恰好封掉了0.0.0.0的入站。Docker Compose 成为内网首选核心在于它的“声明式隔离”能力。我们用docker-compose.yml显式定义每个服务的网络模式、端口映射、卷挂载和环境变量把 GitLab、Redis、PostgreSQL、GitLab Runner 全部约束在一个可控的命名空间里。关键优势有三点第一网络拓扑完全自主可控。通过networks定义一个gitlab-net并设置driver: bridge所有容器都在同一子网如172.20.0.0/16内通信彻底规避宿主机iptables规则和firewalld策略的干扰。Runner 容器里执行ping gitlab能通就说明服务发现没问题不用再纠结localhost到底指谁。第二版本锁定与依赖固化。在docker-compose.yml中明确指定镜像标签例如gitlab/gitlab-ce:16.10.3-ce.0而不是latest。内网环境最怕“自动升级破坏兼容性”。我们曾因latest镜像升级到 16.11导致旧版 Runnerv15.x注册失败报错API version mismatch回滚耗时 4 小时。而用固定标签配合docker images本地镜像缓存整个部署过程可完全离线。第三配置即代码便于审计与复现。gitlab.rb的等效配置全部转为environment下的GITLAB_OMNIBUS_CONFIG变量例如environment: GITLAB_OMNIBUS_CONFIG: | external_url http://gitlab.internal:8080 gitlab_rails[gitlab_shell_ssh_port] 2222 registry_external_url http://registry.internal:5000 prometheus_monitoring[enable] false这段配置直接嵌入 YAML无需登录容器修改文件每次docker-compose up -d都是干净的、可验证的状态。安全团队要审计直接看 Git 仓库里的docker-compose.yml即可比翻查 20 个容器内的配置文件高效十倍。提示不要迷信“GitLab Omnibus 包”的“开箱即用”。在内网它的“即用”建立在对外部源的强依赖上而 Docker Compose 的“即用”是建立在你对每个字节流向的绝对掌控上。2.2 GitLab 与 Runner 的部署模式单机 All-in-One 还是分离部署这是内网部署的第一个战略决策点。很多教程默认推荐“一台服务器GitLab 和 Runner 全装一起”看似省事实则埋下性能与安全双雷。单机模式的致命缺陷资源争抢GitLab 自身就包含 Nginx、Puma、Sidekiq、Gitaly、PostgreSQL、Redis 八大进程内存常驻 3~4GB。当 Runner 启动一个dockerexecutor 的 job 时会额外拉起一个构建容器瞬间吃掉 1~2GB 内存。我实测过在 16GB 内存的服务器上单机模式跑两个并发 jobGitLab Web 界面响应延迟直接从 200ms 拉到 3sSidekiq 队列积压。安全边界模糊Runner 的shellexecutor 默认以gitlab-runner用户执行脚本如果该用户被赋予了sudo权限常见于需要apt install依赖的场景就等于把 GitLab 服务器的 root 权限通过 CI 流水线暴露给了所有开发者。一旦.gitlab-ci.yml被恶意篡改后果不堪设想。分离部署的实操逻辑 我们采用“GitLab 主控节点 Runner 执行节点”的经典二层架构。GitLab 节点只负责代码托管、UI 展示、API 调度配置为8C16G磁盘2TB RAID1存放仓库数据Runner 节点则按需横向扩展每个节点4C8G专用于执行构建任务。两者通过内网10.10.10.0/24网段通信。关键设计点在于 Runner 的注册方式。必须使用--url http://gitlab.internal:8080/注意是内网地址非localhost并指定--registration-token从 GitLab Admin Area → Overview → Runners 页面获取。这样注册后的 Runner其config.toml文件里url字段明确指向内网地址避免了 DNS 解析失败或localhost解析歧义。更进一步我们为不同语言栈划分 Runner 类型java-runner标签用于 Maven 构建预装 JDK 17 和 Maven 3.9python-runner标签用于 PyTest 测试预装 Python 3.11 和 pipenvfrontend-runner标签用于 Vue/React 构建预装 Node.js 18 和 pnpm。这样在.gitlab-ci.yml中只需写tags: [java-runner]GitLab 就只会把该 job 派发给对应标签的 Runner资源利用效率提升 40%故障隔离性也更强。2.3 内网服务发现方案DNS、Hosts 还是 Service Mesh内网没有公共 DNS服务间调用必须解决“名字怎么变 IP”的问题。常见方案有三手动维护/etc/hosts、部署内网 DNS 服务器如 dnsmasq、或用 Consul 等 Service Mesh。我们的选择很务实对于中小规模内网50 台服务器/etc/hosts是最简单、最可靠、最易排查的方案。理由很直接DNS 服务器本身是个单点故障。我们曾在一个客户现场因 dnsmasq 配置文件语法错误导致服务崩溃整个 CI 流水线停摆 2 小时而修复只需 30 秒——改 hosts。更重要的是/etc/hosts的解析优先级高于 DNS且无缓存 TTL 问题ping gitlab.internal返回的 IP 就是最终通信地址排查网络问题时tcpdump -i any host 10.10.10.10抓到的包IP 地址和 hosts 里写的完全一致不存在“DNS 返回了 A 记录但实际走的是 CNAME”这类隐藏路径。具体操作上我们在 GitLab 节点和所有 Runner 节点的/etc/hosts中统一添加三行10.10.10.10 gitlab.internal 10.10.10.11 registry.internal 10.10.10.12 runner01.internal注意gitlab.internal和registry.internal必须与docker-compose.yml中external_url和registry_external_url的域名严格一致。这里有个极易踩的坑很多人把external_url配成http://10.10.10.10:8080以为 IP 最直接结果导致 GitLab 内置的邮件通知链接、Webhook 回调地址全部变成裸 IP既不安全暴露内网结构也不符合 RFC 标准部分邮件客户端拒绝渲染裸 IP 链接。用gitlab.internal这样的 FQDN既满足规范又通过 hosts 统一解析一举两得。注意/etc/hosts方案的维护成本在于“一致性”。我们用 Ansible Playbook 自动同步每次新增 Runner 节点Playbook 会自动将最新 hosts 文件推送到目标机器并执行systemctl restart docker重启 Docker 服务确保容器内 DNS 解析生效。手动改 hosts 后不重启 Docker容器内依然读取旧的/etc/resolv.conf这是 80% 的“改了 hosts 还不通”问题的根源。3. 核心组件部署与关键配置详解3.1 GitLab CE 的 Docker Compose 部署从镜像拉取到服务启动内网部署的第一步是把 GitLab 镜像“搬”进内网。这绝不是简单的docker pull而是一套完整的镜像供应链管理。步骤 1外网环境镜像导出在有外网的跳板机上执行# 拉取 GitLab CE 16.10.3 镜像务必核对官网 Release Notes 确认版本兼容性 docker pull gitlab/gitlab-ce:16.10.3-ce.0 # 导出为 tar 包便于离线传输 docker save gitlab/gitlab-ce:16.10.3-ce.0 gitlab-ce-16.10.3.tar # 同时导出 Redis 和 PostgreSQL 镜像GitLab CE 依赖 docker pull redis:7.0.12-alpine docker pull postgres:14.10-alpine docker save redis:7.0.12-alpine postgres:14.10-alpine db-images.tar导出的gitlab-ce-16.10.3.tar和db-images.tar通过 U 盘或内网 FTP 传入目标内网服务器。步骤 2内网环境镜像加载在内网服务器上执行# 加载镜像顺序无关docker load 会自动处理依赖 docker load gitlab-ce-16.10.3.tar docker load db-images.tar # 验证镜像已存在 docker images | grep -E (gitlab|redis|postgres) # 应输出三行包含对应镜像名和 TAG步骤 3编写docker-compose.yml这是整个部署的核心配置文件。以下是我们生产环境精简后的版本已去除监控、备份等非必需模块version: 3.7 services: web: image: gitlab/gitlab-ce:16.10.3-ce.0 restart: always hostname: gitlab.internal environment: GITLAB_OMNIBUS_CONFIG: | external_url http://gitlab.internal:8080 gitlab_rails[gitlab_shell_ssh_port] 2222 registry_external_url http://registry.internal:5000 # 关闭不必要服务节省内存 prometheus_monitoring[enable] false alertmanager[enable] false grafana[enable] false # 配置 PostgreSQL 连接指向同 Compose 文件中的 db 服务 postgresql[enable] false gitlab_rails[db_host] db gitlab_rails[db_port] 5432 gitlab_rails[db_database] gitlabhq_production gitlab_rails[db_username] gitlab gitlab_rails[db_password] gitlab_db_pass_123 # 配置 Redis 连接 redis[enable] false gitlab_rails[redis_host] redis gitlab_rails[redis_port] 6379 # 配置 GitalyGitLab 14 强制启用 gitaly[enable] true gitaly[bin_path] /opt/gitlab/embedded/bin/gitaly # 配置内置 RegistryDocker 镜像仓库 registry[enable] true registry[storage_path] /var/opt/gitlab/registry registry[http_addr] 0.0.0.0:5000 ports: - 8080:80 # HTTP 访问端口 - 2222:22 # SSH 克隆端口避免与宿主机 SSH 冲突 - 5000:5000 # Registry 端口 volumes: - /srv/gitlab/config:/etc/gitlab - /srv/gitlab/logs:/var/log/gitlab - /srv/gitlab/data:/var/opt/gitlab networks: - gitlab-net depends_on: - db - redis db: image: postgres:14.10-alpine restart: always environment: POSTGRES_USER: gitlab POSTGRES_PASSWORD: gitlab_db_pass_123 POSTGRES_DB: gitlabhq_production POSTGRES_INITDB_ARGS: --encodingUNICODE --lc-collateC --lc-ctypeC volumes: - /srv/gitlab/postgresql:/var/lib/postgresql/data networks: - gitlab-net redis: image: redis:7.0.12-alpine restart: always command: - --maxmemory - 256mb - --maxmemory-policy - allkeys-lru volumes: - /srv/gitlab/redis:/data networks: - gitlab-net networks: gitlab-net: driver: bridge ipam: config: - subnet: 172.20.0.0/16这份配置的关键点解析hostname: gitlab.internal强制容器内 hostname 为gitlab.internal与external_url域名一致避免 GitLab 内部组件如 Gitaly通信时域名解析失败。postgresql[enable] false显式关闭 GitLab 内置 PostgreSQL改用独立的db服务这是为了数据库性能隔离和后续主从扩展。gitaly[enable] trueGitLab 14 版本强制启用 Gitaly 作为 Git 操作代理必须配置bin_path指向正确路径否则启动失败。volumes挂载路径全部使用绝对路径/srv/gitlab/xxx而非相对路径确保宿主机目录结构清晰便于备份和迁移。步骤 4首次启动与初始化# 创建宿主机目录 mkdir -p /srv/gitlab/{config,logs,data,postgresql,redis} # 启动服务-d 后台运行 docker-compose up -d # 查看服务状态等待 3~5 分钟GitLab 初始化较慢 docker-compose ps # 应显示 web/db/redis 全部为 Up 状态 # 查看初始化日志关键确认是否成功 docker-compose logs -f web | grep gitlab Reconfigured # 出现 gitlab Reconfigured! 表示初始化完成初始化完成后浏览器访问http://10.10.10.10:8080即gitlab.internal对应的 IP首次登录用户名为root密码需从容器日志中提取docker-compose exec web cat /etc/gitlab/initial_root_password # 输出类似Password: wWzXyZ123!#3.2 内置 Registry 的配置与安全加固GitLab 内置的 Docker Registry 是内网 CI/CD 的核心枢纽它让构建出的镜像能被安全、高效地存储和分发。但默认配置存在严重安全隐患Registry 默认启用匿名拉取anonymous pull任何知道 URL 的人都能docker pull registry.internal:5000/project/image这在内网等同于裸奔。安全加固四步法强制认证在docker-compose.yml的web服务environment中添加 Registry 认证配置environment: GITLAB_OMNIBUS_CONFIG: | # ... 其他配置 registry[enable] true registry[auth_token] { realm http://gitlab.internal:8080/jwt/auth, service container_registry, issuer omnibus-gitlab-issuer, secret your-very-secret-jwt-key-here }secret字段必须是 32 字节以上的随机字符串可用openssl rand -hex 32生成。这个密钥用于 GitLab 生成 JWT TokenRegistry 用它校验拉取请求的合法性。禁用匿名拉取在registry配置块中添加registry[storage] { filesystem { rootdirectory /var/opt/gitlab/registry } } registry[log_level] info registry[log] { level info, fields { service registry } } # 关键禁用匿名访问 registry[disable_anonymous_auth] true配置 HTTPS强烈推荐虽然内网可跑 HTTP但 Docker 客户端默认只信任 HTTPS Registry。我们采用自签名证书方案# 在 GitLab 节点生成证书有效期 10 年 mkdir -p /srv/gitlab/certs openssl req -x509 -newkey rsa:4096 -keyout /srv/gitlab/certs/registry.key -out /srv/gitlab/certs/registry.crt -days 3650 -nodes -subj /CNregistry.internal # 修改 docker-compose.yml挂载证书 volumes: - /srv/gitlab/certs:/certs environment: GITLAB_OMNIBUS_CONFIG: | # ... 其他配置 registry[https] { certificate /certs/registry.crt, key /certs/registry.key }然后在所有 Runner 节点将/srv/gitlab/certs/registry.crt复制到/etc/docker/certs.d/registry.internal:5000/ca.crt并重启 Dockersystemctl restart docker。这样 Runner 执行docker push时就能信任该 Registry。网络策略收紧在docker-compose.yml中Registry 端口5000不对外暴露只允许gitlab-net内部通信。这意味着只有 GitLab Web 服务和 Runner 容器能访问 Registry宿主机其他进程无法连接从网络层面切断未授权访问。实操心得Registry 的storage.filesystem.rootdirectory必须挂载到宿主机持久化目录如/srv/gitlab/registry否则容器重启后镜像丢失。我们曾因忘记挂载导致一次构建产物全部消失回滚耗时 1 小时。教训是所有volumes挂载点必须在mkdir -p命令中一次性创建完毕并写入部署 Checklist。3.3 GitLab Runner 的注册与 Executor 选型Runner 是 CI/CD 的“手”它的稳定性和安全性直接决定整个流水线的可靠性。内网环境下Executor执行器的选择尤为关键。Executor 类型对比与选型逻辑Executor 类型适用场景内网优势内网风险我们的选用shell简单脚本、无需隔离的构建零容器开销启动最快共享宿主机环境权限泄露风险高仅用于gitlab-runner verify等诊断任务docker标准化构建Java/Maven、Python/pipenv环境隔离好镜像可复用需要 Docker Daemon网络配置复杂主力选用搭配docker:dindDocker in Dockerkubernetes大规模、多租户 CI资源弹性好隔离性强K8s 集群运维成本高内网部署复杂暂不选用待业务规模扩大后评估我们最终选定dockerexecutor并采用docker:dind模式即 Runner 容器内再启动一个 Docker Daemon实现真正的构建环境隔离。配置docker-compose.yml如下version: 3.7 services: runner: image: gitlab/gitlab-runner:alpine-v16.10.0 restart: always volumes: - /srv/gitlab-runner/config:/etc/gitlab-runner - /var/run/docker.sock:/var/run/docker.sock # 关键挂载宿主机 Docker Socket - /srv/gitlab-runner/cache:/cache environment: - GITLAB_URLhttp://gitlab.internal:8080/ - REGISTER_RUN_UNTAGGEDfalse - REGISTER_TAG_LISTjava-runner,python-runner - DOCKER_DRIVERoverlay2 - DOCKER_TLS_CERTDIR networks: - gitlab-net关键点解析volumes挂载/var/run/docker.sock这是docker:dind模式的基石Runner 容器通过此 socket 与宿主机 Docker Daemon 通信从而拉起构建容器。REGISTER_TAG_LIST注册时自动打上java-runner和python-runner标签与.gitlab-ci.yml中的tags字段匹配。DOCKER_TLS_CERTDIR清空 TLS 证书目录避免与宿主机 Docker TLS 配置冲突。Runner 注册实操# 进入 Runner 容器 docker-compose exec -it runner sh # 执行注册命令URL 用内网地址Token 从 GitLab Admin 页面复制 gitlab-runner register \ --non-interactive \ --url http://gitlab.internal:8080/ \ --registration-token GR1348941xYzABC123def456 \ --executor docker \ --docker-image alpine:latest \ --description java-runner-docker \ --tag-list java-runner \ --run-untaggedfalse \ --lockedfalse \ --access-levelnot_protected # 注册成功后退出容器重启 Runner 服务 exit docker-compose restart runner注册成功后GitLab Admin 页面的 Runners 列表会出现新 Runner状态为online。此时可执行验证# 在 Runner 容器内验证连通性 docker-compose exec runner gitlab-runner verify --debug # 输出应包含 Running in system-mode 和 Verifying runner... ok--debug参数会打印详细日志其中Checking for jobs... received行表示 Runner 已成功连接 GitLab 并开始轮询任务这是最关键的健康信号。4. CI/CD 流水线实战从 .gitlab-ci.yml 编写到端到端交付4.1 .gitlab-ci.yml 核心结构解析为什么 80% 的失败源于基础语法错误.gitlab-ci.yml是 CI/CD 的“宪法”它的语法严谨性直接决定流水线能否启动。内网环境下常见的失败并非逻辑错误而是基础配置疏漏。我们梳理出高频错误 Top 3错误 1缩进混乱导致 YAML 解析失败YAML 对空格极其敏感。以下写法是致命的# ❌ 错误tab 字符混入或空格数不一致 stages: - build - test build_job: stage: build script: - echo building... # 这里用了 2 个空格但上一行是 4 个正确做法全文使用 2 个空格缩进禁用 Tab 键。VS Code 中可设置editor.insertSpaces: true, editor.tabSize: 2并安装 YAML 插件实时校验。错误 2image:字段指向不可达的镜像内网没有 Docker Hubimage: maven:3.9-openjdk-17这类写法必然失败。解决方案有三方案 A推荐使用内网 Registry 镜像image: registry.internal:5000/base-images/maven-3.9-jdk17:latest需提前在 Registry 中推送该镜像。方案 B使用services:启动辅助容器build_job: stage: build image: alpine:latest # 基础镜像极小 services: - name: registry.internal:5000/base-images/maven-3.9-jdk17:latest alias: maven script: - apk add --no-cache openjdk17-jre # 安装基础依赖 - mvn -v # 验证 maven 是否可用方案 Cbefore_script中动态安装before_script: - apk add --no-cache openjdk17 maven错误 3variables:作用域理解错误variables有全局、job 级、script 级三个作用域。常见误区是认为在variables:下定义的变量能在script中直接用$VAR引用却忽略了script是在新的 Shell 进程中执行。正确写法variables: MAVEN_OPTS: -Dmaven.repo.local/cache/m2 JAVA_HOME: /usr/lib/jvm/java-17-openjdk build_job: stage: build image: registry.internal:5000/base-images/maven-3.9-jdk17:latest script: - echo JAVA_HOME is $JAVA_HOME # ✅ 正确变量已注入 - mvn clean package -DskipTests4.2 Java 项目完整 CI/CD 流水线从编译到镜像推送以一个 Spring Boot 项目为例展示端到端流水线。关键要求构建产物JAR存档、Docker 镜像推送至内网 Registry、部署到测试环境。# .gitlab-ci.yml stages: - build - test - package - deploy variables: # 全局变量定义项目信息 APP_NAME: spring-boot-demo APP_VERSION: 1.0.0-${CI_COMMIT_SHORT_SHA} # Docker Registry 认证信息从 GitLab CI Variables 中注入非明文 DOCKER_REGISTRY: registry.internal:5000 DOCKER_IMAGE: $DOCKER_REGISTRY/$CI_PROJECT_NAMESPACE/$APP_NAME # 构建阶段编译 Java 代码 build_job: stage: build image: registry.internal:5000/base-images/maven-3.9-jdk17:latest tags: - java-runner script: - mvn -B clean compile -Dmaven.test.skiptrue artifacts: paths: - target/classes/ expire_in: 1 week # 测试阶段运行单元测试 test_job: stage: test image: registry.internal:5000/base-images/maven-3.9-jdk17:latest tags: - java-runner script: - mvn -B test artifacts: paths: - target/surefire-reports/ expire_in: 1 week # 打包阶段生成可执行 JAR 和 Docker 镜像 package_job: stage: package image: registry.internal:5000/base-images/maven-3.9-jdk17:latest tags: - java-runner script: # 1. 构建 JAR跳过测试因已在 test_job 完成 - mvn -B clean package -DskipTests # 2. 构建 Docker 镜像使用项目根目录下的 Dockerfile - docker build -t $DOCKER_IMAGE:$APP_VERSION . # 3. 推送镜像到内网 Registry - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $DOCKER_REGISTRY - docker push $DOCKER_IMAGE:$APP_VERSION artifacts: paths: - target/*.jar expire_in: 1 week # 部署阶段将镜像部署到测试 Kubernetes 集群 deploy_job: stage: deploy image: registry.internal:5000/base-images/kubectl-helm:1.28.3 tags: - k8s-runner script: # 使用 Helm 部署values.yaml 中已配置内网 Registry 地址 - helm upgrade --install $APP_NAME ./helm-chart \ --set image.repository$DOCKER_IMAGE \ --set image.tag$APP_VERSION \ --namespace test-env only: - main # 仅 main 分支触发部署关键配置说明artifacts每个 job 的产物如编译后的 class 文件、测试报告、JAR 包自动存档供下游 job 下载或人工下载。expire_in设置过期时间避免磁盘爆满。only: - main精准控制部署时机避免 feature 分支误触发生产环境变更。docker login使用 GitLab CI 内置的CI_REGISTRY_USER和CI_REGISTRY_PASSWORD变量这些变量由 GitLab 自动注入无需明文写在 YAML 中保障凭证