行业资讯
📅 2026/8/3 7:37:02
从 Dockerfile 到容器:部署一个 SpringBoot 应用
从 Dockerfile 到容器部署一个 SpringBoot 应用本文直接从一个 SpringBoot 项目的 Dockerfile 开始在实际构建过程中理解每条指令的作用。最终我们会完成一个完整流程编写 Dockerfile、构建镜像并启动一个运行中的 SpringBoot 容器。目录SpringBoot 镜像的最小构成Dockerfile 指令解析从镜像构建到容器启动为什么 SpringBoot 镜像这么大多阶段构建分离编译环境与运行环境ENTRYPOINT 与 CMD启动命令如何选择构建缓存为什么改代码还要重新下载依赖SpringBoot 容器化中的常见问题完整的 SpringBoot Dockerfile 模板总结SpringBoot 镜像的最小构成假设你已经用 Maven 把项目打成了 jar 包文件在target/app.jar。一个最小可运行的 Dockerfile 其实只需要几行配置FROM eclipse-temurin:17-jre WORKDIR /app COPY target/app.jar app.jar EXPOSE 8080 ENTRYPOINT [java, -jar, app.jar]先把它构建出来看效果再回头逐行解释。dockerbuild-tmy-app:1.0.dockerrun-d-p8080:8080--namemy-app my-app:1.0容器启动后docker ps能看到运行状态浏览器访问http://localhost:8080就能打开应用。Dockerfile 指令解析FROMFROM eclipse-temurin:17-jreFROM 决定了镜像构建的起点。Docker 镜像不是从空文件系统开始而是在已有镜像基础上逐层构建。这里用的是 Eclipse Temurin 提供的 JRE 17 镜像只包含运行 Java 程序需要的 JVM 环境。JDK 镜像包含完整开发工具例如 javac 和各种诊断工具而运行 SpringBoot 应用实际上只需要 JVM 运行环境。用 JRE 作为基础镜像体积比 JDK 小两百多 MB。WORKDIRWORKDIR /app设置后续指令的工作目录。相当于在容器里先mkdir /app cd /app后续的COPY、RUN等指令都在这个目录下执行。COPYCOPY target/app.jar app.jar把构建上下文中的target/app.jar复制到容器内的/app/app.jar。执行docker build .时最后那个.就是构建上下文——当前目录。Docker 构建时只能访问构建上下文中的文件因此 Dockerfile 无法直接复制宿主机任意目录中的文件。EXPOSEEXPOSE 8080声明镜像预期监听的端口。EXPOSE不会真的打开端口它只是一个元数据声明。真正把端口映射出去的是docker run -p 8080:8080。ENTRYPOINTENTRYPOINT [java, -jar, app.jar]定义容器启动时执行的命令。这里用的是 JSON 数组格式exec 格式不是ENTRYPOINT java -jar app.jarshell 格式。shell 格式会额外启动一个/bin/sh -c进程应用进程不会成为 PID 1可能导致 Docker 的停止信号无法正确传递。两种格式的区别会在后面的启动命令部分展开。从镜像构建到容器启动构建镜像dockerbuild-tmy-app:1.0.-t给镜像打标签格式是名称:版本。构建过程会看到每一步的输出[] Building 2.3s (8/8) FINISHED [1/4] FROM eclipse-temurin:17-jre 0.0s [2/4] WORKDIR /app 0.0s [3/4] COPY target/app.jar app.jar 0.1s [4/4] EXPOSE 8080 0.0s exporting to image 0.2s naming to docker.io/library/my-app:1.0 0.0s运行容器dockerrun-d-p8080:8080--namemy-app my-app:1.0-d后台运行-p 8080:8080把容器的 8080 端口映射到宿主机的 8080 端口宿主机端口:容器端口--name my-app给容器起个名字方便后续操作。查看镜像大小dockerimages my-appREPOSITORY TAG SIZE my-app 1.0 272MB272MB。jar 包本身可能只有几十 MB镜像体积的大头来自基础镜像中的 JRE。这个大小已经比 JDK 版本小了不少但还有优化空间。为什么 SpringBoot 镜像这么大问题出在基础镜像的选择上。如果用 JDK 而不是 JRE 作为基础镜像体积会到 470MB 左右——编译器、调试工具等运行时完全用不上的东西也被打包进去了。更麻烦的是如果直接在 Dockerfile 里用 Maven 编译FROM eclipse-temurin:17-jdk WORKDIR /app COPY . . RUN mvn package -DskipTests ENTRYPOINT [java, -jar, target/app.jar]Maven 缓存、源码、编译中间产物全都被打包进最终镜像轻松突破 800MB。多阶段构建分离编译环境与运行环境多阶段构建把构建环境和运行环境拆开。前一个阶段负责编译后一个阶段只保留应用运行所需的文件。# 阶段一编译 FROM maven:3.9-eclipse-temurin-17 AS builder WORKDIR /app COPY pom.xml . RUN mvn dependency:go-offline COPY src ./src RUN mvn package -DskipTests # 阶段二运行 FROM eclipse-temurin:17-jre WORKDIR /app COPY --frombuilder /app/target/*.jar app.jar EXPOSE 8080 ENTRYPOINT [java, -jar, app.jar]AS builder给第一个阶段命名COPY --frombuilder从编译阶段复制文件。最终镜像只包含第二个阶段的内容JRE jar 包。Maven、JDK、源码都不会出现在最终镜像中。根据基础镜像和项目依赖不同最终镜像体积通常可以明显下降。镜像变小后带来的直接收益是上传、下载和部署时间下降同时减少了最终镜像中无关软件的数量。ENTRYPOINT 与 CMD启动命令如何选择两者都可以定义容器启动行为但最大的区别是运行时参数如何覆盖。CMD是默认命令可以被docker run后面的参数替换CMD [java, -jar, app.jar]dockerrun my-app# 执行 java -jar app.jardockerrun my-app python app.py# 执行 python app.pyCMD 被覆盖ENTRYPOINT默认作为容器主进程执行而docker run后面的参数会追加到 ENTRYPOINT 后面ENTRYPOINT [java, -jar, app.jar]dockerrun my-app# 执行 java -jar app.jardockerrun my-app--spring.profiles.activeprod# 执行 java -jar app.jar --spring.profiles.activeprod实际项目中常见的写法是两者配合ENTRYPOINT [java, -jar, app.jar] CMD [--spring.profiles.activedefault]不传参数时执行java -jar app.jar --spring.profiles.activedefault传了参数就替换 CMD 部分。对于 SpringBoot 应用推荐用ENTRYPOINT固定启动命令CMD放默认参数。构建缓存为什么改代码还要重新下载依赖构建镜像时Docker 会缓存每一层的结果。如果某一层的输入没有变化Docker 会直接使用缓存跳过执行。但缓存有一个关键规则一旦某一层失效它之后的所有层都会重新执行。看这个 DockerfileFROM eclipse-temurin:17-jdk AS builder WORKDIR /app COPY . . RUN mvn dependency:resolve ENTRYPOINT [java, -jar, target/app.jar]COPY . .会把整个项目目录复制进去。你改了一行业务代码COPY这一层就失效了后面的RUN mvn dependency:resolve也要重新执行——即使 pom.xml 根本没变。优化方式是把不常变的东西先 COPY常变的后 COPYFROM maven:3.9-eclipse-temurin-17 AS builder WORKDIR /app COPY pom.xml . RUN mvn dependency:go-offline COPY src ./src RUN mvn package -DskipTests先复制pom.xml并下载依赖。只要pom.xml没变这一层就会命中缓存。之后再复制src目录。改业务代码时只有COPY src和mvn package会重新执行依赖下载被跳过。在依赖没有变化的情况下这种调整可以避免重复下载依赖明显减少构建时间。SpringBoot 容器化中的常见问题容器化部署 SpringBoot 应用时有几个问题经常在第一次部署时遇到。配置文件中的 localhostapplication.yml里的数据库地址如果写的是localhost:3306在容器内会指向容器自己而不是宿主机或其他容器。用 Compose 编排多服务时应该用服务名替代 localhost。这个问题下一篇会展开。JVM 参数需要通过环境变量传递容器内的 JVM 参数不能写死在 Dockerfile 里。推荐的做法是通过环境变量传入ENTRYPOINT [java, -jar, app.jar]dockerrun-eJAVA_OPTS-Xmx512mmy-app更常见的写法是在 ENTRYPOINT 中引用环境变量ENV JAVA_OPTS ENTRYPOINT sh -c java $JAVA_OPTS -jar app.jar容器时间和宿主机不一致部分容器内默认使用 UTC 时区和宿主机不同。日志时间会对不上定时任务也可能出问题。挂载宿主机时区文件可以解决dockerrun-v/etc/localtime:/etc/localtime:ro my-appjar 包名称变化导致 COPY 失败Maven 打包默认生成的 jar 文件名带版本号比如my-app-1.0.0.jar。每次版本升级Dockerfile 里的 COPY 路径也要跟着改。用通配符可以避免这个问题COPY target/*.jar app.jar完整的 SpringBoot Dockerfile 模板把上面的内容综合起来一个生产可用的 SpringBoot Dockerfile# 编译阶段 FROM maven:3.9-eclipse-temurin-17 AS builder WORKDIR /app # 先复制依赖描述文件利用缓存 COPY pom.xml . RUN mvn dependency:go-offline -q # 复制源码并打包 COPY src ./src RUN mvn package -DskipTests -q # 运行阶段 FROM eclipse-temurin:17-jre WORKDIR /app # 只复制编译产物 COPY --frombuilder /app/target/*.jar app.jar # 声明端口 EXPOSE 8080 # 启动命令 ENTRYPOINT [java, -jar, app.jar]使用时只需要把pom.xml和src目录替换成你自己的项目结构。如果是 Gradle 项目把maven:3.9-eclipse-temurin-17换成gradle:jdk17把mvn换成gradle。构建和运行dockerbuild-tmy-app:1.0.dockerrun-d-p8080:8080--namemy-app my-app:1.0总结至此一个 SpringBoot 应用从源码到 Docker 容器的基本流程已经完整走通。Dockerfile 描述镜像构建过程多阶段构建分离编译和运行环境ENTRYPOINT 控制启动程序COPY 顺序影响缓存命中。掌握了这几点大部分 SpringBoot 项目的容器化需求就都能覆盖了。