• SpringBoot系列: 制作Docker镜像的全过程


    本文主要参考了 https://segmentfault.com/a/1190000016449865 , 感谢作者的付出. 另外,  在本文中, 演示了Windows+Maven+Docker Toolbox环境下的制作全过程. 

    和 CI 工具的集成, 可以参考下面文章:
    https://spring.io/guides/topicals/spring-boot-docker/
    https://spring.io/guides/gs/spring-boot-docker/

    =======================================
    Demo 性质的 Dockerfile 文件
    =======================================
    本 Dockerfile 仅仅适合简单的测试. 它不满足下面提及生产环境的几个要求.

    FROM openjdk:8-jdk-alpine
    
    ARG JAR_FILE
    
    COPY target/${JAR_FILE} app.jar
    
    ENTRYPOINT ["java","-Djava.security.egd=file:/dev/./urandom","-jar","/app.jar"]
    
    # 指定暴露端口, 这样在容器运行时可以知道应该映射哪些端口
    EXPOSE 8080


    =======================================
    生产环境对 Docker 容器的要求
    =======================================
    1. 容器的时区应该是东 8 区
    2. 容器中的程序不应该以 root 账号启动
    3. 能传递 JVM 参数、Java System Properties、程序自定义参数等


    =======================================
    Docker 容器的规划
    =======================================
    默认情况下, docker 容器中的用户是 root, 该 root 就是 HostOS 的 root, 应用程序直接使用 root 账号存在较大的安全风险, 所以容器用户应该采用非 root 用户. 在我们的规划中, 容器将使用 java-app 用户, 对应的用户组为 java-app.
    另外, 如果在 Dockerfile 中需要使用 sudo 命令, 推荐使用 gosu 而不是 sudo, sudo 会引起 TTY 和信号转发异常.

    如果使用的是 openjdk:<version>-alpine, Dockerfile 新建用户的指令为:

    RUN set -eux; 
        addgroup --gid 1000 java-app; 
        adduser -S -u 1000 -g java-app -h /home/java-app/ -s /bin/sh -D java-app;
        

    如果使用的是 openjdk:<version>-slim 和标准 openjdk:<version>, Dockerfile 新建用户的指令为:

    RUN set -eux; 
        addgroup --gid 1000 java-app; 
        adduser --system --uid 1000 --gid 1000 --home=/home/java-app/ --shell=/bin/sh --disabled-password java-app;

    在创建用户 java-app 后, Dockerfile 可以使用 USER java-app 指令明确运行的用户.

    容器中的目录规范如下:
    /home/java-app
    ├── docker-entrypoint.sh
    ├── lib
    │ └── app.jar
    ├── etc
    ├── logs
    └── tmp

    =======================================
    功能完备 Dockerfile 文件
    =======================================

    -------------------------
    Dockerfile 文件
    -------------------------
    存放位置: Dockerfile 文件应和 pom.xml 放在同一个目录下.
    源码参考: https://github.com/chanjarster/dockerfile-examples/blob/master/Dockerfile

    修改点有:
    1. 增加了 VOLUME /tmp 指令, /tmp 目录是 Tomcat 的缺省工作目录, 加上 VOLUME /tmp 指令容器会自动映射一个目录到 Host OS 的 /var/lib/docker 下.
    2. base 镜像从 openjdk:8-alpine 修改为 openjdk:8-jdk-alpine, 貌似后者是正式名称.
    3. 增加 docker-entrypoint.sh 赋予执行权限, 不然会报 permission denied 错误. 

    FROM openjdk:8-jdk-alpine
    
    ARG NAME
    ARG VERSION
    ARG JAR_FILE
    
    LABEL name=$NAME 
          version=$VERSION
    
    # 设定时区
    ENV TZ=Asia/Shanghai
    RUN set -eux; 
        ln -snf /usr/share/zoneinfo/$TZ /etc/localtime; 
        echo $TZ > /etc/timezone
    
    # 新建用户 java-app
    RUN set -eux; 
        addgroup --gid 1000 java-app; 
        adduser -S -u 1000 -g java-app -h /home/java-app/ -s /bin/sh -D java-app; 
        mkdir -p /home/java-app/lib /home/java-app/etc /home/java-app/jmx-ssl /home/java-app/logs /home/java-app/tmp /home/java-app/jmx-exporter/lib /home/java-app/jmx-exporter/etc; 
        chown -R java-app:java-app /home/java-app
    
    # 导入启动脚本
    COPY --chown=java-app:java-app docker-entrypoint.sh /home/java-app/docker-entrypoint.sh
    
    # 赋执行权限
    RUN ["chmod", "+x", "/home/java-app/docker-entrypoint.sh"]

    # 导入 JAR COPY
    --chown=java-app:java-app target/${JAR_FILE} /home/java-app/lib/app.jar USER java-app # 增加 sh 前导命令, 避免出现权限不足问题 ENTRYPOINT ["/home/java-app/docker-entrypoint.sh"] # 指定暴露端口, 这样在容器运行时可以知道应该映射哪些端口 EXPOSE 8080 #在容器运行时声明一个 volume, 在容器中的目录为 /tmp VOLUME /tmp


    -------------------------
    docker-entrypoint.sh
    -------------------------
    存放位置: docker-entrypoint.sh 文件应和 pom.xml 放在同一个目录下.
    源码参考: https://github.com/chanjarster/dockerfile-examples/blob/master/docker-entrypoint.sh
    修改点有:
    1. 为了减少 Tomcat 启动时间, java 启动参数中增加 /dev/urandom 作为随机数的熵.

    2. 在 java 命令之前加上 exec 命令, 这样确保 pid 1是java , 而不是 sh . 

    #!/bin/sh
    
    set -ex;
    
    exec /usr/bin/java 
      $JAVA_OPTS 
      -Djava.io.tmpdir="/home/java-app/tmp" 
      -Djava.security.egd=file:/dev/./urandom 
      -jar 
      /home/java-app/lib/app.jar 
      "$@"
       

    =======================================
    pom.xml 增加 dockerfile-maven-plugin 插件
    =======================================
    Spotify 开源的 dockerfile-maven-plugin 插件, 可以在 maven build 的时候基于 Dockerfile 生成 docker 镜像, 需要说明的是, 该插件不是帮助我们生成 Dockerfile 文件的. 使用该插件的好处主要好处有:
    1. 直接和 maven 集成;
    2. 我们可以在 pom.xml 定义参数, 然后很方便第通过该插件将参数传到 Dockerfile 中.

    注意:
    pom.xml 目标的 artifactId 必须是全部为小写字母, 否则后续制作 docker 镜像会报网络错误, 错误内容为: Connection reset by peer: socket write error

    设定 docker 镜像名的前缀和 registry 地址:

    <properties>
      <!--docker 镜像的组织名 --> 
       <docker.image.prefix>myorg</docker.image.prefix>
       <!--docker registry 的路径, 如果是本地 registry, 取值为空 -->
       <docker.registry>localhost:5000/</docker.registry>   
    </properties>

    指定最终 jar 的生成规则, 并启用 dockerfile-maven-plugin 插件:

    <build>
        <finalName>${project.artifactId}-${project.version}</finalName>
        <plugins>
            <plugin>
                <groupId>com.spotify</groupId>
                <artifactId>dockerfile-maven-plugin</artifactId>
                <version>1.4.8</version>
                <configuration> 
                    <repository>${docker.registry}${docker.image.prefix}/${project.artifactId}</repository> 
                    <!--指定 registry 服务器的用户和密码 -->
                    <!--
                    <username>repoUserName</username>
                    <password>repoPassword</password>                
                    -->       
                  <useMavenSettingsForAuth>true</useMavenSettingsForAuth>
                  <tag>${project.version}</tag>
                  <buildArgs>
                        <JAR_FILE>${project.build.finalName}.${project.packaging}</JAR_FILE>                     
                        <VERSION>${project.version}</VERSION>
                        <NAME>${project.artifactId}</NAME>
                  </buildArgs>
                </configuration>
            </plugin>
        </plugins>
    </build>

    =======================================
    准备 Windows 的镜像编译环境
    =======================================
    docker 镜像编译需要连接一个 docker daemon, 我使用 Docker Toolbox for windows 准备环境, 下面是准备步骤:

    (1) 创建一个 Docker2Boot 虚机, 名称为 vm1
    docker-machine create --driver virtualbox vm1
    (2) 检查所有 Docker2Boot 虚机, 会显示每个虚机是否有证书问题
    docker-machine.exe ls
    (3) 如果 vm1 证书有问题, 修复它
    docker-machine.exe regenerate-certs vm1
    (4) 设置 vm1 为缺省的 Docker2Boot 虚机
    docker-machine.exe env vm1
    然后照着该命令的输出, 将它们都增加 Windows 的环境变量中, 并重启机器.
    (5) 验证 vm1 应该是当前 active 的 vm
    docker-machine.exe active

    镜像编译需要连接 docker daemon, 到底要连接哪一台机器上的 docker daemon, dockerfile-maven-plugin 插件是按下面的顺序确定目标 docker daemon 的:
    1. 如果配置了 DOCKER_HOST 等一系列环境变量, 按照环境变量为准.
    2. 如果没有设定环境变量, 会在本机的 ~/.docker/ 配置目录找相应的连接信息.
    3. 如果是 jenkins 服务器的话, 配置目录应该是 C:WindowsSystem32configsystemprofile.docker

    因为我们已经设置了 Windows 环境变量, 不需要再关心 ~/.docker/ 目录中的配置.


    =======================================
    docker 镜像编译
    =======================================

    ---------------------------------------
    推荐: 使用 dockerfile-maven-plugin 插件
    ---------------------------------------
    我是在 Windows Eclipse 中完成 maven 编译过程的.

    构建 docker 镜像的 maven 命令为:
    mvn clean package dockerfile:build -DskipTests

    push 镜像到 docker 私服
    mvn clean package dockerfile:push -DskipTests


    ---------------------------------------
    使用 docker 命令直接编译
    ---------------------------------------
    用 maven package 后, 会在 target 目录下生成最终项目 jar, 然后用下面命令制作 docker image
    $ docker build --build-args=target/*.jar -t myorg/myapp:v1 .

    docker build 的重要参数:
    --build-args list , 如果 Dockerfile 中设定了 ARG, 用这个参数传入变量值
    -t 设定镜像的 tag, 格式为 reps/name:version
    -f 指定 Dockerfile 名称, 如果缺省, 文件名为 Dockerfile

    =======================================
    运行容器
    =======================================
    docker run -init -p 8080:8080 myorg/java-examples-1:1.0-SNAPSHOT
    docker run -init  -p 8080:8080 -e JAVA_OPTS='-Xmx128M -Xms128M -Dabc=xyz -Ddef=uvw' myorg/java-examples-1:1.0-SNAPSHOT
    docker run -init  -p 8080:8080 myorg/java-examples-1:1.0-SNAPSHOT --debug

    对于 Java 8, 推荐增加下面的 JVM 参数, 用来开启容器内存使用的 hint, 防止 SpringBoot 超用内存, Java 11 之后会自动开启该选项.
    -XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap

     
    ============================
    docker 微服务的优雅关闭
    ============================

    使用 docker stop 关闭容器时, 只有 init(pid 1)进程能收到中断信号, 如果容器的pid 1 进程是 sh 进程, 它不具备转发结束信号到它的子进程的能力, 所以我们真正的java程序得不到中断信号, 也就不能实现优雅关闭. 解决思路是: 让pid 1 进程具备转发终止信号, 或者将 java 程序配成 pid 1 进程.

    需要说明的是, docker stop 默认是等待10秒钟, 这个时间有点太短了, 可以加 -t 参数, 比如 -t 30 等待30秒钟.

    ----------------------------------
    背景知识
    ----------------------------------

    上面的 Dockerfile 的pid 1是一个 sh 命令,并不能实现优雅关闭, 需要再改进.

    ENTRYPOINT/CMD 的几种写法, 会影响 pid 1 进程的产生:
    写法1:
    "shell" format 的 ENTRYPOINT/CMD, 不带方括号:
    ENTRYPOINT top -b
    #PID 1 是 /bin/sh -c shell  top -b
    #另外有个 pid 7 是 top -b

    写法2:
    "shell" format 的 ENTRYPOINT/CMD, 不带方括号, 但这次ENTRYPOINT后紧跟了一个 exec :
    ENTRYPOINT exec top -b
    #PID 1 是 top -b

    写法3:
    "exec" form 的 ENTRYPOINT/CMD, 方括号括着, 每个部分都是json字符串.
    ENTRYPOINT ["top","-b"]
    pid 1 进程就是 top -b

    所以推荐使用"exec" form的命令, 而不是 "shell" 形式的命令.


    ----------------------------------
    init 进程调整方案
    ----------------------------------
    方案1: 自行确保 pid 1 是我们的java程序.
    上面的 Dockerfile 可以确保 java 程序作为 pid 1进程.

    方案评价: 有时候不太容易将我们的主程序调整为 pid 1 进程, 另外虽然 docker 容器推荐是单进程, 但实际情形往往不是这么理想. 本方案仅仅适合单进程容器.

    方案2: 适合于 Docker 1.13 以上.
    Docker 1.13以上的docker run 命令新增了 --init 参数, 加了该参数后, docker 会启用 tini 作为 init (pid 1) 进程, 该 tini 进程能够将终止信号转发给其子进程, 同时能reap 子进程, 不会出现因孤儿进程导致的线程句柄无法回收情形.
    详见: https://github.com/krallin/tini

     

    方案3(推荐): 在docker镜像中强制 tini 作为 init(pid 1) 进程, 该方案使用范围广, ENTRYPOINT 可以是任意sh脚本文件.

    改造之前的 Dockerfile

    ENTRYPOINT ["/docker-entrypoint.sh"]

    改造后的 Dockerfile

    # Add Tini
    ENV TINI_VERSION v0.18.0
    ADD https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini /tini
    RUN chmod +x /tini
    
    ENTRYPOINT ["/usr/local/bin/tini", "--", "/docker-entrypoint.sh"]


    tini 文档:
    https://github.com/krallin/tini
    有关 docker run --init 参数的说明
    http://stackoverflow.com/a/39593409/6309



    ===============================
    更多推荐
    ===============================
    https://efekahraman.github.io/2018/04/docker-awareness-in-java

  • 相关阅读:
    在Android studio中,测试输出数组中最大子数组的和
    我所理解的软件开发模式
    java实现随机输出300题四则运算
    Demo(3月28日)
    关于构建之法中小飞问题的个人看法
    对搭档代码的一些意见
    项目复审
    安卓UI测试(基于android studio环境 espresso框架)
    读构建之法后的一些个人感受
    思考题
  • 原文地址:https://www.cnblogs.com/harrychinese/p/springboot_Dockerize_SpringBoot_App.html
Copyright © 2020-2023  润新知