如何构建SpringBoot的Docker镜像

目标

  • 自定义Dockerfile构建一个生产可用的jre base image

  • 配置maven-docker-plugin插件完成从源码的 打包fatjar -> build docker image with fatjar -> push image

  • 支持docker对JVM相关参数的配置。比如Xmx,Xms,以及完全自定义的java启动参数。

    rootfs

  • 说打包之前我们先了解linux内核与发行版操作系统(如centos,ubuntu,debian)之间的关系。

  • 由于linux内核与具体的操作系统是解耦的。即互相不干涉的,docker利用了这个特性,将操作系统的文件打包成一个压缩文件。

  • 在运行时,解压这个压缩包,并通过chroot进行挂载,就完成了容器的内部我们看到的操作系统了。即我们的rootfs。

  • 那么这个和我们docker打包java应用的关系在哪里呢?

  • 总所周知的是java是又提供打包解决方案的,打包成jar,但是此方案的问题在于我并不能在任何一个环境里面运行直接运行(因为依赖JRE),而每一个操作系统又不一样,这就导致许多环境带来的时间浪费。

  • 结合前面提到了docker打包是把操作系统的文件打包的,所以我们能不能把操作系统+jre+application.jar这三老铁一锅端,全给他打包起来不就解决了吗?没错,这就是我们要构建镜像。

  • 没错我们想到了一个好的办法来解决打包的问题。那我们在来看看这个东西是不是还有啥问题?你看啊,我们最初了发布一个fatjar也就60M,要结合上OS jre岂不是每次都大很多,浪费很多的磁盘,网络传送开销也加大的蛮多。

  • 这个问题的docker中利用了分层文件系统来解决这个问题,即我们的OS JRE 这些不变的东西只会在第一次使用时下载一次或者上传一次,其他时候我们只有变化的application.jar层需要进行上传和下载

最终形态

  • 我们像搭积木一样一层一层的把我们所需要的文件系统叠加起来,就完成了我们想要的Image。如何构建SpringBoot的Docker镜像_第1张图片

Docker Image

需求

  • 我们要达到如下预期
  1. 镜像比如要足够小(尽量控制在100M以内)
  2. 字符集必须要支持中文,不然就乱码了
  3. 时区的是UTC+8
  4. 字体的支持,不然excel导出会抛出错误
  5. 还的支持下imagemagick。
  6. 使我们运行在容器中的java进程PID!=1
    如果Pid=1会有很多问题(如jmap,jstat…等工具无法使用,你可以从这里了解更多 jmap not happy on alpine )
    这里我们使用Tini来完成这个工作,关于Tini你可以从这里了解更多 What is advantage of Tini?

措施

  • 基于以上要求我们构建出一下镜像
  • 首先使用apline作为基础镜像足够小只有5M
  • 由于alpine自带支持中文的字符集,这里我们只需要将LANG设置为C.UTF-8即可完美的支持中文。
  • 国内软件源首选阿里云啦,顺道配置一下阿里云的镜像源,加速我们的镜像构建速度。
  • 配置UTC+8时区需要安装tzdata,安装完成之后配置一下即可。
  • 目前alpine携带JDK版本为1.8.232。
  • 使用tini 包装java进程。

Dockerfile构建基础java镜像(图中最一层和第二层)

  • 如下的dockerfile是一个完整的dockerfile文件。基于5M大小的alpine镜像构建,apline提供的包管理器是apk,
    我在这里为了方便使用直接配置了阿里云提供的镜像源,加速构建过程。这里我们劲量拆分步骤,方便
    FROM alpine:3.11.2
    MAINTAINER qingmu [email protected]
    ENV LANG=C.UTF-8 \
     TZ=Asia/Shanghai
    RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime \
    && echo $TZ > /etc/timezone \
    && sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories \
    && apk update \
    && apk add --update procps \
    && apk add --no-cache  ca-certificates ttf-dejavu tzdata tini bash
    # 变动层
    RUN apk add --no-cache  openjdk8-jre
    ENV JAVA_HOME=/usr/lib/jvm/java-1.8-openjdk/jre \
     PATH=$PATH:/usr/lib/jvm/java-1.8-openjdk/jre/bin:/usr/lib/jvm/java-1.8-openjdk/bin
    RUN rm -rf /var/cache/apk/*
  • 如你想动手构建自己,将以上Dockerfile文件copy一份,写入Dockerfile文件中执行构建即可。执行以下命令,即可完成属于您自己的镜像,这里需要一点docker的基础知识
    echo 'dockerfile' > Dockerfile
    docker build -t freemanliu/openjre .
  • 关于docker build 命令你可以点击这里了解更多更详细的用法。
  • 当然了,你如果不想构建那么也可以使用我在github上写好的,并在dockerhub中autobuild好的镜像。
  • 注意该镜像不包含字体文件,在使用到字体相关的api时,会抛出异常,如对excel导出的操作,这时候我们需要安装一下字体库,我们通过apk库添加十分简单,只需在以上的Dockerfile描述文件中加入一下库即可,其他保持不变。如你还用到了其他相关软件,请google查阅相关库,使用apk包管理器添加即可。
    apk add --no-cache ttf-dejavu
  • 如此我们便得到了一个包含了jre的Image镜像,下面我们继续来构建第三层。

SpringBoot打包fatjar

  • 在配置maven项目的parent为spring-boot-dependencies后,springboot默认在pluginManagement中配置好了 maven-shade-plugin插件
    我们可以在我们自己具体业务微服务中激活该插件,配置如下:
     
     
     
     org.apache.maven.plugins
     maven-shade-plugin
     
     
    
  • 配置好了插件,插件怎么知道我们的start class是哪个呢?springboot很贴心的提供了一个start-class属性。我们只需要赋值即可
    
     com.qingmu.account.Application
    
  • 配置完成之后我们可以在命令行执行如下命令,便可完成我们的fatjar的打包。
  • 执行该命令本质上mave插件会将我们所依赖的jar解压并copy到到我们的fatjar中。
mvn clean package
  • 如配置正确你将在你的target目录中看到${artifactId}.jar
  • 在命令行执行java -jar target/${artifactId}.jar也能正确的启动成功。

Docker打包fatjar镜像(构建第三层)

  • 在前面的的章节,我们完成了JRE镜像和fatjar的构建。接下来我们将完成他们的结合,完成灵魂的升华 ~ ~。

Dockerfile VS Docker-mave-plugin

  • 在java项目中我们至少有两种方式对我们的docker和fatjar进行结合打包成image。

Dockerfile

  • 一种常见的方式是编写Dockerfile,并使用Cli命令行对齐进行相关操作,这也是网上比较多的教程所采用的方法。
    然而该方法有比较麻烦的问题:
    第一,如果你的项目较多怎么能,意味着你需要为每一个项目去添加一份dockerfile描述文件,当然这点可以通过自定义maven骨架解决。
    第二,这么多dockerfile散落在微服务项目的几十上百个仓库中,如果有一天要更新这个dockerfile中的某些内容,就会变得异常的麻烦和难以高效率的解决。

Docker-mave-plugin

  • 这是一个由于com.spotify提供的maven插件,使用它将可以轻松的完成SpringBoot项目的docker镜像打包推送工作。
  • 当然他也能很好的解决以上Dockerfile的问题,不需要Dockerfile描述文件了,我们可以将描述的相关信息写到maven插件中,而maven是支持继承的,以为者我们只需要在我们自己的parent-pom中维护这个docker插件就维护了所有的具体微服务的打包插件了。实际用下来是相当的方便啊。实际项目中也采用的是该方案。

ENTRYPOINT VS CMD

  • ENTRYPOINT和CMD命令最大的不同点,在于使用cmd命令是可以在运行是通过传递参数修改镜像的运行命令的,而ENTRYPOINT命令则是不支持的。

  • 简单的说就是ENTRYPOINT类似编程中的常量,一旦定义好了就无法改变。而cmd则类似与变量,你可以在运行时随时赋值。

  • 一起来看下这个例子,这是一个常见的使用docker打包java应用的dockerfile描述文件,。使用了ENTRYPOINT来指定启动命令。

freemanliu/openjre:8.232
    FROM java:8
    ADD app.jar app.jar
    EXPOSE 8800
    ENTRYPOINT ["java","-jar","/app.jar"]
  • 将这个image启动起来,我们只需要执行如下命令,变可以轻松的运行起来我们的应用。在启动镜像是它会默认执行ENTRYPOINT中所书写的描述。
docker run -it -rm freemanliu/app:v1.0.0
  • 如果我们想运行的时候不运行默认的命令,比如说我们执行以下命令,进入到容器的sh命令终端。你会发现无效,由于我们是使用的ENTRYPOINT定义的是”常量”,所以你将会看到的结果任然是java进程成功启动。而如果使用的CMD命令,则是可以正确的进入sh终端。
docker run -it -rm freemanliu/app:v1.0.0 sh
  • 至于CMD也好还是ENTRYPOINT也罢,都可以用,看你的喜好了,我是更喜欢freedom一点的,我推荐CMD

  • 那么好的,基础的东西我们就讨论到这里。

配置POM

在具体配置之前我们先总结下需要干些什么:
第一步,肯定是先引入插件,不多说。
第二步,既然我们想要插件帮我们完成image的push,那的让插件知道我们的私服地址吧,得知道username/password吧,所以第二步我们得配置setting.xml,添加server。
第三步,插件有了,私服配好了,接下来就的琢磨插件的配置了吧,所有第三步我们配置插件。
第四步,配置完成一个插件之后,我们该考虑一下多环境打包的事儿吧。第四步处理profiles

添加插件到pluginManagement

  • 首先是引入插件,你可以点击这里了解更多插件的信息,虽然他现在极力推崇dockerfile插件,但是真的没这个实用性高啊。
  • 我们得养成一个习惯,在添加任何新的依赖是,先在咱们的parent项目的xxxManagement中先添加上,在真正业务pom中去掉版本号,让parent统一管理依赖的版本号。

     
     
     
     com.spotify
     docker-maven-plugin
     1.1.1
     
     
     
    

配置私服

  • 关于私服你可以自己搭建,选择还是比较多,官方的docker-registry,比较知名和完善的Harbor,以及我们的老朋友Nexus3.
  • 自己搭建虽然很方便也很简单,那有没有不用自己维护性能还不错的三方私有仓库选择呢?当然还是有的,毕竟这世上好人还是多,阿里云为我们提供了免费的私有仓库服务,你只需要注册阿里云,控制台搜索容器镜像服务,就能找到他了。
  • 对于经常配置server的老司机来说这一步很简单,但是对于新玩家来说这一步比较复杂,如果你不是很熟悉并且没有配置过maven的setting.xml文件的话,我建议你使用图形化编辑器,找准如下标签插入即可。
  • 为啥~/.m2/settings.xml这是因为maven默认读取的配置目录是当前用户目录下的.m2目录,windows的话你需要找到你当前用户。
 vim ~/.m2/settings.xml
# 找到servers标签添加如下如下信息
    
     
     aliyun-registry # id将在后面插件中用到
     你的私服的username
     你私服的密码
     
     你的email
     
     
    

插件配置

  • 在配置插件前,另一个插件git-commit-id-plugin通过该插件我们可以在maven的其他插件中很方便的用到git相关信息,比如获取到当前的git tag。
  • 通用的我们配置的插件默认不激活,写到pluginManagement

     
     
     pl.project13.maven
     git-commit-id-plugin
     2.2.4
     
     
     get-the-git-infos
     
     revision
     
     
     
     
     
     true
     yyyyMMddHHmmss
     
     true
     ${project.basedir}/.git
     
     false
     
     true
     
     git.properties
     
     
     
     false
     
     false
     
     40
     
     -dirty
     
     false
     
     
     
     
     
  • 在添加插件的时候我们说到应该把通用的东西放到xxxManagement标签中,将变化的内容通过变量的形式抽取出来,完成最大程度的复用。
  • 所以我们对插件的配置也是通用的,所以也是改在Management中完成配置,并抽取变化。具体配置如下图,可能显得有些复杂。
    由于为了灵活性的需要,我们进行了多次变量取,这里我来详细解释下:
  1. 首先properties标签中的大写的变量取值,意思为取得系统上下的属性,至于为什么要这么做呢?是因为为了让我们的镜像在运行是可以通过环境变量的形式对其进行修改优化。仔细看你会发现这些值都是有可能对于不同的硬件服务器有着完全不同的设定。避免了修改java内存及一些相关参数而重写打包镜像,也提供了一个修改的便捷渠道。
  2. 接下来再看env标签,有没有觉得眼熟?这里是定义系统环境变量的地方,这里的定义与我们在properties标签中定义的名称一致,可以看到我们在这里有进行一次变量取值,为什么又是变量取值,而不是写死一个值呢?
    主要出于三点考虑:
    第一点是方便maven执行mvn clean package -Djvm.Xms=2G 通过命令行传递默认参数。
    第二点是为了方便具体业务通过在自己的pom重覆盖该参数完成默认参数的修改。
    第三点当然是为了支持多环境打包准备。

     freemanliu/openjre
     1.8.0_171
     
     -Dservice.name=${project.artifactId} \
     -XX:+UnlockExperimentalVMOptions \
     -Xms${JAVA_HEAP_XMS} \
     -Xmx${JAVA_HEAP_XMX} \
     -XX:CICompilerCount=${CI_COMPILER_COUNT} \
     -XX:G1NewSizePercent=${G1_NEW_SIZE_PERCENT} \
     -XX:G1MaxNewSizePercent=${G1_MAX_NEW_SIZE_PERCENT} \
     -DEUREKA_SERVER=${EUREKA_SERVER} \
     -Dspring.profiles.active=${spring.profile} \
     -Dspring.cloud.config.profile=${spring.profile} \
     -XX:+UseG1GC \
     -XX:+AggressiveOpts \
     -XX:+UseFastAccessorMethods \
     -XX:+UseStringDeduplication \
     -XX:+UseCompressedOops \
     -XX:+OptimizeStringConcat
     
     1G
     1G
     5
     60
     true
     8
     ${prod.eureka}
     test
    

    
    
     
     com.spotify
     docker-maven-plugin
     
     
     ${docker.repository}/${docker.registry.name}/${project.artifactId}:${git.commit.id.describe-short}
     
     ${docker.repository}
     /work
     true
     
     
     
     Asia/Shanghai
     
     ${jvm.Xms}
     
     ${jvm.Xmx}
     
     ${g1.new.size.percent}
     
     ${g1.max.size.percent}
     
     ${ci.compiler.count}
     
     ${eureka.url}
     ${java.opts}
     
     ${docker.jre}:${docker.jre.version}
     
     /sbin/tini java ${JAVA_OPTS} -jar ${project.build.finalName}.jar
     
     
     ${pushImage}
     
     
     ${project.build.directory}
     ${project.build.finalName}.jar
     
     
     
     aliyun-registry
     
     
     
     package
     
     build
     
     
     
     
    
    

多环境配置

  • 完成了插件的配置和变量的抽取之后我们具体使用将十分的简单
  • 下面我贴出常见的一些profile

    
     
     docker
     
     
     
     org.apache.maven.plugins
     maven-shade-plugin
     
     
     com.spotify
     docker-maven-plugin
     
     
     
    
    
     
     prod
     
     ${docker.registry}
     ${prod.eureka}
     ${g1.new.size.percent}
     ${g1.max.size.percent}
     ${prod.jvm.Xms}
     ${prod.jvm.Xmx}
     true
     prod
     
     
     
     
     org.apache.maven.plugins
     maven-shade-plugin
     
     
     
     pl.project13.maven
     git-commit-id-plugin
     
     
     com.spotify
     docker-maven-plugin
     
     
     
    
    
     
     test
     
     ${test.docker.repository}
     ${test.eureka}
     40
     70
     ${test.jvm.Xms}
     ${test.jvm.Xmx}
     false
     
     latest
     test
     
     
     
     
     org.apache.maven.plugins
     maven-shade-plugin
     
     
     com.spotify
     docker-maven-plugin
     
     
     
    
    
## [](https://qingmu.io/2018/08/07/How-to-run-springcloud-in-docker/#构建 "构建")构建
  • 打包prod版本的
mvn clean pacakge  -Pprod
  • 打包test版本的
mvn clean pacakge  -Ptest
  • 打包个性化的版本的
mvn clean pacakge -Djvm.Xms=8G -Djvm.Xmx=8G  -Pdocker

检验

  • 如果你配置正确,你将会看到如下输出,如果你还开启的push,那么将看到push的日志
[INFO] --- docker-maven-plugin:1.2.0:build (default) @ user-server ---
    [INFO] Using authentication suppliers: [ConfigFileRegistryAuthSupplier, FixedRegistryAuthSupplier]
    [INFO] Copying /Users/freeman/IdeaProjects2/user-server/target/user-server.jar -> /Users/freeman/IdeaProjects2/user-server/target/docker/user-server.jar
    [INFO] Building image hub.mayixiaoke.com/my/user-server:latest
    Step 1/12 : FROM freemanliu/openjre:1.8.0_171_font

     ---> 753ecb9267d1
    Step 2/12 : ENV CI_COMPILER_COUNT 8

     ---> Using cache
     ---> 93bebb3621d4
    Step 3/12 : ENV EUREKA_SERVER "-DEUREKA_SERVER=http://192.168.0.204:8761/eureka/"

     ---> Running in 42d31fcd4a7d
    Removing intermediate container 42d31fcd4a7d
     ---> c1e478140e99
    Step 4/12 : ENV G1_MAX_NEW_SIZE_PERCENT 70

     ---> Running in 794778b524dc
    Removing intermediate container 794778b524dc
     ---> 6cf5ff851174
    Step 5/12 : ENV G1_NEW_SIZE_PERCENT 40

     ---> Running in 85592d62fe30
    Removing intermediate container 85592d62fe30
     ---> 8b69e9c6371e
    Step 6/12 : ENV JAVA_HEAP_XMS 450m

     ---> Running in 683b34295fec
    Removing intermediate container 683b34295fec
     ---> 24917bf053e6
    Step 7/12 : ENV JAVA_HEAP_XMX 2G

     ---> Running in 74ba3caadd4b
    Removing intermediate container 74ba3caadd4b
     ---> d1bd5ba01b7d
    Step 8/12 : ENV JAVA_OPTS -Dservice.name=user-server             -XX:+UnlockExperimentalVMOptions             -Xms${JAVA_HEAP_XMS}             -Xmx${JAVA_HEAP_XMX}             -XX:CICompilerCount=${CI_COMPILER_COUNT}             -XX:G1NewSizePercent=${G1_NEW_SIZE_PERCENT}             -XX:G1MaxNewSizePercent=${G1_MAX_NEW_SIZE_PERCENT}             -DEUREKA_SERVER=${EUREKA_SERVER}             -Dspring.profiles.active=test             -Dspring.cloud.config.profile=test             -XX:+UseG1GC             -XX:+AggressiveOpts             -XX:+UseFastAccessorMethods             -XX:+UseStringDeduplication             -XX:+UseCompressedOops             -XX:+OptimizeStringConcat

     ---> Running in 2b4e73c01ad7
    Removing intermediate container 2b4e73c01ad7
     ---> 2fd9b3ffa0ad
    Step 9/12 : ENV TZ Asia/Shanghai

     ---> Running in 85946bd9bb3c
    Removing intermediate container 85946bd9bb3c
     ---> b588ab3d7e2c
    Step 10/12 : WORKDIR /work

     ---> Running in a41a0545883f
    Removing intermediate container a41a0545883f
     ---> 53c863bbcdbf
    Step 11/12 : ADD user-server.jar .

     ---> a3cf23754886
    Step 12/12 : CMD /sbin/tini java ${JAVA_OPTS} -jar user-server.jar

     ---> Running in c3a716574080
    Removing intermediate container c3a716574080
     ---> 262898825ee2
    ProgressMessage{id=null, status=null, stream=null, error=null, progress=null, progressDetail=null}
    Successfully built 262898825ee2
    Successfully tagged hub.xxxx.com/my/user-server:latest
    [INFO] Built hub.xxxx.com/my/user-server:latest
    [INFO] ------------------------------------------------------------------------
    [INFO] BUILD SUCCESS
    [INFO] ------------------------------------------------------------------------
    [INFO] Total time:  26.332 s
    [INFO] Finished at:
    [INFO] ------------------------------------------------------------------------
  • 如果你使用的cmd命令跟我一样,那么你还可以执行sh终端,查看我们的环境变量
docker run -it --rm hub.xxx.com/my/user-server:latest sh
    echo $JAVA_OPTS
    -Dservice.name=user-server -XX:+UnlockExperimentalVMOptions -Xms450M -Xmx2G -XX:CICompilerCount=8 -XX:G1NewSizePercent=5 -XX:G1MaxNewSizePercent=60 -DEUREKA_SERVER=-DEUREKA_SERVER=http://192.168.0.204:8761/eureka/ -Dspring.profiles.active=prod -Dspring.cloud.config.profile=prod -XX:+UseG1GC -XX:+AggressiveOpts -XX:+UseFastAccessorMethods -XX:+UseStringDeduplication -XX:+UseCompressedOops -XX:+OptimizeStringConcat
  • 如果你感兴趣还可以这样
docker run -it --rm -e JAVA_HEAP_XMS=8G -e JAVA_HEAP_XMX=8G hub.xxx.com/my/user-server:latest sh
    echo $JAVA_OPTS
    -Dservice.name=user-server -XX:+UnlockExperimentalVMOptions -Xms4G -Xmx4G -XX:CICompilerCount=8 -XX:G1NewSizePercent=5 -XX:G1MaxNewSizePercent=60 -DEUREKA_SERVER=-DEUREKA_SERVER=http://192.168.0.204:8761/eureka/ -Dspring.profiles.active=prod -Dspring.cloud.config.profile=prod -XX:+UseG1GC -XX:+AggressiveOpts -XX:+UseFastAccessorMethods -XX:+UseStringDeduplication -XX:+UseCompressedOops -XX:+OptimizeStringConcat
  • 思考:同理我们想要灵活的修改其他参数是不是一个流程呢?比如我要修改G1的G1NewSizePercent和G1MaxNewSizePercent。有几种方式呢?你觉得那种方式更优雅呢?

你可能感兴趣的:(如何构建SpringBoot的Docker镜像)