Docker多阶段构建Maven项目
1、Dockerfile
# 第一阶段:构建阶段 (Builder stage)
FROM maven:3.6.3-jdk-11-slim AS builder # <-- 就是这行指令
WORKDIR /app
COPY pom.xml .
COPY src ./src
# 在“builder”容器中执行编译打包命令
RUN mvn clean package -DskipTests# 第二阶段:运行阶段 (Runtime stage)
FROM openjdk:11-jre-slim # <-- 一个更小的运行环境
WORKDIR /app
# 从上一阶段(builder)的容器中复制构建产物,而不是复制本地文件
COPY --from=builder /app/target/my-app.jar ./app.jar
# 暴露端口
EXPOSE 8080
# 启动命令
CMD ["java", "-jar", "app.jar"]
2、基础镜像
FROM maven:3.6.3-jdk-11-slim AS builder
FROM:Dockerfile指令,用于指定当前构建阶段基于的集成镜像
maven:3.6.3-jdk-11-slim:这是官方提供的Maven镜像的一个特定标签
maven:镜像名称
3.6.3:指定的Maven版本
jdk-11:表示这个镜像内部预装了jdk 11
slim:镜像的变种,一个瘦身版本,通常只包含运行指定工具(Maven和JDK)所必须的最小包,去除了文档,不必要的库
as builder:为这个构建阶段命名一个别名(builder)。这个别名将在后续的阶段中被引用,以便从该阶段复杂文件
3、执行过程
当Docker守护进程执行到这个指令时,它会进行以下操作
1、拉取镜像
Docker会检查本地是否存在maven:3.6.3-jdk-11-slim这个镜像,如果不存在,它会自动从Docker Hub(或其他配置的镜像仓库)拉取该镜像
拉取的镜像包含了一个预先配置好的Linux操作系统环境,其中主要安装了:
- 一个最小化的Linux系统(通常Debian或Alpine精简版)
- Java开发工具包(JDK11)完整的JDK不仅仅JRE,因为要编译代码需要javac等
- Apache Maven 3.6.3:预配置好的Maven工具,其setting.xml可能包含基础配置,并且其本地仓库(/root/.m2/repository)可能时空的或只有基本依赖
2、创建并启动一个临时容器
Docker会以这个镜像为基础,创建一个新的、暂时的容器,这个容器被标记为名为builder的构建阶段,此时,这个容器提供了一个高度专门化的环境,这个环境唯一的目的就是用于编译和构建Java Maven项目
3、为后续指令提供上下文
这行指令本身并不执行构建命令,但它设置了舞台,后续的指令(如COPY、RUN、WORKDIR)将在由这个FROM这令创建的临时容器环境中执行
4、设置工作目录
WORKDIR /app
用户指令:设置工作目录为/app,如果目录不存在,则创建它
Docker内部操作:在由上一步FROM指令创建的临时容器中,执行 mkdir -p /app 命令创建目录
设置后续指令(COPY、RUN)的当前工作目录为/app,这意味着任何相对路径都将基于此目录
5、复制
COPY pom.xml .
用户指令:将构建上下文(通常是Dockerfile所在目录)中的pom.xml文件复制到容器当前工作目录(/app)
Docker内部操作:
1、检查缓存:这是构建过程中一个非常重要的优化点,Docker会计算pom.xml文件的校验和(checksum)
2、缓存命中:如果与此前某次构建的校验和完全一致,Docker会直接复用之前构建此步骤时创建的镜像层,跳过后续所有步骤,直接进入第二阶段,提高构建速度
3、缓存未命中:如果文件变化,Docker会继续执行
4、文件复制:将主机上的pom.xml文件复制到临时容器/app目录下,并创建一个新的镜像层来记录这个变化
COPY src ./src
用户指令:将本地src目录递归复制到容器的/app/src目录中
Docker内部操作:
1、类似于上一步,Docker会检查src目录中所有文件的检验和,单由于pom.xml通常先变化,而源代码后变化,所以上一步的缓存失效通常会导致这一步也必须执行
2、将主机上的src目录及全部内容复制到临时容器中,创建另一个新的镜像层
6、构建打包
RUN mvn clean package -DskipTests
用户指令:在容器内执行Maven命令,清理就构建,打包新应用(跳过测试)
Docker内部操作:
1、创建新容器层:RUN 指令总是在新的层上执行命令
2、启动子进程:Docker在当前的临时容器内启动一个shell进程(/bin/sh -c)来运行给定的命令
3、执行Maven
Maven读取pom.xml文件
从远程仓库下载所有的依赖(这些依赖会被下载到容器内的Maven本地仓库,通常位于/root/.m2/repository),这是构建过程中最耗时的步骤。
编译src下的源代码
运行所有编译阶段的生命周期(跳过test)
将最终的制品 jar 打包到target 目录
4、提交结果:命令执行成功后,docker将命令执行结果(即包含了编译产物的容器状态)提交为一个新的、只读的镜像层,这个层包含了 target/test.jar
至此,第一个阶段临时容器使命完成,其最终状态是一个包含了源代码,依赖库和最终构建产物的镜像,这个镜像标记为builder
7、运行阶段
FROM openjdk:11-jre-slim
用户指令:开始一个全新的、独立的构建阶段,基于一个包含JRE的轻量级的镜像
Docker内部操作:
1、重置环境:Docker完全抛弃了第一阶段创建的所有临时状态和环境,它从一个全新的镜像开始,与第一阶段的JDK、Maven、源代码完全隔离
2、拉取镜像:同样,检查并拉取openjdk:11-jre-slim镜像,这个镜像比第一阶段的Maven镜像小的多
3、创建新的临时容器:基于这个新的基础镜像创建一个新的临时容器,用于构建最终的运行时镜像
8、设置工作目录
WORKDIR /app
用户指令和操作:与第一阶段相同,在新的临时容器中创建并设置工作目录/app
9、引用第一阶段
COPY --from=builder /app/target/my-app.jar ./app.jar
用户指令:从之前命名为builder的阶段中复制文件,而不是从主机复制
Docker内部操作:
定位源阶段:Docker根据--from=builder参数,定位到第一阶段最终生成的镜像
复制文件:从builder镜像的/app/target目录找到my-app.jar文件,并将其复制到当前第二阶段的临时容器的当前工作目录/app下,并重命名为app.jar
10、监听端口
EXPOSE 8080
用户指令:什么容器运行时监听的网络端口
Docker内部操作:
1、这只是一个元数据指令,它不会实际打开任何端口
2、Docker将此信息记录到镜像的元数据中,当然使用docker inspect 查看镜像时,可以看到这个信息
3、当基于此镜像运行容器时,使用 -p 8080:8080 参数会将主机8080端口映射到这个申明的容器端口
11、执行命令
CMD ["java", "-jar", "app.jar"]
用户指令:指定容器启动默认执行的命令
Docker内部操作:
1、这也是一个元数据指令,Docker将此JSON数据格式的命令记录到镜像的元数据中
2、当运行docker run <image-name>时,容器内的pid 1进程将是 java -jar app.jar
3、因为工作目录是/app,而app.jar就在该目录下,所以命令可以成功找到并执行