本文主要参考了 , 感谢作者的付出. 另外, 在本文中, 演示了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
如果使用的是 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 的 vmdocker-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 -DskipTestspush 镜像到 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-SNAPSHOTdocker run -init -p 8080:8080 -e JAVA_OPTS='-Xmx128M -Xms128M -Dabc=xyz -Ddef=uvw' myorg/java-examples-1:1.0-SNAPSHOTdocker 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"]