资讯动态

SpringCloud+Jenkins+Docker持续部署积木学堂:微服务发布流水线实战

发布时间:2026/10/14 10:22:37 来源:尧图企业网站定制
上周四晚上十一点我还在手动部署积木学堂这套SpringCloud项目。服务不多六个微服务外加Nacos但每次发版都要先在本地编译再通过scp上传服务器然后按照依赖顺序一个个启动。传到第三个服务的时候我突然反应过来这个流程里我就是一个“人肉自动化脚本”还是一个特别容易出错的脚本。标题里的“SpringCloudJenkinsDocker持续部署积木学堂[谷粒学院]”就是那次之后我折腾出来的结果。这篇文章不聊虚的直接讲清楚为什么这套组合能解决微服务发布的问题、我的流水线是怎么写的、以及那些文档里不会告诉你的坑。适合正在维护微服务项目、又被手动发布折磨的开发或者运维朋友参考。1. 积木学堂为什么要做持续部署手动发布的三座大山1.1 积木学堂是什么一套常见的SpringCloud微服务在线教育项目积木学堂内称谷粒学院是一个典型的在线教育微服务项目站点有首页门户、课程展示、视频点播、订单支付后端用SpringCloud做了微服务拆分。整个项目拆完大约是六七个可独立部署的服务网关、认证、用户、课程、订单、视频服务中间件用到了Nacos注册中心配置中心、MySQL、Redis。每个服务最终打成一个可执行jar包单包体积100MB上下。这种复杂度是你做持续部署的前提。不是说服务少就不能做自动化而是服务一旦变多手动维护成本会指数上升。更麻烦的是这些服务之间不是孤立的启动有先后依赖关系手动部署时必须把顺序记在脑子里时间一长谁都有可能记错。1.2 痛点一服务多了启动顺序全靠脑子记微服务部署首先要处理启动顺序。比如Nacos如果不先起来后面的服务启动时注册不上或者从配置中心拉不到配置启动直接失败。我之前就吃过一次亏那天只改了用户服务想着单独重启用户服务就行结果用户服务启动时往Nacos注册失败日志又没看全过一会儿订单服务调用用户服务一直超时。排查到最后才发现根本不是代码问题而是Nacos在服务启动期间刚好做了一次重启注册信息没真正写入。这种问题在手动部署场景下特别容易发生。因为每个人的部署习惯不一样有人习惯先启网关有人习惯先启业务服务产生的现象五花八门。而当服务多到五个以上时靠人肉记忆依赖顺序本质上就是在赌运气。1.3 痛点二环境配置漂移“本地能跑线上崩”微服务项目最典型的问题是环境差异。本地IDE调试的时候数据库地址、Redis地址、Nacos地址全部是localhost到了服务器上就变成内网IP或者Docker容器地址配置文件稍微改错一个地方整个服务起不来。手动部署时最常见的循环是这样的本地打包传到服务器启动报错连不上数据库回本地改配置重新打包上传。一次发布折腾一晚上最后发现只是配置文件里少写了一个端口段。持续部署解决的就是把环境定义和部署流程标准化一旦流水线正确跑通环境配置就从“每次发版靠人为记忆”变成“镜像加配置中心的产物”。开发环境、测试环境、生产环境之间的差异被收敛到了配置中心这一个地方而不是散落在每台服务器的jar包旁边。1.4 痛点三发版记录缺失回滚靠猜手动发版还有一个隐藏问题当前线上跑的到底是哪个版本的包往往没有记录。很多团队的做法是“反正我本地有这个jar包传上去跑就行”。一旦出问题要回滚就回本地找旧包能不能找到完全靠运气。Jenkins加Docker这套方案天然带版本概念每个镜像有唯一tag流水线记录每次构建对应的代码版本发布本身变成一次参数化操作。回滚也只是把镜像tag指回旧版本不需要重新编译也不怕旧包被覆盖。这件事对长期维护的项目来说特别重要属于“用一次就回不去”的能力。手动部署不是不能干而是当服务数量、发布频率、环境数量都上来之后人不可能稳定地不出错。持续部署的核心并不是自动化工具本身而是把发布过程从“手工步骤”变成“可重复、可追溯的参数化流程”。这就是我做这套东西的出发点。2. 整体架构与前期准备先画图纸再动服务器2.1 部署链路中每个角色的职责动手之前先把整条链路在脑海里过一遍。代码从开发者提交到服务更新中间要经过四个环节代码仓库、持续集成服务Jenkins、镜像仓库、目标服务器。我用一张表格理清它们的职责后面写脚本的时候边界就清晰了角色承担什么我用的方案代码仓库保存源码、触发构建内部自建的Git服务配置WebhookJenkins拉代码、编译、构建镜像、部署宿主机安装配Pipeline插件镜像仓库保存不可变版本镜像自建私有镜像仓库内网拉取快目标服务器运行MySQL、Redis、Nacos与微服务容器两台2核4G Linux这个架构里有一个关键设计编译和构建镜像在Jenkins主机上完成线上服务器只负责拉镜像和跑容器。为什么这样设计因为如果每台服务器都编译代码那么环境差异问题就又回来了。而镜像一旦构建出来里面是什么环境就是什么环境复制到任何一台装Docker的机器上行为完全一致。这一条是持续部署能少踩坑的最底层原因。2.2 Docker与基础环境初始化新服务器到手后不要急着跑容器先把基础环境固定下来。建议安装Docker后立刻做三件事配置镜像加速、设定时区、开启Docker服务并设为开机自启。比如在Linux上# 安装 Docker不同发行版命令有差异这里以常见发行版为例 sudo curl -fsSL https://get.docker.com | bash # 设置 registry-mirrors拉公共镜像会快不少 sudo mkdir -p /etc/docker sudo tee /etc/docker/daemon.json /dev/null EOF { registry-mirrors: [], log-driver: json-file, log-opts: { max-size: 100m, max-file: 5 } } EOF sudo systemctl daemon-reload sudo systemctl restart docker sudo systemctl enable docker # 容器里统一使用东八区避免日志时间少8小时 echo TZAsia/Shanghai | sudo tee -a /etc/profileregistry-mirrors 我故意留空实际填你那边最快的公共镜像加速地址就行。日志参数也建议提前设置不然Docker的json-file日志默认无限增长两三个月就能把磁盘写满。这个问题我后来是在清理磁盘空间时才意识到的早配置能省很多事情。2.3 目录与Compose文件规划整台服务器上我把所有容器定义和数据放在/opt/edu/compose下面原因很简单一个目录搞定所有服务文件备份、迁移都方便。目录结构大概是/opt/edu/compose/ ├── docker-compose.yml ├── .env # IMAGE_TAG 等环境变量 ├── mysql/ │ ├── data/ # MySQL数据目录 │ └── conf/ # my.cnf ├── redis/ │ └── data/ └── logs/ # 各容器日志的宿主机挂载点docker-compose.yml 会把基础组件MySQL、Redis、Nacos和微服务都定义在一起。有人会问为什么不用一堆docker run命令而是用Compose我的理由是Compose方便表达服务之间的依赖关系一条docker compose up -d能按照定义顺序和依赖关系拉起服务整体启停也是统一操作。更重要的是我把镜像tag写进了 .env 文件每次发布只需要改IMAGE_TAG这一个参数然后执行up -dCompose会自己对比镜像并重建需要更新的服务。这正是部署的基本诉求。2.4 一个容易被忽略的点镜像tag设计动手写流水线前先把镜像tag规则定下来。我的规则是“代码提交哈希加Jenkins构建号”。具体来说Jenkins取当前构建对应的 GIT_COMMIT 前7位作为默认tag比如 d3f4a2e同时支持手动指定。这样每次发布都等于给“某一刻的源码”盖了一个永久戳回滚时直接找回旧tag就能恢复。不要用latest这种可变tag。latest 的问题在于你昨天拉下来的 latest 和今天拉下来的 latest 可能是完全不同的两个镜像很难确认线上跑的是哪个版本。用不可变tag之后镜像仓库里的每一个镜像都对应唯一一次代码提交部署和排查都变得有据可查。3. Jenkins流水线核心脚本从源码到镜像再到服务器3.1 Jenkins侧准备宿主机版还是容器版关于Jenkins的安装方式我自己最终选择了宿主机直接安装而不是用官方容器镜像跑。原因很现实流水线里大量shell命令依赖 mvn、docker、git 这些工具如果Jenkins跑在容器里要么在容器里额外装一遍工具链要么把宿主的命令挂载进去还要处理jenkins用户对docker.sock的权限问题。与其纠结这些不如直接让Jenkins进程跑在宿主机上以root身份执行构建任务简单直接个人项目和小团队场景完全够用。如果一定要用容器版Jenkins至少要做这几件事挂载/var/jenkins_home做数据持久化挂载/var/run/docker.sock和宿主docker二进制同时把/var/lib/jenkins/workspace也映射出来否则流水线里的sh命令会找不到依赖或权限不足。一个简单的验证方法在Jenkins的“脚本命令行”里执行def out new ProcessBuilder(docker,version).execute().text; println out能输出版本说明权限通了否则就是挂载或用户组配置有问题。这一步走通后面就顺了。创建流水线任务时选Pipeline类型然后配置Git仓库地址和访问凭据在“Pipeline Script from SCM”里指向仓库根目录下的 Jenkinsfile。首次配置时最容易漏掉的是凭据拉取私有Git仓库需要配一个账号密码或SSH key构建镜像需要配镜像仓库的账号密码远程部署需要配目标服务器的SSH key。这三类凭据全部放进Jenkins的凭据管理里通过插件按需注入。3.2 一份能直接改改就用的Jenkinsfile下面这份脚本是我在积木学堂项目里实际使用的精简版本以“课程服务”为例。它做的事情很直接拉代码、编译、构建镜像、推送、远程部署。pipeline { agent any environment { REGISTRY registry.internal.example REPO edu-platform SERVICE course // 切换服务时改这里 COMPOSE_HOST deployapp-server COMPOSE_DIR /opt/edu/compose IMAGE_NAME ${REGISTRY}/${REPO}-service-${SERVICE}:${GIT_COMMIT.take(7)} } stages { stage(拉取代码) { steps { checkout scm // 从Git仓库拉取当前分支 } } stage(Maven编译) { steps { // 只编译指定服务-am表示同时编译依赖的公共模块 sh mvn clean package -DskipTests -pl service-course/service-course-server -am } } stage(构建镜像) { steps { // Dockerfile 放在服务模块目录里只COPY打好的jar sh docker build -t ${IMAGE_NAME} service-course/service-course-server/ } } stage(推送镜像) { steps { withCredentials([usernamePassword( credentialsId: private-registry-cred, usernameVariable: REG_USER, passwordVariable: REG_PASS )]) { sh docker login ${REGISTRY} -u ${REG_USER} -p ${REG_PASS} docker push ${IMAGE_NAME} } } } stage(远程部署) { steps { sshagent([deploy-ssh-key]) { sh ssh ${COMPOSE_HOST} cd ${COMPOSE_DIR} echo IMAGE_TAG${GIT_COMMIT.take(7)} .env docker compose up -d } } } } }这份脚本已经能跑。课程服务的Dockerfile其实特别简单因为编译已经在Jenkins的Maven阶段做完了镜像只负责把jar包跑起来FROM openjdk:11-jre-slim WORKDIR /app COPY target/edu-service-course-*.jar app.jar EXPOSE 9020 ENTRYPOINT [java,-jar,/app.jar]这里刻意用jre而不是jdk镜像体积能小一半构建和拉取都快。需要注意的是我这里只写了课程服务一个服务。要想部署全部服务可以加一个参数选择服务名然后循环构建和推送或者做多分支流水线不同分支对应不同环境。我的建议是先把单个服务跑通再推广到全量一次全量改动出问题的时候不好排查。3.3 编译阶段为什么用宿主Maven而不是容器Maven最开始我图省事直接在流水线里写docker run -v $WORKSPACE:/app maven:3.8-openjdk-11 mvn clean package。第一次构建能过但第二次、第三次每次都重新下载依赖。整个项目几百MB的依赖仓库一次编译跑七八分钟流水线慢得像蜗牛。后来改成宿主机直接装Maven复用宿主机的 ~/.m2 本地仓库。第一次全量下载后后续每次构建只下载增量编译时间从七八分钟降到几十秒。如果你的构建机是Windows或Mac也可以把本地仓库目录挂载到容器里本质是一样的依赖缓存必须复用不要让每次构建都从零开始。还有一个细节编译前建议用-pl service-course/service-course-server -am只编译当前服务和它依赖的公共模块而不是每次全量编译整个项目。积木学堂有十几个Maven模块全量编译浪费大量时间而实际上每次发布往往只动一两个服务。3.4 镜像构建与推送tag和登录都是重点镜像是整套持续部署的交付物这里有几个细节值得展开。第一tag不能是 latest这一点前面说过。第二构建命令的上下文要传对Dockerfile所在目录就是构建上下文根目录否则 COPY target/*.jar 会找不到文件。第三docker login和push的关键在凭据绝对不要把密码明文写进Jenkinsfile。我用Jenkins的Credentials Binding插件在withCredentials块里动态注入账号密码凭据只在运行时以环境变量形式存在脚本本身提交到Git仓库也不会有泄露风险。构建命令docker build -t ${IMAGE_NAME} service-course/service-course-server/的最后一个参数是构建上下文也就是Dockerfile所在目录。这个目录里必须有 target 子目录里面的jar包是Maven编译阶段刚生成的。如果上下文给错Docker会报文件找不到排查起来又得浪费半天。3.5 远程部署阶段SSH执行还是开放Docker API远程部署有两条路线一条是Jenkins通过SSH登录目标主机执行命令另一条是给目标主机开放Docker API比如2375端口Jenkins直接调用远程Docker接口。我的选择是SSH。理由很直接Docker API一旦开放TCP端口等于把这台机器的一个重要管理接口暴露给网络。如果安全组和防火墙没配好公网扫描到2375端口轻则被人拉一堆挖矿容器重则整个内网被攻破。而SSH方式下目标主机只接受22端口连接Jenkins用密钥登录能执行的命令范围由我在脚本里严格控制。这里也建议你检查一下自己的Docker配置如果/etc/docker/daemon.json里有hosts: [tcp://0.0.0.0:2375]请立刻移除改用SSH方式管理远程Docker。目标主机上执行的命令很简单先进入compose目录把新的 IMAGE_TAG 写入 .env然后执行docker compose up -d。Compose会自动对比运行中的容器和新镜像需要更新的服务自动重建没变化的保持不动。4. 容器化微服务的经典坑地址、顺序、配置日志4.1 localhost 是容器自己不是宿主机这是很多第一次把微服务放进Docker的人必踩的坑。在容器里localhost指向容器自身但你的MySQL、Redis、Nacos往往在其他容器或宿主机上。所以微服务连接数据库不能写localhost:3306而是要写compose文件里定义的服务名比如mysql:3306、redis:6379、nacos:8848。如果使用Nacos做注册中心服务注册地址也要注意有时候需要显式指定注册IP。我刚开始没注意这一点服务一直注册不上Nacos后来把bootstrap.yml里的地址改成服务名才解决。一个常见做法是在每个微服务的bootstrap.yml里把注册中心和配置中心地址写成spring: cloud: nacos: server-addr: nacos:8848 config: file-extension: yaml启动后服务会优先从Nacos配置中心拉取数据库、Redis等其他配置环境差异被收敛到配置中心一个地方。这个“配置集中化”的思路是微服务体系里特别重要的一环值得花时间做好。4.2 启动顺序用 healthcheck 而不是死等几秒很多人写docker-compose的时候用depends_on控制启动顺序结果发现顺序是保证了服务却还是起不来。原因是depends_on默认只保证“容器启动”了并不保证“容器内服务就绪”。我见过一个项目Nacos容器明明是running状态但应用服务往里注册时Nacos还在初始化连接失败直接退出。正确做法是给基础组件加健康检查然后让应用依赖condition: service_healthy。比如Nacos容器可以这样定义nacos: image: nacos/nacos-server:v2.2.0 ports: - 8848:8848 - 9848:9848 environment: MODE: standalone PREFER_HOST_MODE: hostname healthcheck: test: [CMD-SHELL, wget -q -O - http://localhost:8848/nacos/v1/console/health/readiness || exit 1] interval: 15s timeout: 5s retries: 20 start_period: 20s然后微服务这样写edu-service-course: image: registry.internal.example/edu-platform-service-course:${IMAGE_TAG} depends_on: mysql: condition: service_healthy nacos: condition: service_healthyCompose会等MySQL和Nacos真正可用了再启动课程服务。这一套用熟了“启动顺序靠脑子记”的问题基本不会再出现。4.3 环境隔离用Nacos namespace区分开发和测试项目跑起来后配置文件会越改越多。今天改了数据库连接串明天加了Redis密码如果没有环境隔离开发环境改坏一个配置生产环境跟着出问题。Nacos配置中心天然支持namespace我把开发环境和生产环境放在不同的namespace里运行参数通过--spring.cloud.nacos.config.namespace区分。这样做的效果是同一套镜像只要在启动命令里指定不同namespace就能跑出不同环境。真正做到了“一套产物多环境部署”。这一步最好在流水线跑通之前就做好否则等系统跑起来后再改配置中心结构涉及的服务重启排查成本会高很多。4.4 日志别往容器里写堆文件SpringBoot默认日志输出到控制台这个在Docker场景下反而是最合理的docker logs直接看日志跟随Docker的json-file驱动写入宿主目录。千万别在应用配置里开启日志文件写入路径因为容器重建后容器内部文件系统就没了日志也跟着没了。遇到过业务方要排查上周的问题因为日志写在容器内容器一重建日志全没了只能干瞪眼。简单做法是容器日志由Docker统一管理配合前面daemon.json里的max-size和max-file限制大小。要更完整的日志归档后面再接日志采集组件把容器stdout统一收集起来。这一步等整套流程稳定之后再补也不迟初期完全够用。4.5 端口与网络安全安全组和防火墙最容易被忽略微服务部署还有一件容易被忽略的事机器安全组或防火墙放行规则。我第一次把全部服务用Compose跑起来后在办公室用浏览器访问网关9000端口一直超时在服务器上curl却正常。排查一圈才发现是安全组没放行9000端口。如果你也出现“服务器内部正常外部死活连不上”这种症状先去检查安全组和防火墙不要第一时间怀疑代码。另外再次强调不要给Docker daemon开放0.0.0.0:2375。远程管理Docker可以用SSH隧道做端口转发或者用防火墙限定来源IP。这个底线一定要守住。5. 一次完整上线的实操记录与回滚演练5.1 从提交代码到服务更新全过程时间记录流程跑通后一次课程服务的日常发布大概是这样的改完代码推到Git仓库Jenkins的Webhook自动触发流水线然后我在网页上盯着任务跑。整个流水线耗时分布大致如下2核4G的构建机阶段耗时说明拉取代码2s仓库不大秒级Maven编译40-60s有本地依赖缓存增量编译Docker构建镜像15-25sjar包COPY进镜像很快推送镜像10-20s内网镜像仓库速度快SSH远程部署30-40s拉镜像、启动容器、等服务起来合计约2分钟全程无手工操作而之前手动发布单是编译打jar包就超过5分钟接着scp、登录、docker run、改配置一轮下来怎么也要15分钟。省下来的时间是一方面可控性和可重复性才是更值钱的部分。5.2 回滚一个tag搞定不用重新编译持续部署带来的另一个大红利是回滚变成一次参数操作。比如当前线上课程服务镜像tag是 d3f4a2e发布后业务方反馈有问题想回到上一个版本旧tag是 8c19a0f。我只需要去目标服务器执行cd /opt/edu/compose echo IMAGE_TAG8c19a0f .env docker compose up -dCompose会把课程服务容器重新创建为旧镜像版本整个回滚不到30秒。这在手动部署时代是不可想象的你得找到旧jar包重新上传还要祈祷旧包没被人覆盖过。所以每一次发布我都坚持在tag里带GIT_COMMIT前7位就是为了让回滚永远有据可查。5.3 小规模微服务Compose够用别急着上K8s每次聊到SpringCloud部署总会有人问为什么不上K8s我的回答是看规模和团队阶段。积木学堂也就六七个微服务两台机器就能撑起来Compose的抽象能力和运维成本完全够用。K8s带来的调度、伸缩、自愈能力在这个规模下体现不出来反而倒逼团队学习一整条新的技术栈。等到服务数量上到二三十个或者需要弹性伸缩时再考虑K8s不迟。到那时这套Jenkins流水线依然能用只需要把“远程SSH执行compose”这个部署步骤换成“更新Deployment镜像并apply”即可。其余编译、构建、推送逻辑完全不需要改动。这也是我坚持把构建和部署分离的原因构建逻辑稳定部署方式可以随时替换。5.4 流水线的下一步优化方向这套链路跑稳之后我陆续补了几个小功能。第一是Webhook自动触发推代码进仓库后不需要人工去点“立即构建”。第二是加了一个DEPLOY_ENV参数同一份Jenkinsfile可以用于开发和生产环境环境差异收敛到Nacos的namespace。第三是发布完成后给IM机器人发一条消息带上提交人和构建号群里的同事都知道这次是谁在什么时候发布的。第四是定期清理旧镜像和构建工作区防止磁盘被日志和构建产物堆满。这些功能都不复杂但每一条都能明显减少日常维护摩擦。回到底层逻辑持续部署的本质是把发布过程中的变量变成参数。代码版本是参数镜像tag是参数目标服务器是参数环境是参数。人不再需要亲自执行每一个命令只负责做决策——比如这次可以发版有问题就回滚。机器把剩下的活儿全干了而且干得比人稳定得多。我自己做这个项目的最大体会是先别急着上最复杂的技术栈把Jenkins加Docker这条链路先打通让每一次发布都能在两分钟之内完成且随时能回滚这个收益比什么概念都实在。等这条链路稳定了再去研究K8s、服务网格这些更上层的东西你会发现底层思路是完全相通的。最后分享一个小细节如果你也准备给SpringCloud项目做Docker化先把每个服务的基础镜像固定下来不要每次构建时让基础镜像tag浮动不定。基础镜像一变生产环境跑出来就是另一个环境这个坑我踩过希望你们不要再踩。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑