前面快速开始我们通过编写Dockerfile文件创建镜像,这是我们最常见的使用docker部署应用的方式,也间接说明了熟练使用Dockerfile文件的重要程度。那么本章我们就来重点讲解下Dockerfile常用的指令使用和说明以及编写Dockerfile最佳实践。下面我们先看几个常见的自定义Dockerfile文件:
1、自定义JDK镜像
FROM ubuntu:16.04
RUN mkdir -p /opt/software
COPY jdk1.8.0_161 /opt/software/jdk1.8.0_161
ENV JAVA_HOME /opt/software/jdk1.8.0_161
ENV PATH $JAVA_HOME/bin:$PATH
2、前一章的自定义springboot 应用镜像
FROM ubuntu:16.04
RUN mkdir -p /opt/applications/helloworld &&\
mkdir -p /opt/software
COPY jdk1.8.0_161 /opt/software/jdk1.8.0_161
COPY lazy-study-docker-0.0.1-SNAPSHOT.jar /opt/applications/helloworld/
ENV JAVA_HOME /opt/software/jdk1.8.0_161
ENV PATH $JAVA_HOME/bin:$PATH
CMD java -jar /opt/applications/helloworld/lazy-study-docker-0.0.1-SNAPSHOT.jar
微信扫码二维码,关注Java软件变成之家公众号,发送“docker”关键字,获取完整docker简明教程,
有任何疑问可以咨询作者。
下面我们通过问答的方式讲解Dockerfile常用的指令
问:Dockerfile常用有哪些指令?
答:常用指令如下:
1、FROM
语法:
FROM <image>[:<tag>] [AS <name>]
说明:
为后续指令设置基础镜像,有效Dockerfile必须以FROM指令开头。仅ARG可以在FROM前面。FROM可以在单个Dockerfile镜像中多次出现以创建多个镜像,或者使用其中一个构建阶段作为另一个构建阶段的依赖项。只需在每条新FROM指令之前 。每条FROM指令都清除先前指令创建的任何状态。
例子1:
FROM openjdk:latest
例子2:
ARG version=3.7
FROM alpine:${version}
#如果在FROM后面想试用ARG定义的变量,则需要这一行
ARG version
RUN echo "alpine version is: ${version}"
例子3:
ARG CODE_VERSION=latest
FROM base:${version}
CMD /code/run-app
FROM extras:${version}
CMD /code/run-extras
2、RUN
语法:
RUN <command>
RUN ["executable", "param1", "param2"]
说明:
RUN指令将在当前镜像之上的新图层中执行任何命令并提交结果。生成一个新的镜像。RUN在下一次构建期间,指令的缓存不会自动失效。类似指令的缓存 RUN apt-get dist-upgrade -y将在下一次构建期间重用。例如,RUN可以通过使用--no-cache 标志使指令的高速缓存无效docker build --no-cache。
更多优化信息可以参考官方文档:
https://docs.docker.com/develop/develop-images/dockerfile_best-practices/
例子1:
RUN mkdir -p /usr/local/applications
例子2:
RUN ["mkdir ", "-p", "/usr/local/applications"]
上面只能用双引号,不能用单引号
3、CMD
语法:
CMD ["executable","param1","param2"](这是首选形式)
CMD ["param1","param2"](作为ENTRYPOINT的默认参数)
CMD command param1 param2(shell形式)
说明:
该CMD指令指定在运行映像时要执行的命令。一个Dockerfile只能有一个CMD命令,如果有多个,只有最后一个CMD命令生效。
更多优化信息可以参考官方文档:
https://docs.docker.com/develop/develop-images/dockerfile_best-practices/
例子1:
CMD java -jar xxxx.jar
例子2:
CMD ["/usr/local/jdk8/bin/java", "-jar", "/usr/local/applications/xxxx.jar"]
例子3:
CMD ["/usr/local/jdk8/bin/java", "--version"]
上面只能用双引号,不能用单引号
4、LABEL
语法:
LABEL <key>=<value> <key>=<value> <key>=<value> ...
说明:
该LABEL指令将元数据添加到镜像。 LABEL是键值对。要在LABEL值中包含空格,请使用引号和反斜杠.
例子1:
LABEL "com.example.vendor"="ACME Incorporated"
LABEL com.example.label-with-value="foo"
LABEL version="1.0"
LABEL description="This text illustrates \
that label-values can span multiple lines."
5、MAINTAINER(不再推荐使用)
语法:
MAINTAINER <name>
说明:
该MAINTAINER指令设置生成的镜像的Author字段。该LABEL指令是一个更灵活的版本,您应该使用它,因为它可以设置您需要的任何元数据,并且可以轻松查看,例如使用docker inspect。要设置与MAINTAINER您可以使用的字段对应的标签 :
LABEL maintainer="lazy"
然后可以从docker inspect其他标签中看到这一点。
6、EXPOSE
语法:
EXPOSE <port> [<port>/<protocol>...]
说明:
该EXPOSE指令通知Docker容器在运行时侦听指定的网络端口。您可以指定端口是侦听TCP或者UDP,如果未指定协议,则默认为TCP。
该EXPOSE指令实际上不发布端口。它起到一种文档描述的作用,关于哪些端口要发布。要在运行容器时通过-p publish_port:target_port方式进行实际发布端口。
例子1:
EXPOSE 80/tcp
例子2:
EXPOSE 80/upd
7、ENV
语法:
ENV <key> <value>
ENV <key>=<value> ...
说明:
该ENV指令将环境变量<key>设置为该值 <value>。此环境变量将在构建阶段中的所有后续指令使用
例子1:
ENV JAVA_HOME /opt/software/jdk1.8.0_161
ENV PATH ${JAVA_HOME}/bin:$PATH
CMD echo "java home env is:${JAVA_HOME}"
8、ADD
语法:
ADD [--chown=<user>:<group>] <src>... <dest>
ADD [--chown=<user>:<group>] ["<src>",... "<dest>"]
说明:
与 COPY 类似,从 build context 复制文件到镜像。不同的是,如果 src 是归档文件(tar, zip, tgz, xz 等),使用ADD指令的化文件会被自动解压到 dest。
使用ADD需要注意细节问题:
1)src路径必须是构建上下文路径,不能 ADD ../xxxx/xxx /
2)如果src是URL,并且dest不以尾部斜杠结尾,则从URL推断文件名并将文件下载到<dest>/<filename>
3)如果<src>是目录,则复制目录的全部内容,包括文件系统元数据。注意,不复制目录本身,只复制其内容。
4)如果<src>是以识别的压缩格式(identity,gzip,bzip2或xz)的本地 tar存档,则将其解压缩为目录
5)如果<src>直接或由于使用通配符指定了多个资源,则<dest>必须是目录,并且必须以斜杠结尾/
6)如果<dest>不存在,则会在其路径中创建所有缺少的目录。
例子1:
ADD jdk1.8.0_161.tar.gz /opt/software/jdk1.8.0_161
例子2:
ADD --chown=root:root jdk1.8.0_161.tar.gz /opt/software/
9、COPY
语法:
COPY [--chown=<user>:<group>] <src>... <dest>
COPY [--chown=<user>:<group>] ["<src>",... "<dest>"]
说明:
与 ADD 类似,从 build context 复制文件到镜像。
例子1:
COPY jdk1.8.0_161 /opt/software/jdk1.8.0_161
例子2:
COPY --chown=root:root jdk1.8.0_161 /opt/software/jdk1.8.0_161
10、ENTRYPOINT
语法:
ENTRYPOINT ["executable", "param1", "param2"] (执行形式,首选)
ENTRYPOINT command param1 param2 (shell形式)
说明:
设置容器启动时运行的命令。Dockerfile 中可以有多个 ENTRYPOINT 指令,但只有最后一个生效。CMD 或docker run 之后的参数会被当做参数传递给 ENTRYPOINT。
例子1:
ENTRYPOINT ["top","-b", "-H"]
例子2:
ENTRYPOINT top -b -H
11、VOLUME
语法:
VOLUME ["/data"]
说明:
VOLUME 指令可以在镜像中创建挂载点,这样只要通过该镜像创建的容器都有了挂载点。通过 VOLUME 指令创建的挂载点,无法指定宿主机上对应的目录。对应到宿主机的目录是由docker管理的,默认路径为/var/lib/docker/随机id/_data.我们可以通过命令:
docker inspect myhelloworld | grep Mounts -A 10
查看镜像挂载在宿主机的路径列表。
例子1:
VOLUME["/logs"]
12、USER
语法:
USER <user>[:<group>] or
USER <UID>[:<GID>]
说明:
指定后面RUN CMD ENTRYPOINT指令运行的用户权限,当用户没有组时,默认为root组
例子1:
USER mysql
13、WORKDIR
语法:
WORKDIR /path/to/workdir
说明:
指定后面RUN CMD ENTRYPOINT指令将在那个目录下进行,一旦指定,WORKIDR后面的目录必须会创建
例子1:
WORKDIR /hello
WORKDIR world
RUN pwd
最终pwd命令的输出将是 /hello/world
14、ARG
语法:
ARG <name>[=<default value>]
说明:
该ARG指令定义了一个变量,用户可以docker build使用该--build-arg <varname>=<value> 标志在构建时将该变量传递给构建器
例子1:
FROM openjdk
ARG var1
ARG var2=defaultval
如果 ARG指令具有默认值,并且在构建时没有传递值,则构建器将使用默认值。
以上为常用的Dockerfile指令,想了解更多详细指令说明参考官方文档:
https://docs.docker.com/engine/reference/builder/
问:编写Dockerfile文件用来做什么?
答:我们的应用程序如果需要以docker容器方式来部署的话,一般需要自己编写Dockerfile文件,通过docker build -t xxxxx -f Dockerfile . 命令构建出docker镜像。注意后面有个“.” 这个“.”代表构建上下文为当前路径。例如COPY 或ADD命令一般都是从当前构建上下文获取文件资源复制到构建的镜像中的。
问:编写Dockerfile一般套路是什么?
答:编写Dockerfile之前我们需要更多的了解镜像的知识,镜像是有层(layer)的概念,一般的镜像都是一层一层堆叠起来的,这些层对容器来说是只读的。当通过镜像创建容器的时候,会在当前创建的容器最顶层再创建一个简小的可写层,这个可写层可以给实际运行程序写入,创建目录等操作,一旦容器停止且删除,那么这个可写层随之被删除,也就是说这时容器的程序数据不会被保存,会丢失,所以通常我们会通过volume或挂载等方式将需要保留的数据绑定/映射到宿主机上。
编写Dockerfile一般套路是基于某个层创建自己层的时候,使用FROM语法,就像上面那样FROM openjdk:latest,这里是基于openjdk镜像层的基础上增加我们的层。
当然,也可以使用多阶段构建方式,多阶段构建方式一般是在同一个Dockerfile文件中有多个FROM xxx命令,然后后面的FROM 下面的COPY --from=0这种方式应用前面的FROM,这属于优化层面的东西,这里暂时不过多讨论,有兴趣读者可以查看官方文档:
https://docs.docker.com/develop/develop-images/multistage-build/
问:我们怎么找到好的基础镜像,比如上面的openjdk
答:很多基础镜像都是由其官方提供,我们执行docker search xxxx命令查找镜像,这是会返回一个列表,此时我们关心的是STARTS和OFFICIAl这两列,STARTS是被标星或点赞数量,OFFICIAl为OK一般就是其官方发布的,如下图所示。
另外如果我们需要在基础镜做些个性化安装的话(比如需要在操作系统上安装某些字体),我们也可以自定义基础镜像,比如自定义一个myjdk8镜像Dockerfile文件内容如下:
FROM ubuntu:16.04
MAINTAINER lazy
RUN mkdir -p /opt/software
COPY jdk1.8.0_161 /opt/software/jdk1.8.0_161
ENV JAVA_HOME /opt/software/jdk1.8.0_161
ENV PATH ${JAVA_HOME}/bin:$PATH
上面我们自定义了目录/opt/software,然后把jdk放到该目录下,然后配置了jdk环境变量。如果需要做其他自定义操作的话,也是类似的套路。
问:编写Dokcerfile需要注意哪些东西
答:编写Dockerfile就像写代码,需要考虑优化问题。说到优化,我们知道,都是有目标和指标的,比如web应用的优化通常是优化速度,指标一般有响应时间、吞吐量、并发数等。想要知道如何编写好的Dockerfile文件就要知道Dockerfile的优化目标和指标。
Dockerfile优化目标为:减少构建出最终的镜像的大小
Dockerfile优化指标为:减少构建出最终的镜像的大小
1)最小化层数
Dockerfile文件每一条指令如RUN,COPY等都会创建一个新的层,所以在写的时候尽量减少命令,可以利用shell &&的方式减少命令次数比如下面:
RUN mkdir -p /dir1
RUN mkdir -p /dir2
RUN mkdir -p /dir3
优化后为:
RUN mkdir -p /dir1 && \
mkdir -p /dir1 && \
mkdir -p /dir1
这样就会由三层变成一层,可以减少构建时间和最终的镜像大小
2)不要安装不必要的包
3)必要时也可以使用STDIN中的DOCKERFILE从本地构建上下文构建
语法如下:
mkdir examplecd example
touch somefile.txt
docker build -t myimage:latest -f- . <<EOF
FROM busybox
COPY somefile.txt .
RUN cat /somefile.txtEOF
4)如果想从某个不包含Dockerfile文件的的项目git仓库构建镜像,可以使用STDIN中的DOCKERFILE从远程构建上下文,就像下面这样:
docker build -t myimage:latest \
-f- https://github.com/xxx/xxx.git <<EOF
FROM busybox
COPY README.md . EOF
5)推荐使用多阶段构建
多阶段构建允许您大幅减小最终图像的大小,官方文档:
https://docs.docker.com/develop/develop-images/multistage-build/
6)利用构建缓存
在检查每条指令时,Docker会在其缓存中查找可以重用的现有映像,而不是创建新的(重复)映像。
如果您根本不想使用缓存,则可以使用docker build命令中的--no-cache=true 选项
问:如何忽略构建上下文的某些文件或目录?
答:可以在构建上下文的目录中创建.dockerignore文件,里面记录需要排除的文件或目录内容
更多优化建议可以自行参考官方文档:
https://docs.docker.com/develop/develop-images/dockerfile_best-practices/