Dockerfile常用指令详解&镜像缓存特性

Dockerfile简介

Dockerfile 是Docker中用于定义镜像自动化构建流程的配置文件。在Dockerfile中,包含了构建镜像过程中需要执行的命令和其他操作。通过Dockerfile可以更加清晰,明确的给定Docker镜像的制作过程,由于仅是简单,小体积的文件,在网络等介质中传递的速度快,能够更快的实现容器迁移和集群部署。
Dockerfile是一个文本文件,其内包含了一条条的指令,每一条指令构建一层,因此每一条指令的内容,就是描述该层应当如何构建。

相对于提交容器修改在进行镜像迁移的方式相比,使用Dockerfile有很多优势:

  • Dockerfile的体积远小于镜像包,更容易进行快速迁移和部署。
  • 环境构建流程记录在Dockerfile中,能够直观的看到镜像构建的顺序和逻辑。
  • 使用Dockerfile构建镜像能够更轻松的实现自动部署等自动化流程。
  • 在修改环境搭建细节时,修改Dockerfile文件更加简单。

实际开发使用中很少会选择容器提交这种方法来构建镜像,而是几乎采用Dockerfile来制作镜像。

Docker执行Dockerfile的大致流程:

  • (1)docker会从Dockerfile文件头FROM指定的基础镜像运行一个容器
  • (2)然后执行一条指令,对容器作出修改。
  • (3)接着执行类似于docker commit的操作,创建一个新的镜像层。
  • (4)在基于刚创建的镜像运行一个新的容器。
  • (4)执行dockerfile中的下一条指令,直到所有指令都执行完毕。

    docker会删除中间层创建的容器,但不会删除中间层镜像,所以可以使用docker run运行一个中间层容器,从而查看每一步构建后的镜像状态,,这样就可以进行调试。

Dockerfile 基本结构

Dockerfile 又一行行命令语句组成,并且支持以#开头的注释行。
Dockerfile 分为四部分:基础镜像信息维护者信息镜像操作指令容器启动时执行指令
例如,以下为一个完整的Dockerfile:

# This dockerfile uses the ubuntu image
# VERSION 2 - EDITION 1
# Author: docker_user
# Command format: Instruction [arguments / command] ..

# Base image to use, this must be set as the first line
1,第一行必须指定,基础镜像信息
FROM ubuntu

# Maintainer: docker_user <docker_user at email.com> (@docker_user)
2,维护者信息
MAINTAINER docker_user [email protected]

# Commands to update the image
3,镜像操作指令
RUN echo "deb http://archive.ubuntu.com/ubuntu/ raring main universe" >> /etc/apt/sources.list
RUN apt-get update && apt-get install -y nginx
RUN echo "\ndaemon off;" >> /etc/nginx/nginx.conf

# Commands when creating a new container
4,容器启动执行指令
CMD /usr/sbin/nginx

其中,已开始必须要指明所基于的镜像名称,接下来一般会说明维护者信息;后面则是镜像操作指令,例如RUN指令。每执行一条RUN指令,镜像添加新的一层,并提交;最后是CMD指令,来指明运行容器时的操作命令。

Dockerfile 常用指令

以下常见的dockerfile指令,基本包含常用的90%功能。

常用指令目录:

1,FROM--指定基础镜像
2,MAINTAINER--指定维护者信息
3,RUN--运行指定的命令
4,CMD--容器启动时执行的命令
5, EXPOSE--声明容器的服务端口
6,ENV--设置环境变量
7,ARG--构建参数
8,COPY--复制文件或目录
9,ADD--更高级的复制文件/目录
10,ENTRYPOINT--入口点
11,ENTRYPOINT与CMD指令结合使用
12,VOLUME--定义匿名卷
13,WORKDIR--指定工作目录
14,USER--指定当前用户
15,ONBUILD--为镜像添加触发器

1,FROM--指定基础镜像
第一条指令必须为FROM指令。
如果不以任何镜像为基础,那么写法如下,同时意味着接下来所写的指令将作为镜像的第一层开始:
FROM nginx
FROM 指令支持三种格式:

FROM <image>
FROM <image>:<tag>
FROM <image>:<digest>

三种写法,第二种和第三种是可选项,如果没有选择,那么默认为latest。

在Dockerfile中可以多次出现FROM指令,当FROM第二次或者之后出现时,表示在此刻构建时,要将当前指出镜像的呢日用合并到此刻构建镜像的内容。

ARG是唯一可以在FROM之前执行的参数:

ARG  CODE_VERSION=latest
FROM base:${CODE_VERSION}
CMD  /code/run-app

FROM extras:${CODE_VERSION}
CMD  /code/run-extras

2,MAINTAINER--指定维护者信息
格式为:MAINTAINER <name>,指定维护者信息(镜像维护者姓名或邮箱),可选项。
例如:
MAINTAINER docker_user [email protected]
3,RUN--运行指定的命令
RUN有两种格式:

RUN <command>
RUN ["executable", "param1", "param2"]

前者将在shell终端运行命令,即/bin/sh -c; 后者则使用exec执行。指定使用其它终端可以通过第二种方式实现,例如使用bash终端: RUN ["/bin/bash", "-c", "echo hello"]。

注意:多行命令不要写多个RUN,原因是Dockerfile中每一个指令都会建立一层,多少个RUN就构建了多少层镜像,会造成镜像的臃肿、多层,不仅仅增加了构件部署的时间,还容易出错。一般一个RUN指令后边可以跟多个要执行的命令(使用&&符即可),当命令较长时可以使用"\"来换行。
例子如下:

RUN yum -y install gcc* pcre-devel openssl-devel zlib-devel unzip make vim net-tools elinks tree \
 && groupadd nginx \
 &&  useradd  nginx -g nginx  -s /sbin/nologin 

4,CMD--容器启动时执行的命令
CMD支持三种格式:

CMD ["executable","param1","param2"] 使用 exec 执行,推荐方式;
CMD command param1 param2 在 /bin/sh 中执行,提供给需要交互的应用;
CMD ["param1","param2"] 提供给 ENTRYPOINT 的默认参数;

指定启动容器时执行的命令,每个dockerfile只能有一条CMD命令。如果指令了多条命令,只有最后一条会被执行。

如果用户启动容器时候指定了运行的命令,则会覆盖掉CMD指定的命令。

补充细节:如果不是使用shell(/bin/sh)这种方式,”[]“中括号这里边包括参数的一定要用双引号,千万不能写成单引号。原因是参数传递后,docker解析的是一个JSON array
例如:

[root@sqm-docker01 dfs]# cat Dockerfile 
FROM centos:latest
CMD echo hello world
CMD echo hello china
[root@sqm-docker01 dfs]# docker run --rm test:v1   #只有最后一条CMD命令会被执行
hello china    
[root@sqm-docker01 dfs]# docker run --rm test:v1 echo "hello friend"  #在启动容器时指定运行命令,会将其覆盖
hello friend

采用exec格式,那么上面的例子改为:

FROM centos:latest
CMD ["/bin/bash","-c","echo hello world"]
CMD ["/bin/bash","-c","echo hello china"]

还有一种情况下CMD会结合ENTRYPOINT指令使用,后面会讲到。

5, EXPOSE--声明容器的服务端口
格式为 EXPOSE <端口1> [<端口2>...] 。

EXPOSE 指令是声明运行时容器提供服务端口,这只是一个声明,在运行时并不会因为这个声明应用就会开启这个端口的服务。
在Dockerfile 中写入这样的声明有两个好处,一个是帮助镜像使用者理解这个镜像服务的守护端口,以方便配置映射;另一个用户则是运行时使用随机端口映射时,也就是docker run -P 时,会自动随机映射EXPOSE的端口。
要将 EXPOSE 和在运行时使用 -p <宿主端口>:<容器端口> 区分开来。-p,是映射宿主端口和容器端口,换句话说,就是将容器的对应端口服务公开给外界访问,而 EXPOSE 仅仅是声明容器打算使用什么端口而已,并不会自动在宿主进行端口映射。
例如:
EXPOSE 80 443

6,ENV--设置环境变量
格式有两种:

ENV <key> <value>
ENV <key1>=<value1> <key2>=<value2>...

两者的区别是第一种是一次设置一个,第二种是一次设置多个。

ENV这个指令就是指定一个环境变量,会被后续RUN指定使用,并在容器运行时保持。

ENV VERSION=1.0 DEBUG=on \
    NAME="Happy Feet"

这个例子中演示了如何换行,以及对含有空格的值用双引号括起来的办法,这和shell下的行为是一致的。
举例如下:

[root@sqm-docker01 dfs]# cat Dockerfile 
FROM busybox:latest
ENV var1=haha var2=hehe
RUN echo $var1 > a.txt && echo $var2 > b.txt
执行dockerfile后,进行容器查看文件:
[root@sqm-docker01 dfs]# docker run --rm test:v2 /bin/sh
/ # cat a.txt 
haha
/ # cat b.txt 
hehe
#并且定义的变量会在容器运行时保持:
/ # echo $var1
haha
/ # echo $var2
hehe

7,ARG--构建参数

格式:
ARG <name>[=<default value>]

构建参数和ENV的效果一样,都是设置环境变量。所不同的是,ARG所设置的构建环境的环境变量,在将来容器运行时是不会存在的这些环境变量的。但是不要因此就使用ARG保存密码之类的信息,因为docker history还是可以看到所有值的。

#用法一:在执行docker build构建时传递参数

[root@sqm-docker01 dfs]# cat Dockerfile 
FROM nginx:latest
ARG user
ARG password
RUN echo $user > a.txt \
    && echo $password >> a.txt
#传递两个参数时,则在每个参数中添加--build-arg。
[root@sqm-docker01 dfs]# docker build --build-arg user=sqm --build-arg password=123.com  -t arg:v1 .
#运行容器,参数传递成功
[root@sqm-docker01 dfs]# docker run -it --rm arg:v1 /bin/bash
root@4809b0c54f8d:/# cat a.txt 
sqm
123.com
#与ENV不同,在容器中是不会存在这些变量的
root@9383b7d3d21e:/# echo $user

root@9383b7d3d21e:/# echo $password

root@9383b7d3d21e:/# 

注意:如果指定了该参数,但Dockerfile中未使用,构建过程中会输出警告。

#用法二:在dockerfile中设定一个默认值,如果ARG指令具有默认值,并且在构建时未传递任何参数,那么构建其将使用该默认值

[root@sqm-docker01 dfs]# cat Dockerfile 
FROM nginx:latest
ARG user=zhangsan
ARG password=123456
RUN echo $user $password > a.txt
#构建镜像且不传递任何参数:
[root@sqm-docker01 dfs]# docker build -t arg:v2 .
#运行容器,参数传递成功
[root@sqm-docker01 dfs]# docker run -it --rm arg:v2 /bin/bash
root@b52fa70086de:/# cat a.txt 
zhangsan 123456

#多阶段构建中每个阶段都必须包含arg指令:

FROM busybox
ARG user
RUN ./run/setup $user

FROM busybox
ARG user
RUN ./run/other $user

#ENV变量会覆盖同名的ARG变量:

[root@sqm-docker01 dfs]# cat Dockerfile 
FROM nginx:latest
ARG user=zhangsan
ENV user lisi
RUN echo $user > a.txt
[root@sqm-docker01 dfs]# docker build -t arg:v3 .
[root@sqm-docker01 dfs]# docker run -it --rm arg:v3 /bin/bash
root@a2aefd05efee:/# cat a.txt 
lisi

#ARG和ENV指令结合使用:(一般这种用法,不常用)

[root@sqm-docker01 dfs]# cat Dockerfile 
FROM centos
ARG var
ENV user=${var}
RUN echo $user > a.txt
[root@sqm-docker01 dfs]# docker build --build-arg var=zhangsan -t arg:v4 .
[root@sqm-docker01 dfs]# docker run -it --rm arg:v4 /bin/bash
[root@26bf8c139a3f /]# cat a.txt 
zhangsan

8,COPY--复制文件或目录‘’
格式:

COPY [--chown=<user>:<group>] <源路径>... <目标路径>
COPY [--chown=<user>:<group>] ["<源路径1>",... "<目标路径>"]

和RUN指令一样,也有两种格式,前者在shell终端运行; 后者则使用exec执行。

<源路径>为宿主机的上的路径,可以是多个,甚至可以是通配符,其通配符规则要满足 Go 的 filepath.Match 规则,如:

COPY hom* /mydir/
COPY hom?.txt /mydir/

注意:<源路径>必须是在build上下文目录中,也就是Dockerfile配置文件所在的目录及下面的递归目录,如果拷贝上下文之外目录下的文件(不是Dockerfile所在的目录),则无法拷贝。
build上下文的概念可参考:https://www.cnblogs.com/sparkdev/p/9573248.html

<目标路径>是容器中的路径,可以是绝对路径,也可以是相对于工作目录的相对路径(工作目录可以用 WORKDIR 指令来指定)。目标路径不需要事先创建,如果目录不存在会在复制文件前先行创建缺失目录。

此外,还需要注意一点,使用 COPY 指令,源文件的各种元数据都会保留。比如读、写、执行权限、文件变更时间等。可以使用如下命令指定:

COPY [--chown=:] ...
COPY [--chown=:] ["",... ""] (包含空格的路径需要这种形式)

–chown功能仅在用于构建Linux容器的Dockerfiles上受支持
例如:
COPY --chown=nginx:nginx files* /somedir/

举例:

[root@sqm-docker01 dfs]# echo "hello world" > index.html
[root@sqm-docker01 dfs]# echo aaaaaa > a.txt
[root@sqm-docker01 dfs]# cat Dockerfile 
FROM nginx:latest
COPY ./index.html /usr/share/nginx/html/
COPY ./a.txt /test/
[root@sqm-docker01 dfs]# docker run --rm  nginx:v1  /bin/bash
root@8a1ee4925b43:/# cat /usr/share/nginx/html/index.html 
hello world
#即使目标目录不存在,会自己事先创建
root@8a1ee4925b43:/# cat /test/a.txt   
aaaaaa

##拷贝整个目录:
格式:COPY src WORKDIR/src
例如:

[root@sqm-docker01 dfs]# cat test/a.txt 
addddddddddddaaaaa
[root@sqm-docker01 dfs]# cat Dockerfile 
FROM nginx:latest
COPY ./test  /usr/share/nginx/test/  
#必须要在目标路径下指定要拷贝的目录,不然只拷贝源目录中的文件,而不拷贝目录

#进入容器查看整个目录及其目录下的文件
[root@sqm-docker01 dfs]# docker run --rm nginx:v2 /bin/bash
root@4eae96cbe364:/# cd /usr/share/nginx/
root@4eae96cbe364:/usr/share/nginx# ls
html  test
root@4eae96cbe364:/usr/share/nginx# cat test/a.txt 
addddddddddddaaaaa

9,ADD--更高级的复制文件/目录
格式为:

ADD [--chown=<user>:<group>] <源路径>... <目标路径>
ADD [--chown=<user>:<group>] ["<源路径1>",... "<目标路径>"]

ADD指令和COPY 的格式和性质基本一致。但是在 COPY`基础上增加了一些功能:可以是一个URL(通过URL下载下来自动设置权限600);还可以是一个tar文件(自动解压为目录)。

举例:

[root@sqm-docker01 dfs]# cat Dockerfile 
FROM centos
ADD nginx-1.8.0.tar.gz  /usr/src
[root@sqm-docker01 dfs]# docker run --rm  nginx:v3 /bin/bash
[root@0c8d6789aa4c /]# cd /usr/src/
[root@0c8d6789aa4c src]# ls   
debug  kernels  nginx-1.8.0     #自动解压tar包
[root@0c8d6789aa4c src]# ls nginx-1.8.0/
CHANGES  CHANGES.ru  LICENSE  README  auto  conf  configure  contrib  html  man  src

#通过url下载链接文件放到目标路径下:

[root@sqm-docker01 dfs]# cat Dockerfile 
FROM centos
ADD http://nginx.org/download/nginx-1.9.0.tar.gz /

[root@sqm-docker01 dfs]# docker run --rm  nginx:v4 /bin/bash
[root@b9d978e3a333 /]# ls -lh nginx-1.9.0.tar.gz 
-rw------- 1 root root 835K Apr 28  2015 nginx-1.9.0.tar.gz
#默认权限600

#与COPY的区别:
用于与COPY类似,不同的是COPY的只能是本地文件/目录,如果src是归档文件(tar,zip,tgz,xz等),使用ADD命令文件会被自动解压到dest。

因此在COPY和ADD指令中选择的时候,可以遵循这样的原则,所有的文件复制均使用COPY,仅在需要自动解压缩的场合使用 ADD。

10,ENTRYPOINT--入口点
两种格式:

ENTRYPOINT ["executable", "param1", "param2"](exec格式)
ENTRYPOINT command param1 param2(shell中执行)。

ENTRYPOINT 的 目的和CMD一样,都是在指定容器启动程序及参数。ENTRYPOINT 在运行时也可以替代,不过比CMD要略显繁琐,需要通过docekr run的参数 --entrypoint 来指定。

#与CMD比较说明:
1)相同点:只能写一条,如果写多条,那么只有最后一条生效。
容器启动时才运行,运行时机相同。
2)不同点: ENTRYPOINT不会被运行的命令覆盖,而CMD则会被覆盖。
例子:

[root@sqm-docker01 dfs]# cat Dockerfile 
FROM centos:latest
ENTRYPOINT echo hello world
ENTRYPOINT echo hello china
#与CMD一样,只有最后一条指令生效
[root@sqm-docker01 dfs]# docker run --rm  en:v1 
hello china
#不同的是不会被运行时的命令覆盖
[root@sqm-docker01 dfs]# docker run ---rm  en:v1 echo "test"
hello china
#除非使用--entrypoint参数
[root@sqm-docker01 dfs]# docker run --rm --entrypoint hostname  en:v1    
fa667d019ce5
#这里使用hostname命令将hello china覆盖了

11,ENTRYPOINT与CMD指令结合使用
#如果我们在Dockerfile中同时写了ENTRYPOINT和CMD,那么CMD指定的内容就会作为ENTRYPOINT的参数。
举例如下:

[root@sqm-docker01 dfs]# cat Dockerfile 
FROM centos:latest
ENTRYPOINT ["/usr/bin/ping","baidu.com","-c"]
CMD ["4"]
#运行一个容器:
[root@sqm-docker01 dfs]# docker run --rm en:v2
PING baidu.com (39.156.69.79): 56 data bytes
64 bytes from 39.156.69.79: seq=0 ttl=127 time=72.931 ms
64 bytes from 39.156.69.79: seq=1 ttl=127 time=62.366 ms
64 bytes from 39.156.69.79: seq=2 ttl=127 time=58.875 ms
64 bytes from 39.156.69.79: seq=3 ttl=127 time=50.662 ms

--- baidu.com ping statistics ---
4 packets transmitted, 4 packets received, 0% packet loss
round-trip min/avg/max = 50.662/61.208/72.931 ms

此时容器中运行的命令为:ping baidu.com -c 4。

#ENTRYPOINT中的参数始终会被使用,而CMD的额外参数可以在容器启动时动态替换掉,例子如下:

[root@sqm-docker01 dfs]# cat Dockerfile 
FROM centos:latest
ENTRYPOINT ["/usr/bin/ping","baidu.com","-c"]
CMD ["4"]
[root@sqm-docker01 dfs]# docker run --rm en:v2 2
PING baidu.com (39.156.69.79) 56(84) bytes of data.
64 bytes from 39.156.69.79 (39.156.69.79): icmp_seq=1 ttl=127 time=80.4 ms
64 bytes from 39.156.69.79 (39.156.69.79): icmp_seq=2 ttl=127 time=61.5 ms

--- baidu.com ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 3ms
rtt min/avg/max/mdev = 61.532/70.945/80.358/9.413 ms

此时容器运行的命为:ping baidu.com -c 2。

#这两种指令结合起来使用,有什么好处呢?我们通过以场景来理解:(例子和上边例子大同小异)

//假设我们需要一个得知自己当前公网IP的镜像,可以那么先用CMD来实现:

[root@sqm-docker01 dfs]# cat Dockerfile 
FROM centos:latest
RUN yum  -y install curl
CMD ["/usr/bin/curl","-s","https://ip.cn"]
#构建镜像并运行容器:
[root@sqm-docker01 dfs]# docker build -t myip:v1 .
[root@sqm-docker01 dfs]# docker run --rm myip:v1 
{"ip": "117.136.60.216", "country": "江西省", "city": "移动"}

看起来我们是可以把它当作命令使用了,不过命令总有参数,如果我们希望显示HTTP头信息,就需要加上-i参数。那么我们可以直接加-i参数给docker run myip么?

[root@sqm-docker01 dfs]# docker run --rm myip:v1 -i
docker: Error response from daemon: OCI runtime create failed: container_linux.go:348: starting container process caused "exec: \"-i\": executable file not found in $PATH": unknown.

我们可以看到替换文件找不到的报错,executable file not found,之前我们说过,跟在上面名字后面的是command,运行时会替换为CMD的值。因此这里的-i替换了原来的CMD,而不是添加在原来的curl -s https://ip.cn 后面。-i根本不是命令,所以自然找不到。

//为了有很好的解决这个问题,我们将CMD和ENTRYPOINT结合使用:

[root@sqm-docker01 dfs]# cat Dockerfile 
FROM centos:latest
RUN yum  -y install curl
ENTRYPOINT ["/usr/bin/curl","-s","https://ip.cn"]
CMD ["-i"]
#构建并运行容器:
[root@sqm-docker01 dfs]# docker build -t myip:v2 .
[root@sqm-docker01 dfs]# docker run --rm myip:v2 
HTTP/2 200 
date: Wed, 19 Feb 2020 07:10:48 GMT
content-type: application/json; charset=UTF-8
set-cookie: __cfduid=d1903fe7e93a885c6ae4890572cda42161582096248; expires=Fri, 20-Mar-20 07:10:48 GMT; path=/; domain=.ip.cn; HttpOnly; SameSite=Lax
cf-cache-status: DYNAMIC
expect-ct: max-age=604800, report-uri="https://report-uri.cloudflare.com/cdn-cgi/beacon/expect-ct"
alt-svc: h3-25=":443"; ma=86400, h3-24=":443"; ma=86400, h3-23=":443"; ma=86400
server: cloudflare
cf-ray: 56766c505d6fb22e-HKG

{"ip": "117.136.60.216", "country": "江西省", "city": "移动"}

可以看到成功了,这是因为当存在ENTRYPOINT后,CMD的内容将会传递给ENTRYPOINT使用,从而达到了我们预期的效果。

##总结几条规律:

  • 如果 ENTRYPOINT 使用了 shell 模式,CMD 指令会被忽略。
  • 如果 ENTRYPOINT 使用了 exec 模式,CMD 指定的内容被追加为 ENTRYPOINT 指定命令的参数。
  • 如果 ENTRYPOINT 使用了 exec 模式,CMD 也应该使用 exec 模式

12,VOLUME--定义匿名卷
格式为:

VOLUME ["<路径1>", "<路径2>"...]
VOLUME <路径>

在Dockerfile中,我们可以预先指定某些目录挂载为匿名卷,这样在运行时如果用户不指定挂载,其应用也可以正常运行,不会向容器存储层写入大量数据。

VOLUME /data

这里的/data 目录就会在运行时自动挂载为匿名卷,任何向/data 中写入的信息都不会记录进容器存储层,从而保证了容器存储层的无状态化。当然,运行容器时可以覆盖这个挂载设置。

docker run -d -v mydata:/data xxxx

在上边命令中,就使用了mydata 这个命名卷挂载到了/data 这个位置,替代了Dockerfile 中定义的匿名卷的挂载配置。

#注意:从该指令的支持的格式就可以知道,VOLUME只支持docker manager volume的挂载方式,而不支持bind mount的方式。

docker manager volume:不需要指定源文件,只需要指定mount point。把容器里面的目录映射到了本地(宿主机)。

举例如下:

[root@sqm-docker01 dfs]# cat Dockerfile 
FROM nginx:latest
VOLUME /usr/share/nginx/html/
#构建并运行容器:
[root@sqm-docker01 dfs]# docker build -t volume:v1 .
[root@sqm-docker01 dfs]# docker run -itd --name volume volume:v1 
5d963fb3b51ae9ddcc3b55382e289e0447235676f8893900a440f5f9500f035e

我们通过docker inspect查看通过该dockerfile创建的大量生成的容器,可以看到挂载点的信息:

[root@sqm-docker01 dfs]# docker inspect volume 
        "Mounts": [
            {
                "Type": "volume",
                "Name": "190c5a22df09462a9f5fd54209b8bc5ad06fc9382f9c8b9c665c734e4bcf95e0",
                "Source": "/var/lib/docker/volumes/190c5a22df09462a9f5fd54209b8bc5ad06fc9382f9c8b9c665c734e4bcf95e0/_data",
                "Destination": "/usr/share/nginx/html",
                "Driver": "local",
                "Mode": "",
                "RW": true,
                "Propagation": ""
            }
        ],

从上面的信息可以看出默认挂载到本地的源路径为“/var/lib/docker/volumes/190c5a22df09462a9f5fd54209b8bc5ad06fc9382f9c8b9c665c734e4bcf95e0/_data” ,而目标路径(容器内的路径)为自定义的"/usr/share/nginx/html"。

13,WORKDIR--指定工作目录
格式为:

WORKDIR <工作目录路径>

设置工作目录,对RUN,CMD,ENTRYPOINT,COPY,ADD生效。如果目录不存在,则会帮你创建,也可以设置多次,如:

WORKDIR /a
WORKDIR b
WORKDIR c
RUN pwd
#则最终的路径为/a/b/c。

##WORKDIR也可以解析环境变量,如:

ENV DIRPATH /usr/src
WORKDIR $DIRPATH
RUN pwd
#pwd的执行结果是/usr/src。

注意:千万不要把dockerfile等同于shell脚本来书写,例如:

RUN cd /app
RUN echo "hello" > world.txt

将这个dockerfile 进行构建镜像运行后,会发现找不到/app/world.txt 文件,或者其内容不是 hello。原因其实很简单,在dockerfile中这两行RUN命令的执行环境不同,是两个完全不同的容器。这就是对 Dockerfile 构建分层存储的概念不了解所导致的错误。
如果需要改变以后各层的工作目录的位置,那么应该使用 WORKDIR 指令。

14,USER--指定当前用户

格式: USER <用户名>[:<用户组>]

USER指令和WORKDIR相似,都是改变环境状态并影响以后的层。
WORKDIR是改变工作目录,USER是改变之后层的执行RUN,CMD,以及ENTRYPOINT 这类命令的身份。
当然,和WORKDIR一样,USER只是帮助你切换到指定用户而已,这个用户必须是事先建立好的,否则无法切换:

RUN groupadd -r redis && useradd -r -g redis redis
USER redis
RUN [ "redis-server" ]

如果以root执行脚本,在执行期间希望改变身份,比如希望以某个已经建立好的用户来运行某个服务进程,不要使用su或者sudo,这些都需要比较麻烦的配置,而且在TTY缺失的环境下经常出错。一般使用 gosu。
举例如下:

# 建立 redis 用户,并使用 gosu 换另一个用户执行命令
RUN groupadd -r redis && useradd -r -g redis redis
# 下载 gosu
RUN wget -O /usr/local/bin/gosu "https://github.com/tianon/gosu/releases/download/1.7/gosu-amd64" \
    && chmod +x /usr/local/bin/gosu \
    && gosu nobody true
# 设置 CMD,并以另外的用户(redis)执行
CMD [ "exec", "gosu", "redis", "redis-server" ]

15,ONBUILD--为镜像添加触发器

格式:ONBUILD <其它指令>

ONBUILD 是一个特殊的指令,它后面跟的是其他指令,比如RUN,COPY等,而这些指令,在当前镜像构建时并不会被执行。
只有当以当前镜像为基础镜像,去构建下一级镜像的时候才会被执行。
Dockerfile中的其他指令都是为了定制当前镜像而准备的,唯有ONBUILD 是为了帮助别人定制自己而准备的。
举例如下:
//编写一个Dockerfile文件,内容如下:

[root@sqm-docker01 dfs]# cat Dockerfile 
FROM nginx:latest
ONBUILD COPY ./index.html /   #拷贝文件到容器内的 / 下

//利用上面的dockerfile文件构建镜像:
[root@sqm-docker01 dfs]# docker build -t image1 .

//利用image1镜像创建容器:

[root@sqm-docker01 dfs]# docker run --rm -it image1 /bin/bash
root@db8a068d9f30:/# ls    
bin   dev  home  lib64  mnt  proc  run   srv  tmp  var
boot  etc  lib   media  opt  root  sbin  sys  usr

我们发现以image1镜像运行的容器/目录下并没有index.html文件,这说明ONBUILD指定的指令并不会在自己的镜像构建中执行。

//再编写一个新的Dockerfile文件,内容如下:

[root@sqm-docker01 dfs]# cat Dockerfile 
FROM image1  #使用的基础镜像是上面构建的镜像image1
CMD echo hello world

//利用上面的dockerfile文件构建镜像:

[root@sqm-docker01 dfs]# docker build -t image2 .
Sending build context to Docker daemon  102.6MB
Step 1/2 : FROM image1
# Executing 1 build trigger
 ---> 796e32308d29
Step 2/2 : CMD echo hello world
 ---> Running in 5c16f913f5e9
Removing intermediate container 5c16f913f5e9
 ---> 4c0a45374727
Successfully built 4c0a45374727
Successfully tagged image2:latest

//利用image2镜像创建容器:

[root@sqm-docker01 dfs]# docker run -it --rm image2 /bin/bash
root@3e3c1d0fe3f6:/# ls
bin   dev  home        lib    media  opt   root  sbin  sys  usr
boot  etc  index.html  lib64  mnt    proc  run   srv   tmp  var
root@3e3c1d0fe3f6:/# cat index.html 
hello world

我们发现以image2镜像运行的容器根目录下有index.html文件,说明触发器执行了 COPY ./index.html / 指令。

镜像缓存特性:

1)Docker 会缓存已有镜像的镜像层,构建新镜像时,如果某镜像层已经存在,就直接使用,无需重新创建,如下所示:
//创建一个新的dockerfile

[root@sqm-docker01 dfs]# cat Dockerfile 
FROM centos
RUN yum -y install vim
//构建一个镜像
[root@sqm-docker01 dfs]# docker build -t a:v1 .

#往刚刚创建的dockerfile中添加一点新内容(在镜像中添加一条COPY指令):

[root@sqm-docker01 dfs]# cat Dockerfile 
FROM centos
RUN yum -y install vim
COPY testfile /

#以同一个dockefile重新构建镜像:

[root@sqm-docker01 dfs]# touch testfile
[root@sqm-docker01 dfs]# docker build -t a:v2 .
Sending build context to Docker daemon  102.6MB
Step 1/3 : FROM centos
 ---> 0f3e07c0138f
Step 2/3 : RUN yum -y install vim
 ---> Using cache
 ---> 6467d4675159
Step 3/3 : COPY testfile /
 ---> 6b92fe05882f
Successfully built 6b92fe05882f
Successfully tagged a:v2

(1)确保testfile文件已存在
(2)Using cache,重点在这里:之前已经运行过相同的RUN指令,这次直接使用缓存中的镜像层6467d4675159。
(3)执行COPY指令。
其过程是启动临时容器,复制testfile,提交新的镜像层6b92fe05882f,删除临时容器。

如果我们希望在构建镜像时不使用缓存,可以在docker build命令中加上--no-cache参数。

2)Dockerfile中每一个指令都会创建一个镜像层,上层是依赖于下层的。无论什么时候,只要某一层发生变化,其上面所有层的缓存都会失效。
也就是说,如果我们改变 Dockerfile 指令的执行顺序,或者修改或添加指令,都会使缓存失效。
举例说明,比如交换上面RUN和COPY指令的顺序:

[root@sqm-docker01 dfs]# cat Dockerfile 
FROM centos
COPY testfile /
RUN yum -y install vim

虽然在逻辑上这种改动对镜像的内容没有影响,但由于分层的结构特性,Docker 必须重建受影响的镜像层。

//重新构建镜像:

[root@sqm-docker01 dfs]# docker build -t a:v3 .
Sending build context to Docker daemon  102.6MB
Step 1/3 : FROM centos
 ---> 0f3e07c0138f
Step 2/3 : COPY testfile /
 ---> 97e725434a6b
Step 3/3 : RUN yum -y install vim
 ---> Running in 5f30ff393047
......
Removing intermediate container 5f30ff393047
 ---> 90ceae8b3638
Successfully built 90ceae8b3638
Successfully tagged a:v3

从上面的输出可以看到生成了新的镜像层97e725434a6b,缓存已经失效。

除了构建时使用缓存,Docker 在下载镜像时也会使用。例如我们下载 httpd 镜像:

[root@sqm-docker01 dfs]#docker pull httpd
Using default tag: latest
latest: Pulling from library/httpd
f17d81b4b692: Already exists 
06fe09255c64: Already exists 
0baf8127507d: Already exists 
07b9730387a3: Already exists 
6dbdee9d6fa5: Already exists 
Digest: sha256:76954e59f23aa9845ed81146ef3cad4a78f5eb3daab9625874ebca0e416367e2
Status: Image is up to date for httpd:latest

docker pull 命令输出显示第一层(base 镜像)已经存在,不需要下载。

简单了解镜像分层概念
Dockerfile中每行代码都会产生一个新的分层,一个镜像不能超过127层,每个层都会产生一个单独的id。
在已经存在image中的层,都是只读的。当一个image被运行成为一个container的时候,这个层是可读可写的,需要操作镜像层中的文件时,都是通过容器层复制一份,然后在进行操作,这个机制是copy on write(写时复制)。

猜你喜欢

转载自blog.51cto.com/13972012/2472365