Dockerfile语法及构建简单镜像

2022-11-21 19:58:52 浏览数 (1)

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

0 人点赞