先讲个很常见的事:你花了两天把 Spring Boot 项目写好,本地 mvn spring-boot:run 一切正常,于是高高兴兴把 jar 包扔到服务器上。然后你会发现服务器上要么 JDK 版本对不上,要么环境变量被之前遗留的 Java 8 项目搞乱了,要么时区差了 8 个小时。折腾两天终于跑起来了,结果运维同事补一句:"这个服务器上还有别的服务,你下次部署前记得先通知我们。"这时候你才会意识到,部署这件事最危险的从来不是"跑不起来",而是"今天能跑、明天不一定能跑"。
我用 Docker 部署 Spring Boot 项目有三年多了,从最开始只会 docker run 一个 jar,到现在把 MySQL、Redis、日志采集全部容器化,中间踩过的坑不算少。这篇文章不是把官方文档复述一遍,而是把我从零把一个 Spring Boot 项目推到生产环境的完整过程、每个关键选择的理由,以及翻车记录都摊开来讲。适合两类人看:一是刚接触 Docker 的 Java 开发者,想知道 Dockerfile 到底该怎么写、为什么这么写;二是已经用 Docker 部署过 Spring Boot 项目,但总觉得哪里还不够"生产级"的朋友,可以对照检查一下自己的方案还有哪些漏洞。
1. 先想清楚:Spring Boot 项目容器化到底解决了什么问题
1.1 传统部署方式的三个老大难
Java 项目部署这件事,说实话本身不算难,难点在于"稳定地重复"。
以前我部署 Spring Boot 项目,流程是:本地 mvn package 打 jar,scp 到服务器,然后 nohup java -jar app.jar > app.log 2>&1 &。听起来很简单,但坑全藏在细节里。
第一是环境差异。服务器上可能已经有老项目用的是 JDK 1.8,/etc/profile 里的 JAVA_HOME 指向 jdk8,而你手头这个新项目基于 Spring Boot 3.x,最低要求 JDK 17。直接在命令行里跑 java -jar,大概率起来的是 jdk8,然后喜提 UnsupportedClassVersionError。改全局 JAVA_HOME 又会让老项目出问题,这种拉扯我见过太多次。
第二是依赖冲突。同一个服务器跑多个 Java 应用,大家共用一套系统库。A 项目依赖 openssl 1.0,B 项目因为新功能装了依赖 openssl 1.1 的组件,装来装去系统状态变得不可预测,谁也不知道改了什么会导致哪个服务挂掉。这种"因为装了个东西,另一个服务崩了"的诡异事故,在传统部署方式下几乎无解。
第三是配置漂移。新服务器第一次部署一切顺利,第二次第三次开始出幺蛾子——"上次我明明配过时区,怎么这次又不对了"。生产服务器经过长期手动修改,最终变成一台"只有创始人敢碰的机器",出问题了靠着聊天记录和肌肉记忆去猜,这个过程消耗的精力远超容器化改造本身。
1.2 镜像不可变:一次构建、处处运行
Docker 解决这些问题的思路其实很朴素:把运行环境连同应用一起打进镜像。镜像里的 JDK 版本、系统依赖、时区、配置文件,在构建那一刻就固定下来了。同一份镜像,在开发笔记本上怎么跑,到测试服务器、生产服务器就怎么跑,不存在"我这儿明明是好的"这种话。
打个比方,镜像像是一张已经调好配料的预制菜包装,你不关心它在哪个厨房做、锅是什么牌子,只需要按说明加热,出来的菜一定一致。容器只是这个镜像的一个运行实例,镜像不变,行为就不会因宿主机差异而变化。
1.3 不是所有项目都要 Docker
不过我得泼一点冷水。Docker 不是银弹,如果你的情况是:小团队内部管理系统、单台服务器、JDK 环境已经装好、没有多环境迁移需求——那直接用 java -jar 跑,运维成本反而更低。引入 Docker 之后,你多了镜像构建、仓库管理、磁盘清理这些新的维护负担。
判断标准很简单:你需不需要频繁换环境部署?需不需要保证多套环境一致?有没有多人协作的部署需求? 如果三个答案都是"否",那容器化省下的成本可能还不够学 Docker 花的时间。反过来说,只要有一个答案是"是",这篇文章接下来的内容就值得你读完。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 镜像构建前的准备:基础镜像选型与构建上下文
2.1 基础镜像选型:为什么我弃用了 openjdk 官方镜像
现在还能在网上搜到大量老教程让你用 openjdk:8-jdk-alpine,如果你还在照着做,建议停下来。
OpenJDK 官方在 Docker Hub 上的 openjdk 仓库已经停止维护了,版本不再更新,更关键的是存在安全漏洞得不到修复。现在 Java 社区推荐用的是 Eclipse Temurin(eclipse-temurin 镜像)或 Amazon Corretto(amazoncorretto 镜像),两者都是 LTS 版本、长期维护、持续推送安全更新。
| 镜像 | 基础系统 | 大致压缩后大小 | 适用场景 |
|---|---|---|---|
| eclipse-temurin:17-jre | Ubuntu Jammy | 约 80MB | Spring Boot 3.x 首选 |
| eclipse-temurin:17-jdk | Ubuntu Jammy | 约 180MB | 多阶段构建的编译阶段使用 |
| amazoncorretto:17-alpine | Alpine | 约 60MB | 追求极致体积且无 JNI 依赖 |
| openjdk:8-jdk-alpine | Alpine | 约 50MB | 老项目遗留,不推荐新用 |
这里有个很多人不知道的坑:Temurin 官方已经不提供 Alpine 变体了。原因是 Alpine 用的是 musl libc 而不是标准 glibc,Java 应用本身没问题,但一旦你的应用或第三方库通过 JNI 调用本地代码(比如某些加密库、OCR 组件、网络工具库),在 Alpine 下有概率出现诡异崩溃。我踩过一次:一个项目用了某个底层 JNI 库,在 alpine 镜像上偶尔抛出 SIGSEGV,进程直接死掉,换回 Ubuntu 基础镜像之后问题消失。从那以后我的默认选择就是 eclipse-temurin,Alpine 只在纯 IO 类、无复杂依赖的服务上考虑。
2.2 多阶段构建:把构建工具从生产镜像里踢出去
新手写 Dockerfile 最常见的做法是:
dockerfile复制FROM maven:3.9-eclipse-temurin-17
WORKDIR /app
COPY pom.xml .
RUN mvn dependency:go-offline
COPY src ./src
RUN mvn clean package -DskipTests
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "/app/target/app.jar"]
这样能跑,但生产镜像里混进了完整的 Maven 和 JDK,体积轻松超过 300MB,而且把构建工具暴露在生产环境,等于给攻击者留了一扇门。
多阶段构建的思路是:第一阶段用包含 Maven + JDK 的镜像完成编译,第二阶段只用 JRE 镜像把编译好的 jar 拷过去。构建工具只在构建阶段存在,最终镜像只剩运行所需的最小环境,体积
