Spring Boot 项目从代码提交到上线通常要经历编译、测试、打包、构建镜像、推送仓库、服务器拉取并重启等多个环节。如果每次合并到主分支后都靠人工执行这些步骤,不仅耗时,还容易因为环境不一致或操作遗漏导致发布失败。借助 GitLab CI 可以把这些流程写进版本库,让每次推送或合并请求自动触发检查、构建和部署。下面从 Runner 准备、流水线文件编写、镜像瘦身和自动部署几个角度展开。

一、准备 GitLab Runner 并选择合适的执行器
GitLab Runner 是真正执行流水线任务的组件,需要单独安装并注册到 GitLab。注册时可以根据部署环境选择 executor,如果 Runner 跑在 Linux 服务器上且只负责构建 Java 项目,用 shell 执行器可以直接使用宿主机上的 JDK 和 Maven,配置简单,但环境容易受主机已有工具影响。如果希望每次任务都在干净、可复现的环境中运行,更推荐 docker 执行器,通过镜像声明构建环境,避免版本冲突。
以 Docker executor 为例,安装 Runner 后执行 gitlab-runner register,填写 GitLab 地址、注册 token,执行器选择 docker,默认镜像指定 maven:3.9-eclipse-temurin-17。注册完成后可以在 /etc/gitlab-runner/config.toml 中看到对应的 [[runners]] 配置。给 Runner 设置 tags 很关键,例如 spring-boot,这样流水线里的任务可以通过 tags 精准选中具备 Java 构建能力的 Runner,避免任务被分配到没有 JDK 或 Docker 的环境。
如果项目需要构建并推送 Docker 镜像,Runner 还必须能访问 Docker 守护进程。常见做法有两种:一种是启用 Docker-in-Docker 服务,在流水线里声明 docker:24-dind,隔离性更好;另一种是直接挂载宿主机的 /var/run/docker.sock,构建速度更快,但安全性略弱,适合内部可信网络。对于公开项目或多人共享 Runner 的场景,建议优先使用 DinD,减少宿主机被越权访问的风险。
二、拆分流水线阶段与缓存 Maven 依赖
.gitlab-ci.yml 是 GitLab CI 的核心配置文件。针对 Spring Boot 项目,可以按 test、package、docker-build、deploy 四个 stage 来组织。测试阶段执行单元测试并收集 JUnit 报告,打包阶段生成可执行 jar,镜像构建阶段基于 jar 制作 Docker 镜像,部署阶段推送到目标服务器。这样拆分后,如果单元测试失败,后面的打包和部署不会执行,能快速暴露回归问题。
Maven 依赖下载通常是 CI 流程里最耗时的部分。默认情况下 Docker executor 每次任务都会创建新容器,本地仓库不会保留,导致每次都重新下载全部依赖。解决办法是在流水线中声明 cache,把 .m2/repository 目录缓存起来。GitLab 会在下一次任务时恢复该目录,Maven 检测到已有依赖就不会重复拉取。缓存键可以绑定 pom.xml 的 hash 或 CI_COMMIT_REF_SLUG,防止不同分支使用错误缓存。
下面是一个去掉部署阶段的简化示例,包含测试、打包和镜像构建:
stages:
- test
- package
- docker-build
variables:
MAVEN_OPTS: "-Dmaven.repo.local=$CI_PROJECT_DIR/.m2/repository"
DOCKER_IMAGE: registry.ipipp.com/myapp
cache:
paths:
- .m2/repository
test:
stage: test
image: maven:3.9-eclipse-temurin-17
script:
- mvn test
artifacts:
reports:
junit:
- target/surefire-reports/TEST-*.xml
package:
stage: package
image: maven:3.9-eclipse-temurin-17
script:
- mvn package -DskipTests
artifacts:
paths:
- target/*.jar
docker-build:
stage: docker-build
image: docker:24
services:
- docker:24-dind
script:
- docker build -t $DOCKER_IMAGE:$CI_COMMIT_SHORT_SHA .
- docker push $DOCKER_IMAGE:$CI_COMMIT_SHORT_SHA
这个配置里 cache 负责加快依赖解析,artifacts 则负责在不同阶段之间传递文件。测试阶段产生的 JUnit 报告会显示在 GitLab 的合并请求页面上,打包阶段的 jar 产物会被 docker-build 任务下载并使用。每个任务的运行环境都是独立容器,需要共享的变量统一放在 variables 全局区,例如镜像仓库地址。
实际执行时,如果测试用例失败,test 任务会以非零状态码退出,GitLab 会标记流水线失败,后续的 package 和 docker-build 都不会触发。这种快速失败机制对 Spring Boot 项目很有价值,诸如服务层逻辑回归、依赖注入缺失、配置错误等问题,往往在测试阶段就能被发现,不需要等到部署到服务器之后再回滚。
三、用多阶段 Docker 构建减小镜像体积
Spring Boot 可执行 jar 已经内置了 Tomcat,但 Docker 镜像的构建方式不同,最终体积和构建速度差别很大。如果直接使用 Maven 镜像作为最终运行镜像,会把完整的 JDK 与编译工具一起打进镜像,体积可能超过 700MB,传输和启动都更慢。多阶段构建可以把编译和运行拆开,第一阶段用 Maven 镜像产出 jar,第二阶段只使用 JRE 运行镜像,复制产物并启动。
下面给出一个适合 Spring Boot 3.x 的 Dockerfile:
FROM maven:3.9-eclipse-temurin-17 AS build WORKDIR /app COPY pom.xml . RUN mvn dependency:go-offline COPY src ./src RUN mvn package -DskipTests FROM eclipse-temurin:17-jre WORKDIR /app COPY --from=build /app/target/*.jar app.jar ENTRYPOINT ["java","-jar","app.jar"]
第一阶段先复制 pom.xml 并执行 mvn dependency:go-offline,这样能提前拉取依赖,后续源码变动不会重复下载。第二阶段选择 eclipse-temurin:17-jre,只带运行环境,不再包含 javac 等开发工具。最终镜像通常可以降到 200MB 以下,同时减少攻击面,更适合生产环境发布。
如果构建时发现通配符匹配到多个 jar 文件导致 COPY 失败,可以在 Maven 中固定最终文件名。在 pom.xml 里给 spring-boot-maven-plugin 配置 finalName,例如 app,这样打包产物就是 app.jar,Dockerfile 里可以直接写 COPY --from=build /app/target/app.jar app.jar,避免歧义。对于追求更小体积的团队,可以进一步评估 GraalVM Native Image,但需要额外处理反射配置和构建时间,不适合所有项目。
四、自动部署与触发策略控制
镜像推送到仓库后,部署阶段需要把新版本拉取到目标服务器并重启容器。可以在 GitLab CI 中使用 SSH 命令连接远程主机,依次执行 docker pull、docker stop、docker rm 和 docker run。SSH 私钥应保存在项目的 CI/CD 变量中,并开启 Protected 和 Masked,绝对不要提交到版本库。部署脚本尽量拆成多个独立命令,避免在单引号里堆叠复杂逻辑,便于阅读和排错。
下面是一个部署阶段的示例:
deploy:
stage: deploy
script:
- ssh $SSH_USER@$SSH_HOST "docker pull $DOCKER_IMAGE:$CI_COMMIT_SHORT_SHA"
- ssh $SSH_USER@$SSH_HOST "docker stop app || true"
- ssh $SSH_USER@$SSH_HOST "docker rm app || true"
- ssh $SSH_USER@$SSH_HOST "docker run -d --name app -p 8080:8080 $DOCKER_IMAGE:$CI_COMMIT_SHORT_SHA"
only:
- main
这里 DOCKER_IMAGE、SSH_USER 和 SSH_HOST 都来自 GitLab 的 CI/CD 变量,不会暴露在配置文件里。镜像仓库如果需要登录,建议使用 docker login 配合密码输入重定向,并且把令牌类型的密码也放到变量里,避免明文打印到流水线日志。部署完成后可以加一个健康检查步骤,例如休眠几秒后请求 /actuator/health,接口返回非 200 时让流水线失败,方便及时回滚。
不是所有提交都需要触发完整流水线。日常开发分支往往只执行测试和打包,合并到 main 分支后才构建并部署。通过 rules 可以对任务做细粒度控制:
deploy:
stage: deploy
rules:
- if: '$CI_COMMIT_BRANCH == "main"'
when: always
- when: manual
script:
- echo "Deploying to production"
这个配置表示只有在 main 分支提交时自动执行部署,其他分支需要手动触发。预发布环境也可以用 when: manual 实现人工确认,减少误操作。流水线跑通后,日常提交流程变为:开发推送到特性分支,自动运行测试和打包;合并到 main 后触发镜像构建和部署。应用更新频率提高后,建议把敏感配置从 application.yml 迁移到环境变量或配置中心,保证同一镜像在不同环境中的行为一致。
Spring BootGitLab CICI/CD修改时间:2026-09-23 18:52:23