Dockerfile语法及构建简单镜像
前面使用过docker commit
去构建镜像
Docker并不建议用户通过这种方式构建镜像。原因如下:
这是一种手工创建镜像的方式,容易出错,效率低且可重复性弱。比如要在 debian base 镜像中也加入 vi,还得重复前面的所有步骤。 更重要的:使用者并不知道镜像是如何创建出来的,里面是否有恶意程序。也就是说无法对镜像进行审计,存在安全隐患。 既然 docker commit 不是推荐的方法,我们干嘛还要花时间学习呢?
原因是:即便是用 Dockerfile(推荐方法)构建镜像,底层也 docker commit 一层一层构建新镜像的。学习 docker commit 能够帮助我们更加深入地理解构建过程和镜像的分层结构。
准备构建镜像
需要创建一个Dockerfile文件,文件名必须是这个
代码语言:javascript复制[root@localhost ~]# vim Dockerfile
# 添加
FROM centos
RUN yum -y install httpd
RUN yum -y install net-tools
RUN yum -y install elinks
CMD ["/bin/bash"]
构建镜像
使用docker build进行镜像的构建,最后需要指定Dockerfile文件所在路径
代码语言:javascript复制[root@localhost ~]# docker build -t chai/centos-http-net /root
Successfully built 09266c896243
Successfully tagged chai/centos-http-net:latest
看到最后输出两条Successfully则构建成功。
它会根据文件中写的内容,使用centos镜像实例化一个容器,进入容器中执行三个yum命令
查看已经构建好的镜像
代码语言:javascript复制[root@localhost ~]# docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
chai/centos-http-net latest 09266c896243 10 seconds ago 581MB
使用新的镜像实例化容器
代码语言:javascript复制[root@localhost ~]# docker run -it --rm --name test chai/centos-http-net /bin/bash
[root@50da58a03736 /]# ifconfig # 命令可以运行即成功
镜像构建过程
在构建命令执行时输出的一大堆信息中,是执行Dockerfile中的每一行,最关键的几行信息如下
代码语言:javascript复制Step 1/5 : FROM centos # 调用centos
---> 5e35e350aded # centos镜像id
Step 2/5 : RUN yum install httpd -y
---> Running in a16ddf07c140 # 运行一个临时容器来执行install httpd
Removing intermediate container a16ddf07c140 # 完成后删除临时的容器id
---> b51207823459 # 生成一个镜像
Step 3/5 : RUN yum install net-tools -y
---> Running in 459c8823018a # 运行一个临时容器执行install net-tools
Removing intermediate container 459c8823018a # 完成后删除临时容器id
---> 5b6c30a532d4 # 再生成一个镜像
Step 4/5 : RUN yum install elinks -y
---> Running in a2cb490f9b2f # 运行一个临时容器执行install elinks
Removing intermediate container a2cb490f9b2f # 完成后删除临时容器id
---> 24ba4735814b # 生成一个镜像
Step 5/5 : CMD ["/bin/bash"]
---> Running in 792333c88ba8 # 运行临时容器,执行/bin/bash
Removing intermediate container 792333c88ba8 # 完成后删除临时容器id
---> 09266c896243 # 生成镜像
Successfully built 09266c896243 # 最终成功后的镜像id就是最后生成的镜像id
每一步生成一个镜像,都属于一个docker commit的执行结果
在这个过程中一共生成了三个镜像层,都会被存储在graph中,包括层与层之间的关系,查看docker images中生成的镜像id是否为最后生成的镜像id,FROM和CMD都不算做镜像层
代码语言:javascript复制[root@localhost ~]# docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
chai/centos-http-net latest 09266c896243 10 seconds ago 581MB
通过docker history也可以看到简单的构建过程,这几个过程的size容量加起来也就是最终生成镜像的大小,也可将这里的镜像id和上面过程中的id进行对比,我们所看到的是三个yum就是形成的三个镜像层
代码语言:javascript复制[root@localhost ~]# docker history chai/centos-http-net:latest
IMAGE CREATED CREATED BY SIZE COMMENT
09266c896243 17 minutes ago /bin/sh -c #(nop) CMD ["/bin/bash"] 0B
24ba4735814b 17 minutes ago /bin/sh -c yum install elinks -y 121MB
5b6c30a532d4 18 minutes ago /bin/sh -c yum install net-tools -y 112MB
b51207823459 18 minutes ago /bin/sh -c yum install httpd -y 145MB
5e35e350aded 4 months ago /bin/sh -c #(nop) CMD ["/bin/bash"] 0B
<missing> 4 months ago /bin/sh -c #(nop) LABEL org.label-schema.sc… 0B
<missing> 4 months ago /bin/sh -c #(nop) ADD file:45a381049c52b5664… 203MB
镜像的缓存特性
之前文档中说到,nginx和httpd两个镜像都是基于debian系统制作的镜像,所以会使用相同的一部分镜像层去安装,而这个镜像被docker所共享,只需要下载一次即可
还是重新下载这两个镜像看一下是怎么进行使用
下载nginx镜像
代码语言:javascript复制[root@localhost ~]# docker pull nginx
Using default tag: latest
latest: Pulling from library/nginx
68ced04f60ab: Pull complete
28252775b295: Pull complete
a616aa3b0bf2: Pull complete
下载httpd镜像
代码语言:javascript复制[root@localhost ~]# docker pull httpd
Using default tag: latest
latest: Pulling from library/httpd
68ced04f60ab: Already exists # 已经存在
35d35f1e0dc9: Pull complete
8a918bf0ae55: Pull complete
d7b9f2dbc195: Pull complete
d56c468bde81: Pull complete
仔细观察下载的内容中,有两个id是一样的68ced04f60ab,在下载httpd的时候,会报出已经存在,因为在下载nginx时已经下载过了,这是因为这两个程序都是基于debian操作系统制作的镜像,这里就将这个id的镜像共享使用了,也就时使用了同一个镜像的缓存
自己构建的镜像也是具有这个缓存的特性的,在前面构建了一个镜像chai/centos-http-net,那我们根据构建这个镜像的Dockerfile文件的基础上进行一点点小的修改
效仿hello-world最小镜像的方法,也就是构建一个文档的镜像
代码语言:javascript复制[root@localhost ~]# vim testfile
# 添加内容
This is a FeiYi's file
然后将这个文件COPY到Dockerfile文件中,在之前的Dockerfile进行修改
代码语言:javascript复制[root@localhost ~]# vim Dockerfile
FROM centos
RUN yum -y install httpd
RUN yum -y install net-tools
RUN yum -y install elinks
COPY testfile / # 将物理机当前目录的testfile文件复制到镜像中的根目录
CMD ["/bin/bash"]
将以上Dockerfile进行构建镜像,发现构建的很快,也没有很多的输出信息,而且除了新加的复制文件以外,其他的三个RUN镜像层,都多了一行Using cache,这是因为他们形成的镜像层已经作为缓存被构建的这个镜像使用
代码语言:javascript复制[root@localhost ~]# docker build -t test /root
Sending build context to Docker daemon 4.334MB
Step 1/6 : FROM centos
---> 5e35e350aded
Step 2/6 : RUN yum -y install httpd
---> Using cache
---> f01998ebc3ff
Step 3/6 : RUN yum -y install net-tools
---> Using cache
---> a3c39d101d8b
Step 4/6 : RUN yum -y install elinks
---> Using cache
---> 7c705388a610
Step 5/6 : COPY testfile /
---> 2111837dd86c
Step 6/6 : CMD ["/bin/bash"]
---> Running in 36782884ce16
Removing intermediate container 36782884ce16
---> 7b529720bf53
Successfully built 7b529720bf53
Successfully tagged test:latest
镜像层还有一个特点
如果改变了Dockerfile中镜像层的顺序,
代码语言:javascript复制[root@localhost ~]# vim Dockerfile
FROM centos
RUN yum -y install httpd
COPY testfile /
RUN yum -y install net-tools
RUN yum -y install elinks
CMD ["/bin/bash"]
然后进行构建,就会发现所有的内容全部重新构建
代码语言:javascript复制[root@localhost ~]# docker build -t test2 /root
这个特点就时层与层之间的关系,他们第一次构建时的顺序可以理解为他们之间的层级关系,如果执行顺序不一样,那么他们之间的层级关系也不一样,就不会去使用缓存构建。他们之间的关系被存储在graphDB中,如果读取不到相同的关系,是不会去使用缓存的。
–no-cache
–no-cache可以指定你构建镜像时,不适用已经存在的镜像层,也就是不使用缓存的特性
使用该参数重新构建刚才的Dockerfile
代码语言:javascript复制[root@localhost ~]# docker build -t test3 /root --no-cache
这样就会将所有的Dockerfile中的镜像层重新下载构建,而不是使用缓存。
Dockerfile文件排错方法
当个构建镜像时Dockerfile中报错,先来制作一个错误的Dockerfile
代码语言:javascript复制[root@localhost ~]# vim Dockerfile
FROM busybox
RUN touch tmpfile
RUN /bin/bash -c echo "continue to build..."
COPY testfile /
使用这个Dockerfile去构建镜像
代码语言:javascript复制[root@localhost ~]# docker build -t testerror /root
Sending build context to Docker daemon 4.336MB
Step 1/4 : FROM busybox
---> 83aa35aa1c79
Step 2/4 : RUN touch tmpfile
---> Running in 41a8dad29cd6
Removing intermediate container 41a8dad29cd6
---> 8cd5c9a720bb
Step 3/4 : RUN /bin/bash -c echo "continue to build..."
---> Running in bc1849fa8144
/bin/sh: /bin/bash: not found
The command '/bin/sh -c /bin/bash -c echo "continue to build..."' returned a non-zero code: 127
发现构建镜像过程中出现了报错/bin/sh: /bin/bash: not found
可以看到报错信息是从第三步才开始的,说明前两步是没有问题的,可以通过进入前两步最后结束的镜像id中去查看错误,进入前两层的镜像id是一个正常的容器环境,将第三步无法执行的命令,在容器中运行,将会看到真正的错误是没有/bin/bash这个环境
代码语言:javascript复制[root@localhost ~]# docker run -it 8cd5c9a720bb
/ # /bin/bash -c echo "continue to build..."
sh: /bin/bash: not found
因为构建这个镜像使用的是busybox,它使用的环境是/bin/sh
修改后,重新构建成功
代码语言:javascript复制[root@localhost ~]# docker build -t testerror /root
Successfully built ae5870fff063
Successfully tagged testerror:latest
运行容器后,可以看到创建的tmpfile和复制testfile
代码语言:javascript复制[root@localhost ~]# docker run -it testerror
/ # ls
bin etc proc sys tmp usr
dev home root testfile tmpfile var
Dockerfile文件语法
常用构建镜像指令
代码语言:javascript复制FROM # 指定base镜像
MAINTAINER # 指定镜像作者,后面根任意字符串
COPY # 把文件从host复制到镜像内
COPY src dest
COPY ["src","dest"]
src:只能是文件
ADD # 用法和COPY一样,唯一不同时src可以是压缩包,表示解压缩到dest位置,src也可以是目录
ENV # 设置环境变量可以被接下来的镜像层引用,并且会加入到镜像中
ENV MY_VERSION 1.3
RUN yum -y install http-$MY_VERSION
# 当进入该镜像的容器中echo $MY_VERSION会输出1.3
EXPOSE # 指定容器中的进程监听的端口(接口),会在docker ps -a中的ports中显示
EXPOSE 80
VOLUME # 容器卷,后面会讲到,把host的路径mount到容器中
VOLUME /root/htdocs /usr/local/apahce2/htdocs
WORKDIR # 为后续的镜像层设置工作路径
# 如果不设置,Dockerfile文件中的每一条命令都会返回到初始状态
# 设置一次后,会一直在该路经执行之后的分层,需要WORKDIR /回到根目录
CMD # 启动容器后默认运行的命令,使用构建完成的镜像实例化为容器时,进入后默认执行的命令
# 这个命令会被docker run启动命令替代
# 如:docker -it --rm centos echo "hello"
# echo "hello"会替代CMD运行的命令
CMD ["nginx", "-g", "daemon off"] # 该镜像实例化后的容器,进入后运行nginx启动服务
ENTRYPOINT # 容器启动时运行的命令,不会被docker run的启动命令替代
RUN/CMD/ENTRYPOINT区别
在语法中说到CMD和ENTRYPOINT是容器启动后和容器启动时,运行的命令,RUN是构建镜像时运行的命令。三者的区别究竟在什么地方,通过一个例子来看
使用这三者来构建一个镜像
代码语言:javascript复制[root@localhost ~]# vim Dockerfile
# 添加
FROM centos
RUN yum -y install net-tools
CMD echo "hello chai"
ENTRYPOINT echo "hello mupei"
[root@localhost ~]# docker build -t chai /root
Successfully built 10dec59bba24
Successfully tagged chai:latest
构建完成后,运行镜像
代码语言:javascript复制[root@localhost ~]# docker run -it chai
hello mupei
很明显在构建构成中RUN已经完成了它的工作
RUN:执行命令并创建新的镜像层,主要用于安装软件包
而在运行镜像后,只输出了hello mupei,是ENTRYPOINT来执行的命令
这两个都算作是启动指令,也就是必须启动容器才会去执行的指令,一般用来启动运行程序使用
结论:当ENTRYPOINT和CMD同时存在时,ENTRYPOINT生效
ENTRYPOINT和CMD使用格式
shell和exec两种格式
shell格式
CMD command、ENTRYPOINT command
shell格式会始终调用一个shell程序去执行命令
通过一个例子来看
代码语言:javascript复制[root@localhost ~]# vim Dockerfile
FROM centos
RUN yum -y install net-tools
ENV name chai
ENTRYPOINT echo "hello $name"
[root@localhost ~]# docker build -t pei /root
Successfully built cf475e27d587
Successfully tagged pei:latest
[root@localhost ~]# docker run -it pei
hello chai
当指令执行时,shell会调用/bin/bash
exec格式
CMD [“命令”, “选项”, “参数”]、ENTRYPOINT [“命令”, “选项”, “参数”]
exec格式下无法去调用ENV定义的变量,如果非要让exec格式去读取变量的话,它的命令的位置就要使用一个shell环境。因为变量的读取就是使用shell去读取的。 如:ENTRYPOINT ["/bin/sh", “-c”, “echo hello,$变量名”]
再看一个例子
代码语言:javascript复制[root@localhost ~]# vim Dockerfile
FROM centos
RUN ["yum", "-y", "install", "net-tools"]
CMD ["/bin/echo", "hello chai"]
ENTRYPOINT ["/bin/echo", "hello world"]
[root@localhost ~]# docker build -t feiyi /root
Successfully built 52189c01eaf5
Successfully tagged feiyi:latest
运行容器后,只有ENTRYPOINT的命令是正常执行了,输出了我们需要的hello world,
ENTRYPOINT ["/bin/echo", “hello world”]:这一行中,/bin/echo是命令,hello world是执行的参数
而CMD中的/bin/echo和hello chai都做为结果输出,并没有被当做命令
代码语言:javascript复制[root@localhost ~]# docker run -it feiyi
hello world /bin/echo hello chai
结论:当使用exec格式时,ENTRYPOINT的第一个参数被识别为命令,CMD的参数按顺序变为ENTRYPOINT命令的参数
这个结论相当于Dockerfile文件中的以下两行=echo “hello world /bin/echo hello chai”
CMD ["/bin/echo", “hello chai”]
ENTRYPOINT ["/bin/echo", “hello world”]
即:
代码语言:javascript复制[root@localhost ~]# docker run -it feiyi
hello world /bin/echo hello chai
[root@localhost ~]# echo "hello world /bin/echo hello chai"
hello world /bin/echo hello chai