☰
SpringCloud + Jenkins + Docker 微服务持续部署实战
2026/10/15 15:53:59 网站建设 项目流程

日常做单体项目的时候,打包上传服务器、停旧进程、起新进程,一条命令搞定,没什么感觉。一旦服务拆成十几个,每个还要独立维护、独立发布,手工这一套就真的扛不住了。

前段时间我把“积木学堂[谷粒学院]”这套在线教育项目完整部署到了测试环境,用到的就是 SpringCloud + Jenkins + Docker 这条持续部署链路。整个搭完,发布一个微服务从手工半小时缩短到两分钟,而且回滚也只是点一下的事。这篇文章把我从架构拆分到流水线落地的完整过程整理出来,包括 Dockerfile、Jenkins Pipeline、以及一堆文档里不会写的坑。

如果你也准备把微服务项目从手动发布改成自动部署,或者正在为十几个服务怎么管理发愁,可以直接参考我这套方案。

1. 先弄清楚这个项目的部署难点在哪

1.1 积木学堂的微服务结构

积木学堂这种在线教育平台,本身不是一个简单的单体应用。用户端、讲师端、后台管理,加上用户中心、课程中心、订单中心、支付中心、网关、注册中心,大大小小十几个模块。我把它梳理成下面几块:

  • 注册与配置中心:用 Nacos 做服务发现和配置管理
  • 网关服务:Spring Cloud Gateway,统一收口外部请求
  • 业务服务:用户服务、课程服务、订单服务、支付服务、统计服务等
  • 基础中间件:MySQL、Redis、OSS 对象存储、短信验证码服务

这种结构下,如果还靠本地打包然后手动上传服务器再逐个启动,先不说效率,光是“哪个版本部署到哪台机器”这个问题就够让人崩溃。

1.2 为什么需要持续部署

微服务架构下最头疼的不是写代码,而是把代码变成运行中的服务。每次代码提交,都要经过编译、测试、打包、构建镜像、推送镜像、更新容器这一整套流程。如果靠手动执行,流程拉得越长,出错的概率就越高。

我当时踩过一个很典型的坑:本地代码能跑、一上服务器就报错,后来发现是把本地编译好的 jar 包直接传到了服务器,但服务器上的环境变量、配置文件跟本地完全是两套,又没有做统一管理,导致排查了很久。

正是因为这种痛点,我才决定用 Jenkins + Docker 把整个发布流程固定下来。代码提交后自动触发构建,构建通过后自动生成 Docker 镜像并启动容器,整个过程不需要人工登录服务器操作。

1.3 三个工具分别承担什么角色

这三者的分工其实很清晰:

  • Spring Cloud 是项目的架构基础,决定了服务怎么拆分、服务之间怎么通信
  • Docker 负责把每个服务打包成标准化的运行环境,解决“在我机器上能跑”的问题
  • Jenkins 负责把“代码变成镜像”和“镜像变成容器”这两步串联起来,解决自动化的问题

我在实际部署中最大的感受是:Spring Cloud 部分的代码改动反而不是最花时间的,真正费精力的是把每个服务用 Dockerfile 定义好、把 Jenkins 流水线写好,以及处理服务之间的网络通信。这三个点做好了,整套系统跑起来非常省心。

2. 镜像仓库、网络与基础环境准备

2.1 准备阶段要做的三件事

动手部署之前,有三件基础设施必须提前搞定,否则后面会反复返工:

  1. 私有镜像仓库:我用的是 Harbor,用来存放每个微服务构建出来的 Docker 镜像
  2. Docker 网络:创建一个自定义网络,让所有容器可以通过服务名互相访问
  3. Jenkins 服务器:安装好 Jenkins,并且这台机器要能访问 Docker daemon

这里重点说一下 Docker 网络。Spring Cloud 服务之间需要通过服务名(而不是 IP 地址)进行通信,如果你让每个容器跑在默认 bridge 网络下,服务名解析会失败,服务间调用就会报 UnknownHostException。

我创建的 Docker 网络是这样的:

docker network create edu-net

后面所有微服务的容器都指定挂到这个网络下,Nacos、Gateway、各业务模块、MySQL、Redis,全部用服务名互相访问。

2.2 私有镜像仓库的作用

有人可能会问:直接用 Docker Hub 不行吗?行是行,但有两个问题:

  • 公网镜像仓库拉取速度受网络影响大,尤其在高并发部署的时候特别明显
  • 微服务镜像通常包含业务代码,不适合放到公网仓库

我选择 Harbor 的原因是它自带 UI、权限管理,还可以配置镜像复制。如果你嫌 Harbor 重,用 Registry 也是可以的,只是缺少界面管理功能。

安装好 Harbor 之后,需要在每台要拉取镜像的机器上配置信任:

# 编辑 /etc/docker/daemon.json { "insecure-registries": ["192.168.10.10:8082"] }

这里注意,如果你用的 Harbor 是 HTTP 协议,Docker 默认不允许推送和拉取非 HTTPS 的镜像仓库。只有配置了 insecure-registries,Docker 才会允许操作这个地址。这个问题我一开始没注意,Jenkins 里推送镜像一直报错,后来才发现是这里没配。

2.3 数据库与中间件的容器化部署

积木学堂各业务模块共用一个 MySQL 实例,我把 MySQL 和 Redis 也用 Docker 方式部署在同一网络里:

docker run -d --name mysql \ --network edu-net \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORD=root123 \ -v /data/mysql:/var/lib/mysql \ -v /data/mysql/conf:/etc/mysql/conf.d \ mysql:5.7 docker run -d --name redis \ --network edu-net \ -p 6379:6379 \ redis:6

关于数据库初始化,我需要提醒一下:积木学堂的 SQL 脚本文件比较多,包括数据库结构脚本和初始化数据脚本。如果映射宿主机目录时没有把脚本放对位置,容器启动后数据库会是空的,服务启动时连不上表会直接报错。

我当时是把 SQL 脚本放到 /data/mysql/init 目录下,然后通过 docker cp 复制到容器里执行。这里有个更省事的方式:MySQL 官方镜像会自动执行 /docker-entrypoint-initdb.d 目录下的 .sql 脚本,只需要把 SQL 文件挂载到这个目录就行:

-v /data/mysql/init:/docker-entrypoint-initdb.d

但注意这个机制只在“数据目录为空”时才会触发,如果 /var/lib/mysql 已经有数据了,脚本不会重复执行。所以如果你的数据库之前启动过,再挂载这个目录是没用的。

3. 写 Dockerfile:把每个服务打包成标准环境

3.1 服务模块的镜像构建思路

积木学堂项目里的每个微服务,本质上都是一个 Spring Boot 应用,最终产出都是可执行的 jar 包。所以 Dockerfile 的核心逻辑是统一的:基于 JDK 镜像,把 jar 包复制进去,然后用 java -jar 启动。

以课程服务为例,我写的 Dockerfile 是这样的:

FROM openjdk:8-jdk-alpine MAINTAINER edu-dev ENV TZ=Asia/Shanghai RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone ARG JAR_FILE=target/*.jar COPY ${JAR_FILE} app.jar ENTRYPOINT ["java","-jar","/app.jar"]

这个 Dockerfile 有几个细节值得注意:

  • 使用 alpine 版本的 JDK 镜像,体积小,部署速度快
  • 设置时区为 Asia/Shanghai,不然容器日志时间会比本地时间晚 8 小时
  • 使用 ARG 声明 JAR_FILE,构建时可以通过 --build-arg 动态传入

3.2 不同服务依赖的差异化处理

不同服务对基础环境的依赖不一样。比如 Nacos 和 Gateway 本质上也是 Spring Boot 应用,但依赖端口和内存大小不同。我在启动容器时会通过 -e 参数传入 JVM 参数,限制每个容器的内存使用。

积木学堂的服务 JVM 参数我大致是这样配置的:

  • 注册中心 Nacos:内存 512m,端口 8848
  • 网关服务:内存 512m,端口 80
  • 业务服务:内存 512m ~ 1g,端口各自分配

举个例子,网关服务的启动命令:

docker run -d --name edu-gateway \ --network edu-net \ -p 80:80 \ -e JAVA_OPTS="-Xms512m -Xmx512m" \ -e NACOS_ADDR="nacos-server:8848" \ edu-gateway:latest

这里我在环境变量里传入了 Nacos 地址。服务的 bootstrap.yml 里需要配置成从环境变量读取,而不是写死 IP。这样容器 IP 不管怎么变化,服务之间都能通过 Nacos 找到彼此。

3.3 前端镜像的构建

积木学堂有后台管理前端和用户端前端,它们都是 Vue 项目。构建镜像的思路跟后端不太一样,需要先构建静态文件,再放进 Nginx 镜像里。

我当时用了一个多阶段构建的 Dockerfile,示例:

FROM node:16-alpine AS build WORKDIR /app COPY package*.json ./ RUN npm install COPY . . RUN npm run build:prod FROM nginx:stable-alpine COPY --from=build /app/dist /usr/share/nginx/html COPY nginx.conf /etc/nginx/nginx.conf EXPOSE 80

这个多阶段构建的好处是:最终镜像里只保留 Nginx 和静态文件,不包含 Node 环境和源代码,镜像体积从几千兆降到几十兆。

前端镜像要注意 Nginx 配置里的代理转发。积木学堂的前端请求是 /api 开头的,需要把 /api 转发到后端网关:

location /api/ { proxy_pass http://edu-gateway:80/; }

这里踩过很多次的坑是 proxy_pass 后面带不带路径的问题。如果 proxy_pass 写的是 http://edu-gateway:80/,请求 /api/user/info 会转发成 /user/info;如果写的是 http://edu-gateway:80,会保留 /api 前缀。这个区别你需要在 Nginx 里仔细确认,不然会出现前端调接口全部 404 的情况。

4. Jenkins 流水线配置:从手动到全自动

4.1 前后端分离的流水线策略

积木学堂部署的核心思路是前端和后端分开部署。后端每个微服务独立构建,前端单独构建。在 Jenkins 里,我创建了多个任务:

  • edu-service-build:用于构建所有后端服务,可以通过参数选择构建哪个模块
  • edu-web-admin:构建后台管理前端
  • edu-web-user:构建用户端前端

这里我重点说一下后端服务构建任务的设计。

考虑到积木学堂有十几个微服务模块,如果每个模块单独建一个 Jenkins 任务,任务数量太多,维护起来累。我采用了一个带参数的构建任务,通过参数化构建选择要发布的服务。

4.2 后端服务流水线脚本

我用的 Jenkins 流水线脚本,核心步骤如下:

pipeline { agent any environment { DOCKER_REGISTRY = "192.168.10.10:8082" DOCKER_NETWORK = "edu-net" } parameters { choice( name: 'SERVICE_NAME', choices: ['edu-gateway', 'edu-user', 'edu-course', 'edu-order', 'edu-payment', 'edu-statistics'], description: '选择要部署的服务模块' ) string(name: 'TAG', defaultValue: 'latest', description: '镜像标签') } stages { stage('拉取代码') { steps { checkout scm } } stage('构建模块') { steps { script { def serviceName = params.SERVICE_NAME sh "cd ${serviceName} && mvn clean package -Dmaven.test.skip=true" } } } stage('构建镜像') { steps { script { def serviceName = params.SERVICE_NAME sh """ cd ${serviceName} docker build -t ${DOCKER_REGISTRY}/${serviceName}:${params.TAG} . docker push ${DOCKER_REGISTRY}/${serviceName}:${params.TAG} """ } } } stage('部署服务') { steps { script { def serviceName = params.SERVICE_NAME sh """ docker stop ${serviceName} || true docker rm ${serviceName} || true docker run -d --name ${serviceName} \\ --network ${DOCKER_NETWORK} \\ -e NACOS_ADDR="nacos-server:8848" \\ ${DOCKER_REGISTRY}/${serviceName}:${params.TAG} """ } } } } }

这条流水线我用了很长时间,需要说明的是,它有几个设计上的考虑:

  • 每个服务停止的时候用了|| true,防止容器不存在时脚本报错退出
  • 构建模块时加了-Dmaven.test.skip=true,跳过测试,加快构建速度
  • 服务端口在 docker run 时没有指定 -p,因为服务之间是通过 Docker 网络用服务名访问的,不需要暴露宿主机端口

只要不在宿主机上直接访问服务,可以不映射端口。这一点和传统的“每个服务占一个端口”的部署方式是不一样的。

4.3 前端流水线脚本

前端构建流程比较简单,核心是构建静态文件和启动 Nginx:

stage('部署前端') { steps { sh """ docker stop edu-web-admin || true docker rm edu-web-admin || true docker run -d --name edu-web-admin \\ --network ${DOCKER_NETWORK} \\ -p 81:80 \\ ${DOCKER_REGISTRY}/edu-web-admin:${params.TAG} """ } }

前端的端口需要映射出去,因为浏览器要直接访问。后台管理我用了 81 端口,用户端用了 82 端口,各自对应不同的 Nginx 配置。

4.4 部署执行与验证

流水线配置好之后,在 Jenkins 任务页面点击“Build with Parameters”,选择要部署的服务和版本号,点击构建,剩下的就是等结果。

第一次跑完整流程的时候,我盯着控制台输出,每一步都确认有没有报错。顺利跑完一遍大概用了三四分钟。主要耗时在 Maven 依赖下载、镜像推送和容器启动这几块。

验证是否部署成功,我会到 Jenkins 控制台看最后一段输出,确认 docker run 执行成功,然后再到 Nacos 控制台看服务有没有注册上来。如果服务注册成功,说明服务实例健康;如果没注册上来,就需要看容器日志排查。

5. 部署过程中常见的问题与定位方法

5.1 服务注册不上 Nacos

部署中最常见的问题就是“服务一直在 Nacos 里看不到”。排查步骤按顺序来:

  1. 看容器是否在运行:docker ps -a
  2. 看容器日志:docker logs --tail 200 容器名
  3. 确认 Nacos 地址能不能通:进入容器里 ping nacos-server

最典型的错误是配置里写了 localhost 或 127.0.0.1。在容器内,localhost 指向的是容器自身,不是宿主机,也不是 Nacos。必须改成 Nacos 容器的服务名或宿主机 IP。

5.2 微服务互相调用超时

服务都注册上了,但接口调用偶尔超时,这个在积木学堂里头也比较常见。

我遇到过一次比较难查的问题:用户服务调用课程服务时,接口能通但很慢。排查后发现是 Docker 容器的时间不同步,导致某些带时间戳的请求被判定为过期,然后被拦截。

解决办法是在 Dockerfile 里加上时区设置,并且容器启动时挂载宿主机时间:

-v /etc/localtime:/etc/localtime:ro

这个坑比较隐蔽,建议大家部署前就把时区和时间同步的工作做了,后面会省很多事。

5.3 前端接口 404

前端能打开页面,但所有接口请求都返回 404,这个问题十有八九是 Nginx 代理配置的问题。

你需要检查 Nginx 里的 location /api 配置是否存在、proxy_pass 的路径是否正确。我建议用 curl 直接在容器里测试:

docker exec -it edu-web-admin curl http://edu-gateway:80/user/info

如果容器内能通,说明后端没问题,问题出在 Nginx 转发配置上。

5.4 构建机镜像磁盘占满

持续部署用久了,构建机上会堆积大量版本镜像,磁盘占用会越来越大。

我的处理方式是在 Jenkins 流水线的最后加了一个清理步骤:

docker image prune -f

也可以保留最近几个版本,手动清理更早的旧镜像。这个操作建议放在流水线里自动执行,而不是等人去发现磁盘满了再处理。

6. 一点个人经验总结

整套方案跑通之后,我再发布积木学堂的任何一个微服务,操作就是:在 Jenkins 上选一下服务名,点构建,两分钟以后服务就自动更新好了。排查问题的时候,直接看容器日志,版本、时间、状态都清清楚楚。

我个人感觉,持续部署这件事情,本质上不是为了省那几分钟,而是为了让发布过程可控、可预测。手工部署最大的问题不是慢,而是每次操作都可能跟上次不一样。用 Jenkins + Docker 把流程固定下来之后,每次发布都是同一套动作,出问题时能很快定位。

最后给你两个建议。第一,刚开始用这套方案时不要贪多,先拿一个业务服务做试点,跑通了再复制到其他服务。第二,Dockerfile、Jenkins 脚本这些尽量用统一的模板,这样可以避免每个服务的构建方式都不一样,后面维护起来会轻松很多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询