资讯动态

基于Docker封装Git+Maven+JDK8构建镜像实现Spring Boot自动化部署

发布时间:2026/10/15 13:05:22 来源:尧图企业网站定制
在做 Spring Boot 项目的自动化部署时最让人头疼的往往不是 Spring Boot 本身而是它背后的构建工具链。只要换一台机器Git 版本不一致、Maven 仓库地址配错、JDK8 没装或者被系统自带的 JDK11 抢先占用了 JAVA_HOME构建结果就可能变得五花八门。与其在每台服务器上重复折腾环境不如直接把 Git Maven JDK8 封装成一个 Docker 镜像让所有构建动作都在这套标准化的环境里完成。这篇文章记录的就是我实际在做的方案基于 centos:7 构建一个内置 Git、Maven、JDK8 的构建镜像再配合一段部署脚本把 Spring Boot 项目从拉取代码、Maven 编译、Jar 打包到容器启动的全过程串起来实现轻量级自动化部署。它不依赖重量级 CI 服务小团队、个人服务器、内网环境都能直接照搬需要快速上手容器化部署的读者也可以把它当做一个完整的参考样例。1. 方案梳理为什么要把 Git、Maven、JDK8 一起塞进镜像1.1 这套方案解决的是哪一类痛点很多团队的部署流程其实还停留在“半手工”状态登录服务器、手动 git pull、手动执行 mvn package、再手动 kill 掉老进程重启 jar。这套流程在只有一台服务器、一个项目的时候勉强能用但项目一多问题就全出来了。首先是环境不一致。开发机上的 Maven 是 3.8服务器上是 3.5开发机用 JDK8服务器上被某依赖装了个 JDK11Git 的全局配置也五花八门。构建工具链只要有一处不同打出来的 Jar 就可能出问题更别提那种“本地明明是好的一上服务器就编不过”的经典情况了。其次是环境污染。同一台服务器上跑多个项目各自依赖的依赖包版本如果互相冲突处理起来非常消耗时间。就算用mvn dependency:tree慢慢查也往往要折腾大半天。而 Docker 镜像天然隔离了运行环境把工具链放进镜像之后宿主机上装了什么、环境变量乱不乱都跟构建过程没关系了。还有一点容易被忽略构建过程的可复现性。今天能构建成功不代表换了台机器还能成功。用 Docker 封装之后构建环境变成了一个可携带的“标准容器”只要镜像 tag 不变任何机器上执行的构建逻辑都是一样的。1.2 和现成官方镜像比自己拼装的取舍看到这里可能有读者会问Maven 官方不是有现成的 Maven JDK 镜像吗为什么还要自己拼一个确实Docker Hub 上有很多现成镜像但实际用下来有几个具体问题。第一官方 Maven 镜像默认不包含 Git。项目代码如果放在 Git 仓库里容器里没有 Git连代码都拉不下来。你可能会说“在宿主机拉代码再挂载进去”也行但这样就回到老路了宿主机上还是要装一套 Git 环境而且跟镜像里的项目目录结构还得对齐。第二基础镜像版本不一定满足团队要求。有的团队因为历史原因或者客户要求运行环境必须锁定 CentOS 7。官方镜像大多基于 Alpine 或 Ubuntu虽然跑 Java 没问题但一旦涉及系统级操作、调试工具、底层兼容性验证差异就会暴露出来。第三版本控制不可控。官方镜像的 tag 更新策略是按照自己的节奏来的什么时候升级 JDK、什么时候升级 Maven你控制不了。自己拼的镜像可以严格锁定 JDK8 的小版本、Maven 的小版本构建行为完全可控。当然自己拼镜像也有代价需要维护 Dockerfile、需要处理二进制包的获取和许可、需要定期做安全更新。但对比“每次部署都靠手工在宿主机上凑环境”的隐性成本这个代价其实非常值得。还需要澄清一个概念这个“三合一”镜像本质上是构建镜像不是运行镜像。构建镜像可以比较大因为编译本身就需要一堆工具真正跑 Spring Boot 的时候应该用另外的轻量级 JRE 镜像只塞一个 Jar 进去。把构建和运行分开才是标题里“轻量级”三个字的关键。2. 镜像构建的前期准备与 Dockerfile 编写2.1 需要的文件与目录结构在开始写 Dockerfile 之前建议先把所有材料放到同一个目录里方便统一管理和后续备份。我常用的目录结构是这样deploy/ ├── Dockerfile.build ├── Dockerfile.run ├── settings.xml ├── jdk8.tar.gz └── apache-maven.tar.gzJDK8 的二进制压缩包和 Maven 的二进制压缩包需要提前下载好放到 deploy 目录。之所以用压缩包而不是直接在 Dockerfile 里执行 yum install是为了版本锁定。比如 yum 仓库里的 openjdk 版本会随源更新变化今天装的是 1.8.0_312过半年可能就变成 1.8.0_402 了你无法保证每次 build 出来的镜像完全一致。用固定压缩包只要不主动换文件构建结果就是稳定的。Maven 同理。二进制解压安装虽然笨但胜在可控、路径清晰、升级时只需要换压缩包再重建镜像即可。settings.xml 也要提前准备好。这里至少要做两件事配置一个稳定可用的 Maven 镜像源解决依赖下载速度问题再配置好本地仓库路径方便后续挂载持久化。一个参考的 settings.xml 核心内容如下settings xmlnshttp://maven.apache.org/SETTINGS/1.0.0 localRepository/opt/maven/repo/localRepository mirrors mirror idmirror/id mirrorOf*/mirrorOf namemirror/name urlhttps://mirror.example.internal/repository/maven-public//url /mirror /mirrors /settings注意 localRepository 我特意指定到了/opt/maven/repo这个路径后面要用来挂载卷实现依赖缓存在多次构建之间复用。2.2 Dockerfile 完整内容构建镜像的 Dockerfile 我拆成了两个一个是构建环境镜像Dockerfile.build一个是运行环境镜像Dockerfile.run。先看构建镜像FROM centos:7 ENV JAVA_HOME/opt/jdk8 \ MAVEN_HOME/opt/maven \ PATH${PATH}:/opt/jdk8/bin:/opt/maven/bin RUN yum install -y git yum clean all COPY jdk8.tar.gz /tmp/jdk8.tar.gz RUN tar -xf /tmp/jdk8.tar.gz -C /opt \ mv /opt/jdk* /opt/jdk8 \ rm -f /tmp/jdk8.tar.gz COPY apache-maven.tar.gz /tmp/apache-maven.tar.gz RUN tar -xf /tmp/apache-maven.tar.gz -C /opt \ mv /opt/apache-maven* /opt/maven \ rm -f /tmp/apache-maven.tar.gz RUN mkdir -p /opt/maven/repo COPY settings.xml /opt/maven/conf/settings.xml WORKDIR /build CMD [/bin/bash]这个 Dockerfile 逻辑很直白先装 Git再部署 JDK8再部署 Maven最后覆盖 Maven 配置文件。运行镜像的 Dockerfile 要简单得多目标是足够轻量FROM centos:7 RUN yum install -y java-1.8.0-openjdk-headless tzdata curl \ yum clean all ENV JAVA_HOME/usr/lib/jvm/java-1.8.0 \ TZAsia/Shanghai WORKDIR /app ENTRYPOINT [java, -jar]运行镜像里只装 headless 版本的 JRE不装编译工具也不装 Maven。这才是真正给 Spring Boot 用的容器。构建镜像和运行镜像分开之后日常部署的“重头戏”都在构建镜像里完成线上容器始终保持着最小体积。2.3 每一条指令背后的执行细节有几点值得单独拿出来说。yum install -y git后面直接加yum clean all是为了清掉 yum 缓存避免这些缓存文件进入镜像层。如果不清镜像体积会白白增加上百 MB而且这些缓存对运行期完全无用。JDK8 解压后我用的mv /opt/jdk* /opt/jdk8看起来有点“偷懒”但实际很实用。不同版本的 JDK 压缩包解压出来的目录名不一样有的是jdk1.8.0_202有的是jdk8u392-b08用通配符统一移动到固定路径/opt/jdk8可以少改 Dockerfile。Maven 解压后的目录名同样不固定apache-maven-3.6.3这类目录名经常带版本号用通配符移动更省心。ENV 变量集中在文件头部定义不只是为了好看。Docker 构建时只要 ENV 指令本身没变化后续层都能命中缓存如果我把 JAVA_HOME 写在后面的 RUN 里每次改了 RUN 命令之前的层可能失效构建效率会受影响。这里还有一个容易被忽视的点容器里的 Git 默认可能不认识挂载进来的目录。后面挂载代码目录时如果 Docker 使用的是新版本 Git会报一个 “dubious ownership” 错误。遇到这个问题在镜像里加一句git config --global --add safe.directory /build就能解决。3. 构建镜像与轻量化瘦身3.1 构建命令与镜像验证材料备齐后执行构建命令cd deploy docker build -f Dockerfile.build -t build-env:centos7-jdk8 .构建过程中如果网速慢或者 yum 源不稳定可能会在yum install -y git这一步卡住。建议提前把系统源的 baseurl 替换成内网可用的 yum 源不然反复重试很浪费时间。构建完成后先做一次环境验证确保 Git、JDK、Maven 三条命令都可用docker run --rm build-env:centos7-jdk8 bash -c \ git --version java -version mvn -version正常情况下能看到 Git 版本、JDK8 版本和 Maven 版本信息。如果java -version打印出来的不是你预期的版本说明解压后的 JDK 目录可能没被正确移动到/opt/jdk8或者 PATH 里残留了系统自带的 Java。运行镜像的构建也一样docker build -f Dockerfile.run -t jre8-run:latest .构建完可以用docker images查看两个镜像的 size通常运行镜像会明显小于构建镜像。如果 run 镜像体积异常大大概率是 yum clean all 没生效或者装了一些非必要的软件包。3.2 镜像瘦身的四个实操手段我对“轻量级”的理解是构建镜像不应该过度臃肿但也没必要为了省几百 MB 把自己逼疯真正需要瘦身的是线上运行镜像。以下四个手段是我实际操作经验中比较有效的。第一能用系统包就不用源码编译。Git 直接通过 yum 安装而不是下载源码 make install。源码编译不仅慢还会引入 gcc、make 等一整套编译工具镜像体积直线膨胀。第二所有临时安装包在同一个 RUN 里清理干净。比如COPY jdk8.tar.gz后在解压命令的最后用rm -f删掉压缩包。压缩包往往有 100MB 左右必须清理。注意清理命令跟解压命令放在同一个 RUN 层里否则压缩包会残留在上一层删除也减不了体积。第三合并 RUN 指令。Docker 镜像由一层层只读文件系统叠加组成每一层都会占用空间。把yum install、tar、mv、rm通过串在一条 RUN 里能有效减少镜像层数镜像文件数量也会变少。第四运行镜像不保留任何构建产物。Jar 包只通过挂载方式进入运行容器或者复制到一个非常精简的基础镜像里。构建镜像再大也不影响线上容器启动速度和占用空间。3.3 让 Maven 依赖仓库在多次构建间复用这是在实际使用中最影响部署体验的一环。如果不做任何处理每次docker run启动构建容器时Maven 都会在一个全新的文件系统里下载所有依赖。一个中型 Spring Boot 项目的依赖可能有几百 MB首次构建能拖到十几分钟这个速度不可接受。解决办法是在 run 构建镜像时挂载一个 Maven 本地仓库卷docker volume create maven-repo然后在后续的构建容器运行命令里加上挂载参数docker run --rm \ -v maven-repo:/opt/maven/repo \ build-env:centos7-jdk8 \ mvn -version只要 settings.xml 里的 localRepository 是/opt/maven/repo这个 volume 就会把下载好的依赖持久保留。第二次构建访问同一个依赖时会直接命中本地仓库速度提升非常明显。这个 volume 和镜像本身是解耦的以后升级镜像依赖缓存还在。4. Spring Boot 自动化部署脚本的实现4.1 部署脚本的整体编排流程镜像准备好之后下一步就是实现自动化部署。我不建议用太复杂的工具一段 Bash 脚本配合 Docker 命令就足够可靠。整个编排流程是准备应用目录并把它挂载到构建容器的/build。在构建容器内执行 Git clone 或者 Git pull同步最新代码。在构建容器内执行 Maven 打包生成 Jar。从target目录中挑出可执行 Jar复制到应用目录。用精简的 JRE 运行镜像启动容器只挂载这一个 Jar。停止并删除旧容器避免端口冲突。脚本入参我设计为应用名、Git 仓库地址、分支和端口。这样同一套东西可以复用到多个项目只需要换参数不需要改逻辑。4.2 完整脚本和关键步骤说明下面是完整的部署脚本#!/usr/bin/env bash set -euo pipefail APP_NAME${1:?请提供应用名} GIT_URL${2:?请提供Git仓库地址} BRANCH${3:-main} HTTP_PORT${4:-8080} WORKSPACE${WORKSPACE:-/data/workspace} BUILD_IMAGEbuild-env:centos7-jdk8 RUN_IMAGEjre8-run:latest APP_DIR${WORKSPACE}/${APP_NAME}/${BRANCH} mkdir -p ${APP_DIR} docker run --rm \ -v ${APP_DIR}:/build \ -v maven-repo:/opt/maven/repo \ -e GIT_URL${GIT_URL} \ -e BRANCH${BRANCH} \ -e MAVEN_REPO/opt/maven/repo \ ${BUILD_IMAGE} \ bash -c set -e if [ ! -d /build/.git ]; then git clone -b $BRANCH $GIT_URL /build else git -C /build fetch origin git -C /build checkout $BRANCH git -C /build pull origin $BRANCH fi cd /build mvn clean package -DskipTests -Dmaven.repo.local$MAVEN_REPO JAR_FILE$(find ${APP_DIR}/target -maxdepth 1 -name *.jar ! -name *sources* | head -n1) if [ -z ${JAR_FILE} ]; then echo 构建完成但没有找到 Jar 文件 exit 1 fi cp -f ${JAR_FILE} ${APP_DIR}/app.jar docker rm -f ${APP_NAME} /dev/null 21 || true docker run -d \ --name ${APP_NAME} \ -p 127.0.0.1:${HTTP_PORT}:8080 \ -v ${APP_DIR}/app.jar:/app/app.jar:ro \ -e TZAsia/Shanghai \ -e JAVA_OPTS-Xms256m -Xmx512m \ -e SPRING_PROFILES_ACTIVE${SPRING_PROFILES_ACTIVE:-prod} \ --restart unless-stopped \ ${RUN_IMAGE} \ bash -c java $JAVA_OPTS -jar /app/app.jar脚本开头用set -euo pipefail是我踩过坑之后养成的习惯。默认情况下脚本遇到失败命令可能继续往后跑最后明明构建失败了还把旧容器给停了这种事故很容易发生。加上这行之后命令失败立即退出至少能保证不产生更严重的后果。容器内先判断.git目录是否存在存在就 fetch、checkout、pull不存在就 clone。这种写法比每次强制 clone 要稳因为 clone 大仓库时如果网络中断整个目录就废了而增量 pull 只下载差异部分速度快很多。这里要特别说明的是我把 App 的代码编译放在构建容器里但docker run -v maven-repo挂载的是 volume。这样依赖缓存能持久存在但不会把本地的.m2目录和宿主机的文件系统混在一起避免目录权限互相污染。Jar 复制到${APP_DIR}/app.jar之后运行容器通过只读挂载把它映射进/app/app.jar。加:ro是防止容器内程序意外修改 Jar 文件。线上运行镜像里不提前 COPY Jar既有好处也有坏处好处是每次部署不需要重新 build 运行镜像直接重启容器就行坏处是如果镜像里没有基础的服务发现逻辑那么 Jar 只能靠挂载。实际用下来这种“构建产物放在宿主机、运行容器以只读卷方式加载”的模式最接近轻量级预期。4.3 运行参数的轻量级调优Spring Boot 容器启动时我习惯把 JVM 内存参数和运行参数都放在外层脚本里控制而不是写死在 Dockerfile 里。JAVA_OPTS-Xms256m -Xmx512m是一个非常保守的配置适合小内存的服务器。如果你的应用本身要处理更大的并发根据实际压测结果调整。不要把 Xmx 开得比宿主机内存还大Docker 容器默认会被 cgroup 限制住强行申请内存只会导致 OOM。端口绑定我写了127.0.0.1:${HTTP_PORT}:8080只把端口暴露到本机回环地址。如果服务器同时跑 Nginx由 Nginx 反向代理转发到本机端口不需要把 Spring Boot 端口直接暴露到公网减少安全暴露面。SPRING_PROFILES_ACTIVE用来传入环境标识部署测试环境传test生产环境传prod。这个参数在脚本里默认是prod但可以通过环境变量覆盖。不同环境的配置文件在 Spring Boot 里通过 application-profile 区分这里只要把激活标识传对就行。运行容器里设置了TZAsia/Shanghai这个很多人会漏掉。默认情况下容器的时区是 UTC数据库记录时间、日志时间都会差 8 小时。Spring Boot 项目里如果用了LocalDateTime.now()会直接受到容器时区影响。加上这个环境变量比在代码里处理简单得多。如果希望对容器做健康检查可以在运行镜像的 Dockerfile 里加HEALTHCHECK --interval30s --timeout3s --retries3 \ CMD curl -fs http://127.0.0.1:8080/actuator/health || exit 1前提是项目引入了 actuator 依赖。没有 health 端点的情况下也可以用curl -fs http://127.0.0.1:8080/做基础探测。5. 接入持续集成与日常维护5.1 用定时任务或触发钩子拉起部署这套脚本本身已经自动化了从 Git 到容器的全流程所以接入更上层的持续集成非常容易。最轻量的方式是 Linux 的 crontab。比如每天凌晨构建一次测试环境0 2 * * * /usr/local/deploy/deploy.sh demo-app gitgit.example.com:backend/demo-app.git main 8080 /var/log/deploy-demo.log 21日志重定向一定要做不然 crontab 跑出错后你完全不知道失败原因。脚本里的set -euo pipefail配合日志文件排查问题会快很多。如果团队已经有现成的 CI 工具也可以把它封装成一个构建步骤由 CI 触发脚本、把构建结果归档、再记录本次部署对应的 Git commit。脚本本身不依赖任何特定的 CI 服务只要宿主机能执行 Bash 脚本就能直接跑。Git 仓库的 webhook 同样可以接。当代码 push 到远程分支时Git 服务端发送回调给一个简单的接收程序由接收程序调用 deploy.sh 完成部署。要注意回调地址的鉴权避免有人伪造请求反复触发部署。5.2 镜像版本、构建产物和回滚策略很多新手在维护镜像时喜欢一律用latest这是我在实践中强烈不建议的。构建镜像和运行镜像最好都打上带版本信息的 tag。构建镜像建议是build-env:jdk8-2024.01运行镜像可以是jre8-run:2024.01。版本信息里放时间能一眼看出当前环境用的镜像是什么时候建的排查环境问题时非常有用。Jar 包保留策略也要想清楚。脚本每次部署都会把新 Jar 复制到${APP_DIR}/app.jar旧 Jar 被覆盖。如果线上出了问题想快速回滚可以额外做一个动作把本次构建的 Jar 先备份到带时间戳的目录再更新app.jarTIMESTAMP$(date %Y%m%d%H%M) cp -f ${JAR_FILE} ${APP_DIR}/backup/${APP_NAME}-${TIMESTAMP}.jar回滚时就只需要改一下运行容器挂载路径把/app/app.jar指到backup目录里对应的 Jar 即可。生产环境如果要保留多个历史运行容器定期用docker image prune清理无用的旧镜像避免磁盘被占满。运行镜像本身很小但 Spring Boot 镜像如果自己还 build 了很多临时版本占满磁盘其实很容易。5.3 构建镜像的升级与灰度构建环境也不是一成不变的。JDK 如果因安全公告需要从小版本升级或者 Maven 要升级到新版本时要先把新压缩包放到 deploy 目录重新 buildbuild-env的镜像不能直接在原镜像上打补丁。我的建议是旧镜像先留着不要急着删。在一个项目上试用新构建镜像确认所有依赖编译没问题后再把部署脚本里的 BUILD_IMAGE 变量切到新 tag。构建镜像和运行镜像分开维护天然就能形成一套简单的灰度流程。6. 常见问题与排查实录6.1 构建阶段的高频报错现象原因解决办法构建时 Maven 依赖下载超时Maven 仓库地址不可达或速度太慢在 settings.xml 中配置内网镜像源确认容器 DNS 可以解析仓库域名java -version显示的是 JDK11 而不是 JDK8系统 yum 仓库里默认装了更高版本 JDK检查 Dockerfile 中 JDK8 是否正确移动到 /opt/jdk8PATH 是否被其他层覆盖Git 操作报 “dubious ownership”挂载目录的所有权与容器内用户不匹配在运行命令或 Dockerfile 中添加git config --global --add safe.directory /build私有 Git 仓库 clone 报权限错误镜像内没有加部署凭据挂载只读凭据文件或通过环境变量注入有效 token不要把私钥写死在镜像层里首次构建特别慢本地 Maven 仓库是空的使用 docker volume 持久化 maven-repo第二次构建会显著提速Maven 下载依赖慢这个问题属于必踩项。如果不配置镜像源在容器里直接走默认中央仓库某些场景下的下载速度非常不稳定。但其实也别把所有问题都归到网络上先确认挂载的maven-repovolume 是否生效。我见过有同事把挂载路径错写成了/root/.m2而 settings.xml 里 localRepository 配置的却是/opt/maven/repo导致每次构建还是从头下载。6.2 运行阶段的典型故障现象原因解决办法容器启动后立刻退出缺少 Jar 包、端口被占用、启动命令路径错误先docker logs看报错确认app.jar是否被正确挂载宿主机端口明明没被占用容器却启动失败容器内 8080 端口已被其他进程占用检查docker ps -a看是否有同名或同端口残留容器访问应用时返回 404Spring Boot 上下文路径配置和反向代理路径不一致核对 server.servlet.context-path 与 Nginx 转发规则日志时间比本地时间慢 8 小时容器时区是 UTC在运行容器里加-e TZAsia/Shanghai应用报 “no main manifest attribute”打出来的不是可执行 Jar检查 pom.xml 里是否配置了 spring-boot-maven-plugin 的 repackage 目标运行阶段最容易犯的错误是把构建镜像直接当运行镜像启动。如果构建镜像的 CMD 是/bin/bash容器会进入交互模式看起来“启动了”但应用其实没有跑起来。我自己就曾在一台部署机上一口气跑了三四个构建镜像容器每个都占着内存和进程号最后排查半天才意识到这个问题。6.3 几个直接影响体验的小技巧构建容器建议加--rm参数。构建容器是一次性使用的跑完就销毁避免容器堆积。如果容器堆积docker ps -a会变得非常乱而且磁盘上会残留大量可写层。给 Maven 仓库 volume 单独命名可以有效防止多个项目共享时出现权限问题。当然多项目共享同一个仓库卷也会带来依赖冲突风险稳妥做法是按项目分 volumedocker volume create maven-repo-demo部署脚本里每个项目用各自的 volume互不干扰。代价是每个项目都得多占一份磁盘空间但对小团队来说安全性大于空间成本。尝试改脚本时最好先验证构建镜像能单独抽出正确 Jar再启动运行容器。也就是说分阶段测试。不要一上来就把脚本完整串起来跑否则构建出错、端口冲突、镜像 tag 写错这些问题会混在一起排查起来非常费劲。我个人现在比较喜欢在项目根目录放一个deploy/目录里面同时维护构建 Dockerfile、运行 Dockerfile、settings.xml 和 deploy.sh。换一台服务器部署时只要把 deploy 目录整体带过去先构建两个镜像再执行脚本整套环境就起来了。这也算是我自己更愿意维护这套方案而不是引入一堆运维平台的原因简单、直接、迁移成本低。这套组合跑熟悉之后你们大概率也会觉得轻量级自动化部署确实可以做到既干净又省心。

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

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

免费获取报价 →
↑