ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

SpringBoot+Docker+Jenkins:从零搭建CI/CD自动化部署流水线

SpringBoot+Docker+Jenkins:从零搭建CI/CD自动化部署流水线 做了几年 Java 后端最烦的就是“本地编译没问题一上线就各种崩”这种事。反复打 jar 包、传服务器、手动重启一两次还能忍项目一多每周都能烧掉大半天。后来我把 SpringBoot、Docker、Jenkins 串成一条自动化的构建、测试、部署流水线代码一推到仓库后面所有事都不用再管了。整个过程不复杂但里面的坑是真不少。这篇文章把我从零搭到稳定运行的全过程记录下来包括流水线怎么设计、Jenkinsfile 怎么写、部署脚本怎么处理以及那些“照着文档做却死活跑不通”的诡异问题。适合正在搭 CI/CD 的小团队也适合想把手动部署彻底解放出来的个人开发者。1. 整套流水线的设计思路1.1 为什么偏偏是 SpringBoot、Docker、Jenkins 这三个组合先说清楚这套方案没有什么玄学就是各司其职。SpringBoot 负责把业务跑起来。它的核心优势是“可执行 jar”和“内嵌 Web 容器”这也决定了它的交付物天然适合容器化——一个 java -jar 就能启动的服务扔进 Docker 镜像里几乎零成本。Docker 负责把运行环境固定下来。SpringBoot 解决了“代码跑起来”的问题但没解决“环境长什么样”的问题。同一个 jar 包在 JDK 8 和 JDK 17 上行为可能完全不同在 CentOS 和 Alpine 上也可能遇到 glibc 缺失的问题。Docker 把 JDK 版本、系统依赖、时区、编码方式全部打包进镜像无论部署到哪台机器运行环境都一模一样。Jenkins 负责把前面两件事串成流水线。它其实就是一个自动化调度中心监听代码变更、执行测试、跑构建、调 Docker 打镜像、推送到仓库、再触发远程服务器更新。可以把 Jenkins 想象成一个工厂的车间主任SpringBoot 是产品Docker 是包装线Jenkins 就是那个喊着“下一个”的工头。这三者组合起来收益最直接的还不是“少打几次命令”而是让每次发布都走同一条经过验证的路径。不会出现“上次是手动部署所以少传了一个配置文件”这种事因为整条链路里没有手动操作了。1.2 完整流水线是怎么串起来的一条标准的流水线从开发者的视角看只有一步push 代码。但背后实际上走了六个环节代码拉取Jenkins 从 Git 仓库拉取最新代码按分支区分。自动测试执行 Maven 的单测、集成测试测试失败直接中断不许进入下一步。项目构建mvn package 打出可执行 jar。镜像构建用 Dockerfile 把 jar 包和运行环境打成 Docker 镜像。镜像推送把镜像推到镜像仓库可以是 Docker Hub也可以是自建的 Harbor 或云厂商的镜像服务。远程部署目标服务器拉取新镜像停掉旧容器启动新容器。这六个环节里前三步跑在构建机上后三步涉及产物流转。这里有一个关键设计理念构建环境和部署环境必须隔离。测试、打 jar、打镜像这些重活全在 Jenkins 所在的构建机上完成目标服务器只负责拉取和运行保持轻量。我见过不少团队图省事直接在服务器上装 Jenkins然后构建、部署全在一台机器上完成。短期内没问题一旦项目多起来磁盘被镜像占满、CPU 被构建任务打满、测试环境被搞挂都是必然会发生的事。1.3 部署方案的取舍部署方式决定了流水线的复杂程度。我建议按团队规模分三档单机 Docker Compose适合个人项目或小团队。镜像推到仓库后目标机器上一条 docker compose pull docker compose up -d 搞定。简单直观出了问题也好排查。多机 Docker 直连适合已经有几台服务器的场景。通过 SSH 在远程机器上执行部署命令或者在 Jenkins 里配置多个目标节点用 sshPublisher 插件往不同机器分发容器启动指令。Kubernetes 集群适合业务量起来之后的阶段。这时候 Jenkins 的角色会偏重为“生成镜像 更新工作负载”部署动作变成 kubectl set image 或者 helm upgrade流水线的后端逻辑不变只是最后一步的调用对象换了。我个人强烈建议从单机方案起步但从第一天起就要把镜像推到私有仓库。哪怕只有一台服务器也养成“先推送、再拉取”的习惯。因为一旦后面加了第二台服务器你不需要回头改流程。2. 前置环境与项目基础准备2.1 Docker 环境安装与镜像加速构建机上必须先有 Docker。Linux 上安装很简单官方脚本一条命令curl -fsSL https://get.docker.com | sh systemctl enable --now dockerWindows 上一般用 Docker Desktop但有个极其常见的坑安装完启动报Docker Desktop failed to start because virtualisation support wasnt detected。这不是 Docker 的问题是 hypervisor 没开。需要进 BIOS 把 CPU 虚拟化Intel VT-x / AMD SVM打开同时确认 Windows 的 Hyper-V 功能或 WSL2 已启用。装完执行 docker info 确认没问题再往下走。国内环境还有个绕不开的点镜像加速。 Docker 默认从 Docker Hub 拉镜像在国内网络环境下经常超时。需要改 /etc/docker/daemon.json配置国内镜像源{ registry-mirrors: [https://docker.m.daocloud.io] }改完执行 systemctl daemon-reload systemctl restart docker。注意这个配置影响的是拉取镜像的速度不影响已配置的镜像仓库地址。后续 Jenkins 构建时如果在同一台机器上也会自动享受加速。2.2 SpringBoot 工程改造SpringBoot 项目本身的改造点不多但有几个直接影响流水线成败的细节第一个是版本。网上常有人问“SpringBoot 版本太高怎么办”其实就是兼容性问题。比如 SpringBoot 3.x 强制要求 JDK 17某些老版本的 Maven 插件、动态代理库不兼容。做流水线时要先确认项目用的 JDK 版本、Maven 版本然后在 Dockerfile 和 Jenkins 里锁死同一个版本避免出现“本地 JDK 17 能编Jenkins 容器里 JDK 8 报错”的镜像。第二个是健康检查。容器编排和部署脚本需要知道应用是否真的启动成功SpringBoot 的 Actuator 就是为这个准备的。pom.xml 加依赖dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-actuator/artifactId /dependency然后在 application.yml 里暴露健康端点management: endpoints: web: exposure: include: health,info部署脚本里就可以通过 curl http://localhost:8080/actuator/health 判断服务是否就绪而不是傻等几秒。第三个是端口配置。容器化的 SpringBoot 建议直接用环境变量覆盖配置比如server: port: ${SERVER_PORT:8080}这样部署时通过 -e SERVER_PORT9090 就能灵活调整端口不用改代码重新打包。2.3 Jenkins 环境安装与基础配置Jenkins 本身也推荐用 Docker 安装一来干净二来后续升级简单。一个可以落地的启动命令如下docker run -d \ --name jenkins \ -p 8080:8080 \ -p 50000:50000 \ -v /home/jenkins_home:/var/jenkins_home \ -v /var/run/docker.sock:/var/run/docker.sock \ -e TZAsia/Shanghai \ jenkins/jenkins:lts-jdk17这里有两个细节要解释一下。挂载 /var/run/docker.sock 是为了让 Jenkins 容器内可以直接执行 docker 命令这解决了“Jenkins 容器内使用 Docker 命令”的经典问题。时区设置为 Asia/Shanghai 是为了让构建日志时间显示正常否则默认 UTC排查问题时会差 8 小时非常难受。首次启动后浏览器访问 http://服务器IP:8080用初始密码解锁。初始密码在容器日志里可以这样拿docker exec jenkins cat /var/jenkins_home/secrets/initialAdminPassword插件安装时建议选“自定义安装”不推荐装全套推荐插件。你真正需要的只有这些Git Parameter构建时选择分支或标签Pipeline核心流水线插件Docker Pipeline构建和推送镜像的增强支持SSH Agent / Publish Over SSH远程部署Blue Ocean查看流水线可视化界面会舒服很多装完插件后可以顺手汉化一下安装 Locale 插件把语言设为 zh_CN。这不是必须的但团队里有英语不太熟练的同事时汉化能减少很多基础沟通成本。2.4 编写一个可复用的 DockerfileSpringBoot 项目的 Dockerfile 推荐用多阶段构建把“编译环境”和“运行环境”分开最终镜像只保留运行所需的最小内容。一个比较稳妥的模板是这样# 第一阶段编译 FROM maven:3.9-eclipse-temurin-17 AS builder WORKDIR /workspace COPY pom.xml . RUN mvn dependency:go-offline COPY src ./src RUN mvn clean package -DskipTests # 第二阶段运行 FROM eclipse-temurin:17-jre WORKDIR /app COPY --frombuilder /workspace/target/*.jar app.jar EXPOSE 8080 ENV TZAsia/Shanghai ENTRYPOINT [java, -jar, /app.jar]这里的关键点是mvn dependency:go-offline。它在 COPY src 之前先把所有依赖下载好这样源码改动后重新构建时能充分利用 Docker 的层缓存——只要 pom.xml 没变这一层就不会重新执行镜像构建速度会快很多。注意运行阶段用的是eclipse-temurin:17-jre比默认的 openjdk 镜像更省空间也是目前社区推荐的 Java 运行镜像。另外我把时区在镜像里显式设好了这样容器内 Java 进程取到的时间就是北京时间配合 Jenkins 上的时区设置日志时间线完全对齐。3. 核心流水线脚本与落地实操3.1 Jenkinsfile 基础框架流水线的核心是 Jenkinsfile它用代码描述整条构建流程。我建议用声明式语法它对新人友好结构一眼能看懂而且 Blue Ocean 的可视化兼容性最好。一个可以直接用的最小框架如下pipeline { agent any tools { maven maven-3.9 jdk jdk-17 } environment { REGISTRY registry.cn-hangzhou.aliyuncs.com/demo IMAGE_NAME order-service IMAGE_TAG ${BUILD_NUMBER} } stages { stage(检出代码) { steps { checkout scm } } stage(单元测试) { steps { sh mvn test } } stage(打包构建) { steps { sh mvn clean package -DskipTests } } stage(构建镜像) { steps { script { sh docker build -t ${REGISTRY}/${IMAGE_NAME}:${IMAGE_TAG} . } } } stage(推送镜像) { steps { script { withCredentials([usernamePassword( credentialsId: docker-registry, usernameVariable: REGISTRY_USER, passwordVariable: REGISTRY_PASS )]) { sh echo ${REGISTRY_PASS} | docker login ${REGISTRY} -u ${REGISTRY_USER} --password-stdin docker push ${REGISTRY}/${IMAGE_NAME}:${IMAGE_TAG} } } } } } }注意几个地方。Maven 和 JDK 的配置要在“全局工具配置”里提前配好环境中定义的 tools 会保证每个 stage 使用统一的编译工具版本。IMG_TAG 用构建号BUILD_NUMBER好处是每次构建产物的标识唯一回滚时直接指定上一个构建号对应的镜像即可。3.2 参数化构建与多分支策略实际使用中你不可能只构建 master 分支。功能分支、预发分支、打标签发布这些都是常见场景。用参数化构建可以让你在点击“立即构建”时选择分支和动作。在 Jenkinsfile 顶部增加参数定义pipeline { agent any parameters { string(name: BRANCH, defaultValue: main, description: 要构建的分支) choice(name: ACTION, choices: [build, deploy, build-deploy], description: 执行动作) } stages { stage(检出代码) { steps { checkout([$class: GitSCM, branches: [[name: ${params.BRANCH}]], userRemoteConfigs: [[url: https://git.example.com/demo/order-service.git]]]) } } ... } }如果想要更自动化的体验可以装 Git Parameter 插件构建时以下拉列表的方式展示所有远程分支和标签选一个点构建就行不用手输。多分支流水线是另一套思路Jenkins 会自动扫描仓库的所有分支为每个分支生成一条流水线你可以为不同分支单独配置是否“自动构建”、“自动部署”。适合做大项目时区分开发环境、测试环境、生产环境。3.3 测试、构建、推送环节的细节打磨单测阶段不要只跑mvn test就完事一定要让流水线在测试失败时“红掉”。Maven 默认行为就是测试失败会导致构建失败所以这个 stage 的核心价值是尽早阻断——与其等镜像推上去了再发现代码有问题不如在第一步就拦住。我还会在测试阶段加一个干净的环境执行防止本地 target 目录里的瞬时产物干扰测试结果stage(单元测试) { steps { sh mvn clean test } post { always { junit target/surefire-reports/*.xml } } }这句 junit 指令会把单测报告收集到 JenkinsBlue Ocean 界面里可以直接看到哪些用例挂了、报错内容是什么。之后版本迭代多了还能看到测试趋势图这个积累对团队质量建设很有价值。构建镜像环节有一个容易被忽略的问题构建缓存导致的“假构建”。如果你的 Dockerfile 里 COPY src 这层缓存生效了但构建上下文里有旧 target 目录则可能把老 jar 打进去。所以我通常在 mvn clean package 之后才执行 docker build并且如果用了多阶段构建则依赖层面的缓存没问题问题只可能出在业务代码那几层那层缓存本来就是按需失效的不用太过担心。推送镜像时前面提到用 withCredentials 取用户名密码做 docker login。更安全的做法是做一层封装使用 Docker Pipeline 插件的 docker.withRegistrystage(推送镜像) { steps { script { docker.withRegistry(https://${REGISTRY}, docker-registry-creds) { docker.image(${REGISTRY}/${IMAGE_NAME}:${IMAGE_TAG}).push() } } } }这样 Jenkins 会在执行期间自动完成登录凭证也不会出现在构建日志里比手动拼 shell 的方式干净很多。3.4 自动化部署与容器生命周期管理走到部署这一步我推荐用 Docker Compose 做容器编排。哪怕只有一个容器也建议用 Compose因为部署参数端口、挂载卷、环境变量、重启策略都固化在 docker-compose.yml 里可审查、可版本化。docker-compose.yml 一个示例services: order-service: image: registry.cn-hangzhou.aliyuncs.com/demo/order-service:${TAG} container_name: order-service ports: - 8080:8080 environment: - TZAsia/Shanghai - SPRING_PROFILES_ACTIVEprod restart: unless-stopped这里用环境变量 TAG 来控制部署的镜像版本。Jenkins 部署 stage 可以这样写stage(部署服务) { steps { sh export TAG${IMAGE_TAG} docker compose -f /opt/deploy/docker-compose.yml up -d sleep 15 curl -sf http://localhost:8080/actuator/health || exit 1 } }注意我用的是 /opt/deploy/docker-compose.yml 这个固定路径而不是项目仓库里的 compose 文件。原因是部署用的 compose 文件往往会包含生产环境的特殊配置比如某些内网地址、流量控制参数不该和测试环境共用一份。部署文件由运维统一维护流水线只管把 TAG 传过去。curl -sf探测健康检查端点这个动作是对“容器起来了”这个状态做二次确认。有时候容器起来了但应用没起来比如数据库连接不上这时候 health 接口会返回非 2xx流水线就会失败不会出现“明明部署失败你还以为成功”的情况。3.5 触发方式与通知机制流水线准备好之后最后一步是让构建“自动发生”。两种常见触发方式Webhook在代码仓库GitLab/Gitea/GitHub配置 Webhook推送代码时请求 Jenkins 接口触发构建。这种方式时效性最高但要保证 Jenkins 能被仓库服务器访问到且需要配置 token。轮询Jenkins 每隔一段时间检查仓库有没有新提交。配置简单但有时差而且会浪费不必要的请求。我推荐 Webhook。如果用的是 GitLab在 Jenkins 里安装 GitLab 插件然后配置令牌即可。如果你用的是 Gitea 或 Gitee也都支持类似方式。通知方面不一定要接邮件。我在实际项目里用企业微信或者钉钉机器人 Webhook构建失败时往群里推一条消息包括分支、构建号、失败阶段和日志链接。因为大家看群消息的频率远高于看邮件。实现方式就是在流水线末尾加一个 post 块post { success { sh curl https://qyapi.weixin.qq.com/cgi-bin/webhook/send?keyxxx -H Content-Type: application/json -d {\msgtype\:\text\,\text\:{\content\:\构建成功order-service #${BUILD_NUMBER}\}} } failure { sh curl https://qyapi.weixin.qq.com/cgi-bin/webhook/send?keyxxx -H Content-Type: application/json -d {\msgtype\:\text\,\text\:{\content\:\构建失败order-service #${BUILD_NUMBER}\}} } }4. 真实环境里的问题排查实录4.1 Jenkins 容器里用不了 Docker 命令这个问题的经典报错是Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running?或者docker: command not found。有两个层次的原因。一是容器里根本没有 docker 命令这需要在镜像里装 Docker CLI或者在启动容器时把宿主机的 docker CLI 挂载进去。二是有命令但连不上守护进程这就是 socket 没挂对。最直接的解决方案就是启动 Jenkins 容器时加一行-v /var/run/docker.sock:/var/run/docker.sock这样 Jenkins 容器内执行 docker 命令时其实是和宿主机上的 Docker daemon 通信。注意这种方式有个安全代价Jenkins 容器拥有了宿主机 Docker 的全部权限相当于能执行任意容器命令。如果是生产环境建议使用更严格的安全控制比如在 Jenkins agent 容器上做限制或者独立出一台专门做构建的机器不要和企业核心服务器混用。4.2 时区、编码与镜像源那些事时区问题在构建日志里非常隐蔽。表现为 Jenkins 页面时间正常但应用日志时间全是 UTC差 8 小时。排查半天才发现不是代码问题是容器时区没设。解决方案是三层同步Jenkins 容器启动时设置-e TZAsia/ShanghaiDockerfile 运行镜像内设置ENV TZAsia/Shanghaidocker-compose.yml 里同样设置TZAsia/Shanghai编码问题也很常见。Windows 上写的代码、配置的 Jenkins跑出来的日志可能是乱码典型场景是 Maven 编译时控制台输出中文测试名变成问号。解决思路是保证三处编码统一构建机系统编码为 UTF-8、Jenkins 里设置系统属性-Dfile.encodingUTF-8、pom.xml 里设置project.build.sourceEncoding为 UTF-8。另外Jenkins 默认插件安装源在国外第一次装插件经常卡死或者超时。同样可以改成国内镜像在“插件管理→高级”里替换更新站点地址为国内源不然光是装基础插件就能耗掉一下午。4.3 测试通过但镜像却没更新这是一个“看起来成功实际上失败”的经典场景。现象是流水线全绿新代码也推送了但部署后功能还是旧的。排查方向有两个。第一个方向代码根本没进镜像。检查流水线的“检出代码”阶段是不是真的拉到了最新分支。特别是手动触发构建时如果没选对分支就会用旧代码完成整条流水线而旧代码当然能通过测试。这也是我后来坚持在参数化构建里把“当前分支”做成默认值的原因尽可能避免人为选错。第二个方向镜像缓存导致的旧 jar 包。如果你没有用多阶段构建而是直接在一个带 Maven 的镜像里 COPY 源码然后构建那么 Docker 会按层缓存只要 Dockerfile 里前面的指令没有变化后续层也不会重新执行最终打进去的可能是历史 jar。用多阶段构建能大幅缓解这个问题只要 COPY src 那层的校验值变了后续就会被重新执行。4.4 一个踩坑速查表我把这几年被问得最多的几个问题做成一张表方便遇到问题时快速定位现象可能原因解决方向构建机拉取镜像超时Docker Hub 网络不稳定配置国内镜像加速Jenkins 镜像构建失败提示 socket 连接异常未挂载 docker.sock启动 Jenkins 容器时加挂载参数容器启动后日志全部是 UTC 时间未设置 TZ 环境变量Dockerfile、compose、Jenkins 三层同步设置构建产物是历史版本Git 分支选错或 Docker 缓存校验分支参数使用多阶段构建部署成功但服务一直重启应用启动失败健康检查未通过查看容器日志确认数据库等外部依赖可访问插件安装失败默认更新源网络慢更换国内插件更新站点SpringBoot 3.x 项目构建报 JDK 错误构建环境 JDK 版本过低统一 Jenkins 工具链与项目要求的 JDK 版本4.5 权限与安全配置建议安全这块容易被忽略但真出事的时候代价很大。我的建议是按下面几条做基础防护Jenkins 开启全局安全配置创建普通用户账号不要所有人共用 admin。用于镜像仓库登录的凭证尽量用一个权限受限的机器人账号避免使用管理员令牌。Jenkins 容器和部署服务器之间如果走 SSH建议用专门的部署密钥而不是开发者的个人密钥。不要把镜像仓库凭证、服务器密码直接写在 Jenkinsfile 或 docker-compose.yml 里一律通过 Jenkins 的凭据管理机制注入。定期清理构建机上的旧镜像和 Docker 悬空数据执行docker system prune -f避免磁盘被占满后触发莫名奇妙的构建失败。5. 后续可以继续扩展的方向这套流水线跑稳之后其实已经能满足大多数中小团队的日常发布需求了。但如果团队质量和规模化在往前走下面几个方向是我建议考虑接入的代码质量门槛在测试阶段后加一个 SonarQube 扫描如果代码质量指标不达标比如代码覆盖率低于设定阈值则流水线直接失败。这听起来有点严格但确实能逼着团队把单测质量做上去。自动化接口测试SpringBoot 服务部署到测试环境后自动跑一轮 Postman/Newman 或基于 Testcontainers 的集成测试把部署后的活体验证也纳入流水线。Kubernetes 部署当服务数量多了单机 Compose 管理起来会越来越困难。可以考虑把最后一步换成更新 Deployment 镜像或 Helm Chart流水线前面的部分几乎可以原封不动地保留。制品版本管理镜像 tag 目前用的是构建号但发布到生产时更适合用语义化版本号。可以让打标签的发布走独立分支触发流水线时读取 pom.xml 里的版本号作为镜像 tag这样制品标识和代码版本就形成了可追溯的对应关系。在我实际使用的过程中感受最深的还不是省了多少时间而是开发、测试、部署之间的沟通成本真的降下来了。以前“这个东西我本地是好的”是团队里最有杀伤力的一句话有了统一流水线之后这句话的含金量被彻底消解了——每个人都用同一套路径验证代码所有环境跑的都是同一个产物的不同版本。如果你也在被手动部署反复折磨不要犹豫挑一个空闲的周末把这一整套搭起来后续收益会远超你的预期。
返回列表