使用docker构建自己的应用
引言
自从docker入门以后,一发不可收拾,越学习越感觉有趣,本文记录一下在学习dockerfile构建自己应用遇到的坑以及学习心得
Talk is cheap, show me the code
环境
使用gradle+jdk11编译及打包springboot项目,然后使用docker制作镜像
项目
springboot,作为现在最流行的微服务基础框架,我相信大家已经非常非常熟悉了,即使没有使用过,肯定听说过.一般比较通用的创建方式是使用springboot官网提供的创建工具进行创建,如果你使用intellij idea那么也可以在创建的时候使用spring initializr,这个和使用官方提供的创建工具是一回事
命令
FROM
该指令是dockerfile的起始命令,是必须的,而且必须是第一个,作用是以一个镜像为基础,在该镜像上进行定制.FROM [--platform=<platform>] <image>[:<tag>] [AS <name>]FROM [--platform=<platform>] <image> [AS <name>]FROM [--platform=<platform>] <image>[@<digest>] [AS <name>]ARG
该指令是声明一个变量:1
ARG <name>[=<default value>]
如果想覆盖默认值,可以在执行
docker build命令时候指定--build-arg <name>=<value>
ps:在FROM之前声明的ARG在构建阶段之外,因此,FROM之后的任何指令都不能使用它。要使用在第一个FROM之前声明的ARG的默认值,请使用ARG指令,且在构建阶段内部不带值
1
2
3
4
ARG VERSION=latest
FROM busybox:$VERSION
ARG VERSION
RUN echo $VERSION > image_version
LABEL
该指令添加metadata到镜像之中,格式为键值对,如:LABEL maintainer="dengbojing@qq.com"ps: 这里正好用
maintainer字段来说明一下,官方已经将MAINTAINER这个命令废弃,改用LABEL代替RUN
该指令有两种格式shell格式,
RUN <command>command将会在shell中执行,对于linux系统shell为/bin/bash, 对于windows系统shell为 cmd /S /Dexec格式,
RUN ["executable", "param1", "param2"], 注意该指令不会进行shell处理,比如RUN ["echo", "$home"]是不会对$home处理的,你需要自己指定shell,RUN ["sh", "-c", "echo $home"].该命令执行一次会产生一层layer,所以应该尽量合并
RUN后面command比如:RUN && apt-get update
&& apt-get install -y $buildDeps
CMD
该指令主要作用是为容器提供一个默认的执行命令,三种格式:exec格式,
CMD ["executable","param1","param2"],该格式是官方推荐首选格式,同样该格式也不会进行shell处理.参数格式:
CMD ["param1","param2"], 该格式需要指定ENTRYPOINT,作为ENTRYPOINT的参数shell格式,
CMD command param1 param2ps: 该指令在文件中只有一个,如果有多个那么只有最后一个
CMD才会起作用,如果在docker run后面指定了其他命令或者参数会覆盖CMD后面的命令或者参数
ENTRYPOINT
该指令主要作用是为容器提供一个每次都执行的命令,该命令有两种格式:exec格式:
ENTRYPOINT ["executable", "param1", "param2"],官方推荐
shell格式:ENTRYPOINT command param1 param2ps: 同
CMD指令如果有多个ENTRYPOINT也只有最有一个起作用,如果想覆盖默认的ENTRYPOINT可以使用:docker run --entrypoint;不同点在于,该指令可以直接在docker run后面跟参数,而CMD指令不可以.COPY
顾名思义,该指令主要作用就是–复制,两种格式:COPY [--chown=<user>:<group>] <src>... <dest>COPY [--chown=<user>:<group>] ["<src>",... "<dest>"]ps: 1. 该指令的
--chown只有linux才有,windows和linux权限管理不一样;另外该指令还支持通配符2. 该指令只会cp源目标下文件. 3. 如果目标目录没有/, 则会将目标地址当成一个文件 4. 如果目标目录不存在, 则会创建目标目录的所有层级的目录
EXPOSE
该指令暴露一个容器内部端口到外部,格式为:EXPOSE <port> [<port>/<protocol>...]
ps: 该指令并非真正暴露一个端口供外部使用,只是一种说明,说明容器内部哪些端口可以被访问,在启动时候需要使用docker run -p <out port>:<expose port>
- WORKDIR
该指令指定工作目录,相当于shell命令里面的cd,指定工作目录之后,后续的COPY,RUN,CMD,ENTRYPOINT等命令都是在当前目录下完成
USAGE & CONTEXT
当执行 docker build 的时候需要一个 Dockerfile 文件和一个 context, context 的涵义是指包含一些列文件的PATH或者URL,这里的 PATH 代表了文件系统的目录, URL 则代表了 Git 仓库地址.
这里文件系统的目录是包含下面的子目录以及子目录中的文件,也就是 whole directory 都会被作为上下文发送给 docker daemon.
docker build 构建的时候不是在CLI(命令行界面)构建而是把 当前目录 作为 context 发送给 docker daemon, 也就是docker的守护进程,所以说不能发送过大的目录,特别是不要在根目录执行 docker build, 官方推荐是使用一个空目录作为 context 来存放 Dockerfile ,仅仅添加 Dockerfile 需要的文件.
这里遇到一些问题,执行 docker build 命令的时候会将当前目录作为 context 发送给守护进程, 但是 Dockerfile 不能直接使用这些文件,官方说明为:
To use a file in the build context, the Dockerfile refers to the file specified in an instruction, for example, a COPY instruction
翻译过来就是–要使用 context 中使用某个文件, Dockerfile 指定一个命令来引用这个文件,例如: COPY 命令, 换句话说,就是这些文件发送给守护进程,但是不能直接使用,得通过命令来使用(后面会说明碰到的问题).
制作
学习了dockerfile和指令之后,我想到应该有两种方式制作镜像
方法一: 使用gradle构建项目,然后在使用dockerfile把jar包制作成镜像: 这种方法简单,但是感觉没什么意义啊,不过随后我还真的在springboot官方指导下找到了这个方法.
第一步,执行gradle构建项目gradle build -x test
第二步,编写dockerfile
1
2
3
4
5
6FROM openjdk:11
LABEL maintainer="dengbojing@qq.com"
ARG JAR_FILE=build/libs/*.jar
COPY ${JAR_FILE} app.jar
EXPOSE 8090
ENTRYPOINT ["java","-jar","/app.jar"]docker build -t dengbojing/gateway .点代表把当前目录作为context发送给dockerdeamon
方法二: 把方法一的第一步放在Dockerfile里面,这样就少执行一步命令,
进入项目目录,新建一个空白的
Dockerfile文件,填写如下内容:1
2
3
4
5
6FROM openjdk:11
LABEL maintainer="dengbojing@qq.com"
COPY . .
RUN ./gradlew build -x test
EXPOSE 8090
ENTRYPOINT ["java","-jar","build/libs/service-gateway-0.0.1-SNAPSHOT.jar"]ps: 第一次写命令时候不了解
Dockerfile和context的工作原理,觉得将当前工作目录发送给docker daemon就能直接使用了,没有写COPY . ., 结果就是怎么都运行不过去,找不到gradlew文件.后面Google之,看到这种写法,一脸懵,后来请教群里大神,加上仔细阅读文档,最终解惑.这种方法有一个弊端,就是构建之后的镜像会比较大,因为
gradle构建项目阶段所需要的额外的文件最终也被添加到镜像中了, 所以官方提供了多阶段构建. 例:1
2
3
4
5
6
7
8
9
10FROM openjdk:11 AS build
LABEL maintainer="dengbojing@qq.com"
COPY . .
RUN ./gradlew build -x test
FROM openjdk:11 AS final
WORKDIR /app
COPY --from=build build/libs/service-gateway-0.0.1-SNAPSHOT.jar app.jar
EXPOSE 8090
ENTRYPOINT ["java","-jar","app.jar"]可以对比一下两种不同方式构建的镜像最后的大小, 如下图:

可以看到,v1是通过非多阶段构建的,构建之后有1.16g大小,而通过多阶段构建,抛弃了gradle文件,只留下需要的项目jar包, 只有652M,好处显而易见.
ps: 如果还想那个精简,那么可以使用jre而非jdk; 我这里是使用的自己的一个spring-cloud-gateway项目进行学习的.
方法三: 以上的方法,是我直观能想到的方法,但是通过学习,找到了更简便的方法,那就是
gradle插件,编写gradle构建脚本,生成docker镜像, 具体文档, 点击这里
后记
目前方法三还处于理论阶段,文档是看懂了,但是没有实质性的操作过.而且在项目构建过程中涉及到网络通信, spring-cloud 所有的项目都应该在注册中心注册, 我采用的 zookeeper 作为注册中心和配置中心, 这就涉及到了两个容器之间相互通信, 目前还没有学会, 目前做法是在宿主机启动 zookeeper, 然后找到 docker 虚拟网卡, 找到宿主机相对于 docker 的 ip address , 将镜像里面的 zk 地址改为宿主机相对于容器的ip, 这种方法很不容器化, 所以接着学习, 学会很容器化的方式方法.
骐骥一跃,不能十步;驽马十驾,功在不舍.