早在2023年,我就已经部署完了GitLab,用来做基础的代码管理,初步尝试CI/CD未果之后搁置。
如今通过 AI agent 来完成软件开发全流程已经最初从美好的畅想、混乱的实践,逐步变得越来越稳健、可落地了。
为了实现 code-agent 开发完成之后 test-agent 能够基于实际打包后的功能做黑盒测试 、完成图形页面的检查,自动化部署从可选优化项变成了刚需。
目前为止,我先跑通了简单的前端-本机自动部署,后续有精力再参照大厂最佳实践做一些改善。
逐步演进到 “全镜像化交付部署”,最终拥抱 “K8s 云原生编排”。
一、GitLab部署
官方提供了Docker部署的方式,不过有点笨重,它内置了Grafana和MySQL,好在可以通过配置环境变量让GitLab使用外部的通用版,节省内存,统一管理数据。
通过Portainer + docker-compose 的方式部署,个人配置过程中还是踩了非常多的坑,不过回顾了一下,很多经验没什么泛用性,还是略过了。
建议直接参考官方文档:

二、架构概览
GitLab Runner (Docker executor, privileged=false)
│
├─ 同一宿主机 (ubuntu jammy)
│ ├─ Nginx (:xxx 前端静态, :yyyy 后端代理)
│ └─ jdk17-base 容器 (:zzzz, jar包更新到 jre容器中,触发脚本重启应用)
│
└─ GitLab (:xxx)
├─ 后端项目 → main 分支触发 → 自动部署生产
└─ 前端项目 → main 分支触发 → 自动部署生产二、准备打包专用镜像
GitLab runner在执行CI/CD的时候,会创建一个临时容器处理打包动作,会需要用到打包基础镜像。
我准备了一个包含 JDK 17 + Maven + Node.js 20 + pnpm、做了各类镜像源加速优化的镜像,前后端通用(对于我自己的项目)。
Dockerfile:
FROM ubuntu:jammy
LABEL version="1.0.0"
LABEL description="专用于zdream前后端CI/CD,包含阿里云maven镜像源、JDK 17 + Maven + Node.js 20 + pnpm"
LABEL maintainer="zc@zdream.cn"
# 设置环境变量
ENV DEBIAN_FRONTEND=noninteractive
ENV JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64
ENV MAVEN_HOME=/usr/share/maven
# 1. 替换 APT 源为阿里云,并安装基础依赖 (JDK、Maven、curl、xz-utils)
RUN sed -i 's/archive.ubuntu.com/mirrors.aliyun.com/g' /etc/apt/sources.list && \
sed -i 's/security.ubuntu.com/mirrors.aliyun.com/g' /etc/apt/sources.list && \
apt-get update && apt-get install -y \
openjdk-17-jdk \
maven \
curl \
xz-utils \
# 清理 apt 缓存以减小镜像体积
&& apt-get clean && rm -rf /var/lib/apt/lists/*
# 2. 极速下载安装 Node.js 20 (使用阿里镜像二进制包) 与 pnpm
RUN curl -fsSL "https://npmmirror.com/mirrors/node/v20.11.1/node-v20.11.1-linux-x64.tar.xz" -o node.tar.xz \
&& tar -xJf node.tar.xz -C /usr/local --strip-components=1 \
&& rm node.tar.xz \
# 使用阿里镜像安装 pnpm
&& npm install -g pnpm --registry=https://registry.npmmirror.com \
# 配置 pnpm 默认使用阿里镜像源
&& pnpm config set registry https://registry.npmmirror.com \
# 关闭 pnpm 更新提示,保持 CI 输出整洁
&& pnpm config set update-notifier false
# 3. 配置 Maven 阿里云镜像与本地仓库路径
# 将准备好的 aliyun.xml 直接覆盖系统的全局 settings.xml
COPY aliyun.xml /usr/share/maven/conf/settings.xml
# 预先创建你在 xml 中配置的仓库挂载目录
RUN mkdir -p /usr/share/maven/ref/repository
# 4. 验证安装版本 (构建成功时会在日志末尾打印版本号)
RUN echo "=== 环境版本验证 ===" \
&& java -version \
&& mvn -version \
&& echo "Node version: $(node -v)" \
&& echo "npm version: $(npm -v)" \
&& echo "pnpm version: $(pnpm -v)"
# 5. 设置工作目录
WORKDIR /workspace附 aliyun.xml 文件
<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0
https://maven.apache.org/xsd/settings-1.0.0.xsd">
<localRepository>/usr/share/maven/ref/repository</localRepository>
<mirrors>
<mirror>
<id>aliyunmaven</id>
<mirrorOf>*</mirrorOf>
<name>阿里云公共仓库</name>
<url>https://maven.aliyun.com/repository/public</url>
</mirror>
</mirrors>
</settings>
构建完成之后,推送到自己的GitLab 镜像仓库
二、更新 Runner 默认镜像
无需修改 runner 配置,.gitlab-ci.yml 中的 image: 会覆盖 runner 默认值。
当前 runner 配置(已存在,不需要改动):
executor: docker
privileged: false(安全,无 DinD)
concurrent: 1
builds_dir:
/builds(映射到宿主机/zdream/datas/gitlab-runner/builds)挂载:
/.m2(Maven 缓存)、/home/project/cache(npm 缓存)
三、前端.gitlab-ci.yml
# 定义 CI/CD 的阶段
stages:
- build
- deploy
# 全局变量配置
variables:
# 将 pnpm 的全局 store 设置到我们挂载的缓存目录中,实现跨 Pipeline 的依赖缓存加速
PNPM_STORE_PATH: '/home/project/cache'
NODE_OPTIONS: '--max-old-space-size=4096'
# 全局默认配置
default:
# 使用你提前准备好的包含了完整环境的构建镜像
image: registry.我的域名端口/zdream/zdream-builder:latest
# ==========================================
# 1. 构建阶段
# ==========================================
build_frontend:
stage: build
script:
- echo "开始安装依赖并构建..."
# 配置 pnpm 使用缓存目录
- pnpm config set store-dir $PNPM_STORE_PATH
# 安装依赖
- pnpm install
# 执行打包 (Vite 默认产物是 dist 文件夹)
- pnpm run build
# 收集构建产物,传递给下一个阶段
artifacts:
name: 'dist-$CI_COMMIT_SHORT_SHA'
paths:
- dist/
expire_in: 1 day # 产物在 GitLab 服务器上保留 1 天即可
# 仅在指定分支发生变更时执行
only:
- main
- web-zdream
# ==========================================
# 2. 部署阶段
# ==========================================
deploy_frontend:
stage: deploy
# 依赖 build 阶段,确保先构建后部署
dependencies:
- build_frontend
script:
- echo "开始部署到宿主机目录..."
# 确保宿主机的目录存在 (容器内执行时,如果宿主机没这个目录会自动创建空目录)
- mkdir -p /zdream/web/dist
# 清空旧的代码 (避免旧的 hash 文件残留)
- rm -rf /zdream/web/dist/*
# 将上一阶段 artifacts 传过来的 dist 目录内容复制到宿主机挂载目录下
- cp -r dist/* /zdream/web/dist/
- echo "部署完成!"
only:
- main
- web-zdream

添加 .gitlab-ci.yml 到项目
推送更新到 main 分支
排错,或者 ☆[BINGO!]

四、后端 .gitlab-ci.yml
stages:
- build
- deploy
variables:
MAVEN_OPTS: "-Dmaven.repo.local=/.m2/repository"
cache:
key: "${CI_COMMIT_REF_SLUG}"
paths:
- /.m2/repository
build:backend:
stage: build
tags:
- base-runner
image: registry.gitlab.cn/YOUR_USERNAME/zdream/builder:latest
script:
- mvn clean package -DskipTests -q
- ls -lh target/*.jar
artifacts:
paths:
- target/*.jar
expire_in: 1 hour
only:
- main
- develop
deploy:backend:
stage: deploy
tags:
- base-runner
image: registry.gitlab.cn/YOUR_USERNAME/zdream/builder:latest
script:
- |
echo "=== [Backend] 部署 ==="
# jar 路径
JAR_PATH=$(find ${CI_PROJECT_DIR}/target -name "*.jar" | head -1)
JAR_NAME=$(basename ${JAR_PATH})
# 备份旧版本(保留最近 3 个)
BACKUP_DIR="/zdream/datas/jdk-base/backups"
mkdir -p ${BACKUP_DIR}
if [ -f /zdream/datas/jdk-base/app/zdream/${JAR_NAME} ]; then
cp /zdream/datas/jdk-base/app/zdream/${JAR_NAME} \
${BACKUP_DIR}/${JAR_NAME}.$(date +%Y%m%d%H%M%S)
echo "备份完成"
ls -t ${BACKUP_DIR} | tail -n +4 | xargs -I{} rm -f ${BACKUP_DIR}/{}
fi
# 复制新 jar(挂载卷自动同步到 jdk17-base 容器)
cp ${JAR_PATH} /zdream/datas/jdk-base/app/zdream/${JAR_NAME}
echo "JAR 已复制: ${JAR_NAME}"
# 重启 jdk17-base 容器
docker restart jdk17-base
echo "jdk17-base 重启命令已发送"
sleep 8
docker logs jdk17-base --tail 15
environment:
name: ${CI_COMMIT_REF_NAME}
url: https://www.hitagi.cn:18898
dependencies:
- build:backend
only:
- main
- develop五、进阶计划
AI 时代,学习新技能的门槛大大降低。
了解到大厂的CI/CD最佳实践,他们实现的是容器化交付,每次更新的产物是一个新的镜像,版本更新直接修改docker-compose中的镜像版本号update一下即可。
当单机 Docker-Compose 无法满足高可用需求时,架构将不可避免地走向 Kubernetes 集群编排。叠加了 K8s 之后,CI/CD 流程可以完成质的飞跃:
CD(持续交付)视角的转变: 部署不再是去操作具体的某一台服务器,而是向 K8s 的 API Server 提交一份 YAML 声明文件。告诉 K8s:“我期望后端应用运行 3 个副本,镜像版本是 v1.0.2”。
零停机滚动更新(Rolling Update): 我们目前重启 Java 容器时,必然会存在几秒到几十秒的服务不可用。而在 K8s 中,它会先启动新版本的 Pod,等新 Pod 的健康检查(Readiness Probe)通过后,再把流量切过去,最后平滑销毁老 Pod。全程对用户无感。
弹性伸缩与自愈: 如果某个 Node 节点宕机,K8s 会自动把上面的应用迁移到其他健康的节点上;如果遇到双十一等突发流量,可以通过 HPA(Horizontal Pod Autoscaling)根据 CPU 使用率自动横向扩容容器数量,流量过后再自动缩容。
评论区