博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
SpringBoot系列: 制作Docker镜像的全过程
阅读量:7028 次
发布时间:2019-06-28

本文共 9093 字,大约阅读时间需要 30 分钟。

本文主要参考了  , 感谢作者的付出. 另外,  在本文中, 演示了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-alpineARG JAR_FILECOPY target/${JAR_FILE} app.jarENTRYPOINT ["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-alpineARG NAMEARG VERSIONARG JAR_FILELABEL name=$NAME \      version=$VERSION# 设定时区ENV TZ=Asia/ShanghaiRUN set -eux; \    ln -snf /usr/share/zoneinfo/$TZ /etc/localtime; \    echo $TZ > /etc/timezone# 新建用户 java-appRUN 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"] # 导入 JARCOPY --chown=java-app:java-app target/${JAR_FILE} /home/java-app/lib/app.jarUSER java-app# 增加 sh 前导命令, 避免出现权限不足问题ENTRYPOINT ["/home/java-app/docker-entrypoint.sh"]# 指定暴露端口, 这样在容器运行时可以知道应该映射哪些端口EXPOSE 8080#在容器运行时声明一个 volume, 在容器中的目录为 /tmpVOLUME /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/shset -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 地址:

myorg
localhost:5000/

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

${project.artifactId}-${project.version}
com.spotify
dockerfile-maven-plugin
1.4.8
${docker.registry}${docker.image.prefix}/${project.artifactId}
true
${project.version}
${project.build.finalName}.${project.packaging}
${project.version}
${project.artifactId}

 

=======================================

准备 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:\Windows\System32\config\systemprofile\.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 TiniENV TINI_VERSION v0.18.0ADD https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini /tiniRUN chmod +x /tiniENTRYPOINT ["/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

转载地址:http://kloxl.baihongyu.com/

你可能感兴趣的文章
凯撒日期
查看>>
svn sqlite:databse disk image is malformed
查看>>
我的困惑
查看>>
Android内核开发:学会分析系统的启动log
查看>>
Shell函数与数组
查看>>
我的友情链接
查看>>
解决CentOS7下运行docker容器时出现的报错Error resolving syscall name...
查看>>
我的友情链接
查看>>
python 常用标准库
查看>>
1分钟实现“延迟消息”功能
查看>>
xen live migrate过程分析
查看>>
Nginx的源码安装
查看>>
认识Nginx
查看>>
我的友情链接
查看>>
FILE 结构体及缓冲区
查看>>
我的友情链接
查看>>
我的友情链接
查看>>
vim的设置
查看>>
linux基础篇-05,linux目录管理ls cd pwd mkdir rmdir tree
查看>>
在android studio上使用git
查看>>