DockerFile编写以及指令

发布时间 2023-06-07 10:28:33作者: 春游去动物园

DockerFile编写以及指令

什么是 Dockerfile?(重点是构建镜像)

Dockerfile 是一个用来构建镜像的文本文件,文本内容包含了一条条构建镜像所需的指令和命令

使用 Dockerfile定制镜像

这里仅讲解如何运行 Dockerfile 文件来定制一个镜像,具体 Dockerfile 文件内指令详解,将在下一节中介绍,这里你只要知道构建的流程即可。

1、下面以定制一个 nginx 镜像(构建好的镜像内会有一个 /usr/share/nginx/html/index.html 文件)

在一个空目录下,新建一个名为Dockerfile的文件,并在文件中添加以下内容:

FROM nginx

RUN echo '这是一个本地构建的nginx镜像' > /usr/share/nginx/html/index.html

2.FROM 和 RUN 指令的作用

FROM:定制的镜像都是基于FROM的镜像,这里的nginx就是定制需要的基础镜像。后续的操作都是基于nginx。

RUN:用于执行后面跟着的命令。有以下两种格式:

shell格式

RUN <命令行命令>

<命令行命令> 等同于,在终端操作的 shell 命令。

exec格式:

RUN ["可执行文件", "参数1", "参数2"]

例如:RUN ["./test.php", "dev", "offline"] 等价RUN ./test.php dev offline

注意:Dockerfile 的指令每执行一次都会在 docker 上新建一层。所以过多无意义的层,会造成镜像膨胀过大。例如:

FROM centos
RUN yum -y install wget
RUN wget -O redis.tar.gz "http://download.redis.io/releases/redis-5.0.3.tar.gz"
RUN tar -xvf redis.tar.gz

以上执行会创建 3 层镜像。可简化为以下格式:

FROM centos
RUN yum -y install wget
&& wget -O redis.tar.gz "http://download.redis.io/releases/redis-5.0.3.tar.gz"
&& tar -xvf redis.tar.gz

如上,以 && 符号连接命令,这样执行后,只会创建 1 层镜像。

3.开始构建镜像

在Dockerfile文件的存放目录下,执行构建动作。

以下示例,通过目录下的 Dockerfile 构建一个 nginx:v3(镜像名称:镜像标签)。

:最后的 . 代表本次执行的上下文路径。

docker build -t nginx:v3 .

以上显示,说明已经构建成功。

4.上下文路径

上面,有提到指令最后一个 . 是上下文路径,那么什么是上下文路径呢?

docker build -t nginx:v3 .

上下文路径,是指 docker 在构建镜像,有时候想要使用到本机的文件(比如复制),docker build 命令得知这个路径后,会将路径下的所有内容打包。

解析:由于 docker 的运行模式是 C/S。我们本机是 C,docker 引擎是 S。实际的构建过程是在 docker 引擎下完成的,所以这个时候无法用到我们本机的文件。这就需要把我们本机的指定目录下的文件一起打包提供给 docker 引擎使用。

如果未说明最后一个参数,那么默认上下文路径就是 Dockerfile 所在的位置。

注意:上下文路径下不要放无用的文件,因为会一起打包发送给 docker 引擎,如果文件过多会造成过程缓慢。
我们在使用 docker build 命令去构建镜像时,往往会看到命令最后会有一个 . 号。

比如:

docker build -t xxx .
那么这里的 . 号代表什么意思呢?

以为是用来指定 Dockerfile 文件所在的位置的?

但其实 -f 参数才是用来指定 Dockerfile 的路径的,那么 . 号究竟是用来做什么的呢?

Docker 在运行时分为 Docker引擎(服务端守护进程) 以及 客户端工具,我们日常使用各种 docker 命令,其实就是在使用客户端工具与 Docker 引擎 进行交互。

那么当我们使用 docker build 命令来构建镜像时,这个构建过程其实是在 Docker引擎 中完成的,而不是在本机环境。

那么如果在 Dockerfile 中使用了一些 COPY 等指令来操作文件,如何让 Docker引擎 获取到这些文件呢?

这里就有了一个 镜像构建上下文 的概念,当构建的时候,由用户指定构建镜像的上下文路径,而 docker build 会将这个路径下所有的文件都打包上传给 Docker 引擎,引擎内将这些内容展开后,就能获取到所有指定上下文中的文件了。

比如说 dockerfile 中的 COPY ./package.json /project,其实拷贝的并不是本机目录下的 package.json 文件,而是 docker引擎 中展开的构建上下文中的文件,所以如果拷贝的文件超出了构建上下文的范围,Docker引擎 是找不到那些文件的。

所以 docker build 最后的 . 号,其实是在指定镜像构建过程中的上下文环境的目录。

理解了上面的这些概念,就更方便的去理解 .dockerignore 文件的作用了。

关于 .dockerignore 的使用参见另一篇博文:.dockerignore 文件的作用

当我们在 docker build 的过程中,首先会将指定的上下文目录打包传递给 docker引擎,而这个上下文目录中可能并不是所有的文件我们都会在 Dockerfile 中使用到,那么这个时候就可以在 .dockerignore 文件中指定在传递给 docker引擎 时需要忽略掉的文件或文件夹。
比如我们在前端项目中,node_modules 文件夹在构建镜像过程中如果用不到,但是又异常庞大,那么向 docker引擎 传递其实是并没有必要的(其实大家电脑性能都这么好,也不在乎这几秒钟了。。。只是举个例子,可以提升镜像构建速度),这个时候就可以将 node_modules 文件夹加入 .dockerignore 文件中。

指令详解

1.COPY

复制指令,从上下文目录中复制文件或者目录到容器里指定路径。

格式:

COPY [--chown=<user>:<group>] <源路径1>...  <目标路径>
COPY [--chown=<user>:<group>] ["<源路径1>",...  "<目标路径>"]
exec格式用法(推荐):
COPY ["<src>",... "<dest>"],推荐,特别适合路径中带有空格的情况

shell格式用法:
COPY <src>... <dest>

[--chown=:]:可选参数,用户改变复制到容器内文件的拥有者和属组。

<源路径>:源文件或者源目录,这里可以是通配符表达式,其通配符规则要满足 Go 的 filepath.Match 规则。例如:

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

<目标路径>:容器内的指定路径,该路径不用事先建好,路径不存在的话,会自动创建。

2.ADD

ADD 指令和 COPY 的使用格类似(同样需求下,官方推荐使用 COPY)。功能也类似,不同之处如下:

  • ADD 的优点:在执行 <源文件> 为 tar 压缩文件的话,压缩格式为 gzip, bzip2 以及 xz 的情况下,会自动复制并解压到 <目标路径>。
  • ADD 的缺点:在不解压的前提下,无法复制 tar 压缩文件。会令镜像构建缓存失效,从而可能会令镜像构建变得比较缓慢。具体是否使用,可以根据是否需要自动解压来决定。
ADD指令不仅能够将构建命令所在的主机本地的文件或目录,而且能够将远程URL所对应的文件或目录,作为资源复制到镜像文件系统。

所以,可以认为ADD是增强版的COPY,支持将远程URL的资源加入到镜像的文件系统。

exec格式用法(推荐):
ADD ["<src>",... "<dest>"],特别适合路径中带有空格的情况

shell格式用法:
ADD <src>... <dest>

ADD和COPY的区别

ADD

(1)
ADD命令相对于COPY命令,可以解压缩文件并把它们添加到镜像中的功能,如果我们有一个压缩文件包,并且需要把这个压缩包中的文件添加到镜像中。需不需要先解开压缩包然后执行 COPY 命令呢?当然不需要!我们可以通过 ADD 命令一次搞定:
FROM frolvlad/alpine-java:jre8-slim
MAINTAINER oas.cloud
ADD nickdir.tar.gz .
WORKDIR /usr/local/oas/


(2)
同时ADD还可以从 url 拷贝文件到镜像中,但官方不推荐这样使用,官方建议我们当需要从远程复制文件时,最好使用 curl 或 wget 命令来代替 ADD 命令。原因是,当使用 ADD 命令时,会创建更多的镜像层,当然镜像的 size 也会更大,代码如下:
ADD http://example.com/big.tar.xz /usr/src/things/
RUN tar -xJf /usr/src/things/big.tar.xz -C /usr/src/things
RUN make -C /usr/src/things all

如果使用下面的命令,不仅镜像的层数减少,而且镜像中也不包含 big.tar.xz 文件,代码如下:
RUN mkdir -p /usr/src/things \
    && curl -SL http://example.com/big.tar.xz \
    | tar -xJC /usr/src/things \
    && make -C /usr/src/things all
    
所以ADD命令官方推荐只有在解压缩文件并把它们添加到镜像中时才需要。

COPY

(1)
COPY命令用于将于Dockerfile所在目录中的文件在镜像构建阶段从宿主机拷贝到镜像中,对于文件而言可以直接将文件复制到镜像,代码如下:
FROM frolvlad/alpine-java:jre8-slim
MAINTAINER oas.cloud
ARG JAR_FILE
COPY ${JAR_FILE} /usr/local/oas/
WORKDIR /usr/local/oas/


(2)
对于目录而言,该命令只复制目录中的内容而不包含目录自身,代码如下:
FROM frolvlad/alpine-java:jre8-slim
MAINTAINER oas.cloud
COPY nickdir .
WORKDIR /usr/local/oas/

3.CMD

类似于RUN指令,用于运行程序,但二者运行的时间点不同:

  • CMD 在docker run 时运行。
  • RUN 是在 docker build。

作用:为启动的容器指定默认要运行的程序,程序运行结束,容器也就结束。CMD 指令指定的程序可被 docker run 命令行参数中指定要运行的程序所覆盖。

注意:如果 Dockerfile 中如果存在多个 CMD 指令,仅最后一个生效。

格式:

CMD <shell 命令> 
CMD ["<可执行文件或命令>","<param1>","<param2>",...] 
CMD ["<param1>","<param2>",...]  # 该写法是为 ENTRYPOINT 指令指定的程序提供默认参数

推荐使用第二种格式,执行过程比较明确。第一种格式实际上在运行的过程中也会自动转换成第二种格式运行,并且默认可执行文件是 sh。

4.ENTRYPOINT

类似于 CMD 指令,但其不会被 docker run 的命令行参数指定的指令所覆盖,而且这些命令行参数会被当作参数送给 ENTRYPOINT 指令指定的程序。

但是, 如果运行 docker run 时使用了 --entrypoint 选项,将覆盖 ENTRYPOINT 指令指定的程序。

优点:在执行 docker run 的时候可以指定 ENTRYPOINT 运行所需的参数。

注意:如果 Dockerfile 中如果存在多个 ENTRYPOINT 指令,仅最后一个生效。

格式:

ENTRYPOINT ["<executeable>","<param1>","<param2>",...]

可以搭配 CMD 命令使用:一般是变参才会使用 CMD ,这里的 CMD 等于是在给 ENTRYPOINT 传参,以下示例会提到。

示例:

假设已通过 Dockerfile 构建了 nginx:test 镜像:

FROM nginx

ENTRYPOINT ["nginx", "-c"] # 定参
CMD ["/etc/nginx/nginx.conf"] # 变参 

1、不传参运行

$ docker run  nginx:test

容器内会默认运行以下命令,启动主进程。

nginx -c /etc/nginx/nginx.conf

2、传参运行

$ docker run  nginx:test -c /etc/nginx/new.conf

容器内会默认运行以下命令,启动主进程(/etc/nginx/new.conf:假设容器内已有此文件)

nginx -c /etc/nginx/new.conf

5.ENV

设置环境变量,定义了环境变量,那么在后续的指令中,就可以使用这个环境变量。

格式:

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

以下示例设置 NODE_VERSION = 7.2.0 , 在后续的指令中可以通过 $NODE_VERSION 引用:

ENV NODE_VERSION 7.2.0

RUN curl -SLO "https://nodejs.org/dist/v$NODE_VERSION/node-v$NODE_VERSION-linux-x64.tar.xz" \
  && curl -SLO "https://nodejs.org/dist/v$NODE_VERSION/SHASUMS256.txt.asc"

6.ARG

构建参数,与 ENV 作用一致。不过作用域不一样。ARG 设置的环境变量仅对 Dockerfile 内有效,也就是说只有 docker build 的过程中有效,构建好的镜像内不存在此环境变量。

构建命令 docker build 中可以用 --build-arg <参数名>=<值> 来覆盖。

格式:

ARG <参数名>[=<默认值>]

7.VOLUME(可以简单理解为持久化,或者文件映射,共享文件或文件夹)

定义匿名数据卷。在启动容器时忘记挂载数据卷,会自动挂载到匿名卷。

作用:

  • 避免重要的数据,因容器重启而丢失,这是非常致命的。
  • 避免容器不断变大。

格式:

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

在启动容器 docker run 的时候,我们可以通过 -v 参数修改挂载点。

在Docker中,要想实现数据的持久化(所谓Docker的数据持久化即数据不随着Container的结束而结束),需要将数据从宿主机挂载到容器中。目前Docker提供了三种不同的方式将数据从宿主机挂载到容器中:
(1)volumes:Docker管理宿主机文件系统的一部分,默认位于 /var/lib/docker/volumes 目录中;(最常用的方式)

  由上图可以知道,目前所有Container的数据都保存在了这个目录下边,由于没有在创建时指定卷,所以Docker帮我们默认创建许多匿名(就上面这一堆很长ID的名字)卷。
  注意:如果volume是空的而container中的目录有内容,那么docker会将container目录中的内容拷贝到volume中,但是如果volume中已经有内容,则会将container中的目录覆盖。

7.1管理卷

docker volume create edc-nginx-vol // 创建一个自定义容器卷

docker volume ls // 查看所有容器卷

docker volume inspect edc-nginx-vol // 查看指定容器卷详情信息

例如,这里我们创建一个自定义的容器卷,名为"edc-nginx-vol":

7.2创建使用指定卷的容器

有了自定义容器卷,我们可以创建一个使用这个数据卷的容器,这里我们以nginx为例:

# docker run -d -it --name=edc-nginx -p 8800:80 -v edc-nginx-vol:/usr/share/nginx/html nginx

其中,-v代表挂载数据卷,这里使用自定数据卷edc-nginx-vol,并且将数据卷挂载到/usr/share/nginx/html (这个目录是yum安装nginx的默认网页目录)。如果没有通过-v指定,那么Docker会默认帮我们创建匿名数据卷进行映射和挂载。
创建好容器之后,我们可以进入容器里面看看:

这时我们新启动一个SSH连接到宿主机去到刚刚创建的数据卷里边看看:

可以看到,我们可以访问到容器里面的两个默认页面,由此可知,volume帮我们做的类似于一个软链接的功能。在容器里边的改动,我们可以在宿主机里感知,而在宿主机里面的改动,在容器里边可以感知到。这时,如果我们手动stop并且remove当前nginx容器,我们会发现容器卷里面的文件还在,并没有被删除掉。
由此可以验证,在数据卷里边的东西是可以持久化的。如果下次还需要创建一个nginx容器,那么还是复用当前数据卷里面的文件。
此外,我们还可以启动多个nginx容器实例,并且共享同一个数据卷,复用性和扩展性较强。

7.3清理卷

如果不再使用自定义数据卷了,那么可以手动清理掉:

docker stop edc-nginx // 暂停容器实例

docker rm edc-nginx // 移除容器实例

docker volume rm edc-nginx-vol // 删除自定义数据卷

8.EXPOSE

仅仅只是声明端口。

作用:

  • 帮助镜像使用者理解这个镜像服务的守护端口,以方便配置映射。
  • 在运行时使用随机端口映射时,也就是 docker run -P 时,会自动随机映射 EXPOSE 的端口。

格式:

EXPOSE <端口1> [<端口2>...]

8.1什么是EXPOSE指令?

EXPOSE是Dockerfile中的一条指令,它用于声明容器运行时所监听的网络端口。当其他人阅读该Dockerfile时,可以很清楚地了解该容器所需要打开哪些端口。EXPOSE并不会真正将宿主机上的端口暴露出来,它只是一个元数据,方便用户了解容器的网络设置。

8.2如何使用EXPOSE指令?

EXPOSE指令的语法格式如下:

EXPOSE <port> [<port>/<protocol>...]

其中,表示需要被监听的端口号,而[/…]则表示可选的协议类型,例如TCP或UDP等。需要注意的是,EXPOSE指令声明的端口必须与容器内应用程序所监听的端口一致,否则无法正常通信。

我们可以在Dockerfile中通过EXPOSE指令来声明容器应该监听哪些端口。例如:

FROM nginx:alpine

EXPOSE 80/tcp上述代码表示我们使用带有ALPINE操作系统的NGINX镜像,并声明容器应该监听80端口。

8.3EXPOSE指令与Docker端口映射

EXPOSE指令并不会自动将容器内部的端口映射到宿主机上,因此我们需要使用Docker的端口映射功能。在运行容器时,我们可以使用-Docker run命令的-p选项将容器的端口映射到宿主机上,如下所示:

docker run -p 宿主机端口:容器端口 image_name

例如,如果我们想将NGINX容器的80端口映射到宿主机的8080端口,则可以执行以下命令:

docker run -p 8080:80 nginx

通过上述命令,Docker就会将容器内部的80端口映射到了宿主机的8080端口上。

8.4Dockerfile中EXPOSE的作用

Dockerfile中的EXPOSE指令用于向Docker守护进程声明容器运行时需要监听的网络端口。它并不会自动将这些端口映射到主机上的任何端口,而是只是向用户以及后续的Dockerfile指令传达这个信息。

这个指令可以帮助其他开发人员或者管理员清楚的知道应用程序在容器内部所侦听的端口号。从而更好地配置和管理容器,使其更加高效。通常情况下,我们可以将使用EXPOSE指令来记录应用程序的端口,并将应用程序端口映射到主机端口的工作放在Docker run命令或Docker Compose文件中进行。

请注意,EXPOSE指令只生效于运行时,而非构建时。它不会自动打开这些端口,也不能代替 Docker run 中 -p 或 -P 参数的作用。如果要将容器内的端口映射到主机上,请在运行容器时,使用 Docker run 命令的 -p 或 -P 选项。

8.5不用EXPOSE有什么影响吗?

在 Dockerfile 中不使用 EXPOSE 指令不会有任何影响。容器内的应用程序依然可以监听和处理网络请求,只是默认情况下这些端口号对于外部网络是不可访问的。如果需要从外部网络访问容器应用程序提供的服务,那么必须在运行容器时,使用 Docker run 命令的 -p 或 -P 选项进行端口映射或使用其他工具(如Docker Compose)来实现端口映射。

因此,EXPOSE并不是必需的,但是它可以帮助其他开发人员或管理员了解容器内应用程序所监听的端口号,以更好地管理、配置容器,同时也可以作为一种文档形式,方便开发人员在编写 Dockerfile 的时候记录和查看应用程序的通信端口。

8.6总结

我们可以清楚地了解到EXPOSE指令的作用、语法格式以及与Docker端口映射的关系。在编写Dockerfile时,正确使用EXPOSE指令可以让其他人更好地了解容器的网络设置,避免出现运行时不必要的困扰。

9.WORKDIR

指定工作目录。用 WORKDIR 指定的工作目录,会在构建镜像的每一层中都存在。以后各层的当前目录就被改为指定的目录,如该目录不存在,WORKDIR 会帮你建立目录。

docker build 构建镜像过程中的,每一个 RUN 命令都是新建的一层。只有通过 WORKDIR 创建的目录才会一直存在。

格式:

WORKDIR <工作目录路径>

9.1WORKDIR 指定工作目录详细

格式为 WORKDIR <工作目录路径>。

使用 WORKDIR 指令可以来指定工作目录(或者称为当前目录),以后各层的当前目录就被改为指定的目录,如该目录不存在,WORKDIR 会帮你建立目录。

之前提到一些初学者常犯的错误是把 Dockerfile 等同于 Shell 脚本来书写,这种错误的理解还可能会导致出现下面这样的错误:

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

如果将这个 Dockerfile 进行构建镜像运行后,会发现找不到 /app/world.txt 文件,或者其内容不是 hello。原因其实很简单,在 Shell 中,连续两行是同一个进程执行环境,因此前一个命令修改的内存状态,会直接影响后一个命令;而在 Dockerfile 中,这两行 RUN 命令的执行环境根本不同,是两个完全不同的容器。这就是对 Dockerfile 构建分层存储的概念不了解所导致的错误。

之前说过每一个 RUN 都是启动一个容器、执行命令、然后提交存储层文件变更。第一层 RUN cd /app 的执行仅仅是当前进程的工作目录变更,一个内存上的变化而已,其结果不会造成任何文件变更。而到第二层的时候,启动的是一个全新的容器,跟第一层的容器更完全没关系,自然不可能继承前一层构建过程中的内存变化。

因此如果需要改变以后各层的工作目录的位置,那么应该使用 WORKDIR 指令。

WORKDIR /app

RUN echo "hello" > world.txt

如果你的 WORKDIR 指令使用的相对路径,那么所切换的路径与之前的 WORKDIR 有关:

WORKDIR /a
WORKDIR b
WORKDIR c

RUN pwd

pwd 输出的结果为 /a/b/c。

10.USER

用于指定执行后续命令的用户和用户组,这边只是切换后续命令执行的用户(用户和用户组必须提前已经存在)。

格式:

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

11.HEALTHCHECK

用于指定某个程序或者指令来监控 docker 容器服务的运行状态。

格式:

HEALTHCHECK [选项] CMD <命令>:设置检查容器健康状况的命令
HEALTHCHECK NONE:如果基础镜像有健康检查指令,使用这行可以屏蔽掉其健康检查指令

HEALTHCHECK [选项] CMD <命令> : 这边 CMD 后面跟随的命令使用,可以参考 CMD 的用法。

12.ONBUILD

用于延迟构建命令的执行。简单的说,就是 Dockerfile 里用 ONBUILD 指定的命令,在本次构建镜像的过程中不会执行(假设镜像为 test-build)。当有新的 Dockerfile 使用了之前构建的镜像 FROM test-build ,这时执行新镜像的 Dockerfile 构建时候,会执行 test-build 的 Dockerfile 里的 ONBUILD 指定的命令。

格式:

ONBUILD <其它指令>

13.LABEL

LABEL 指令用来给镜像添加一些元数据(metadata),以键值对的形式,语法格式如下:

LABEL <key>=<value> <key>=<value> <key>=<value> ...

比如我们可以添加镜像的作者:

LABEL org.opencontainers.image.authors="runoob"