学习如何使用Docker文件

121 阅读19分钟

在早期的Docker文章中,我们能够手动构建镜像并让它们作为容器运行。在本教程中,我们将探讨如何通过用代码构建镜像来更进一步。这就是Dockerfile发挥作用的地方。Dockerfile是描述如何组装Docker镜像的小程序。你可以使用docker build命令运行这些小程序。一旦docker build命令完成了对Dockerfile的运行,一个新的Docker镜像将被放入你电脑上的本地Docker注册表。一旦你有了一个合适的镜像,你就可以用docker容器运行命令来运行它。


什么是Dockerfile?

Dockerfile是创建镜像的配方,默认名称为大写字母D的Dockerfile。Dockerfile中的每条语句都是它自己的层,它们的顺序很重要,因为文件是自上而下运行的。

  • Dockerfile就像一个创建镜像的小 "程序"。

  • 这个小程序在运行时要注意。

  • docker build -t <em>name-of-result</em> .

每一步都会产生一个镜像

  • 每一行都从上一行获取图像并制作另一个图像。
  • 之前的镜像是没有变化的
  • 前一行的状态永远不会被编辑。

构建过程的每一步都会产生一个新的图像。构建序列是一系列的步骤,它从一个镜像开始,用它做一个容器,在里面运行一些东西,然后制作一个新的镜像。前一个步骤中的镜像是没有变化的。接下来的每一步都是说,从上一步开始,做一些新的改变,然后保存到一个新的镜像。Dockerfiles不是shell脚本,尽管它们看起来像shell脚本,因为这有助于人们熟悉它们,而且使它们更容易学习。你在一行中启动的进程不会在下一行中运行。你运行它们,它们在该容器的持续时间内运行,然后该容器被关闭,保存到一个镜像中,并在下一行重新开始。我们将通过几个例子来看看这是如何工作的。


一个基本的Docker文件在运行

要开始使用Dockerfile,我们可以从三个命令开始,以保持事情简单。你会在你遇到的几乎所有Docker文件中发现这些命令。

  • FROM- 设置后续指令要使用的baseImage。FROM必须是Docker文件中的第一个指令。
  • RUN- 在当前镜像之上作为一个新层执行任何指令,并提交结果。
  • CMD- 为一个执行中的容器提供默认值。如果没有指定一个可执行文件,那么也必须指定ENTRYPOINT。一个Docker文件中只能有一条CMD指令。

首先,在你选择的目录中创建一个名为Dockerfile的文件,没有任何扩展名

dockerfile-tutorial> touch Dockerfile

现在,我们可以用下面的命令来填充这个文件。

FROM busybox

RUN echo "building a docker image"

CMD echo "hello from the container!"

运行一个Dockerfile

现在,有趣的部分来了。我们要运行我们刚刚创建的Dockerfile。回顾一下,这可以用docker build命令来完成,它是命令的构建者管理集合的一部分。的 **-t**我们包括的是简单地给图像标记一个名字,这样我们就有了可以参考的东西,否则,产生的图像就只有一个神秘的ID,而没有容易读懂的名字。另一点要注意的是,在命令的最后有一个点。这只是说,使用我们所在的这个目录中的Docker文件。

dockerfile-tutorial> docker builder build -t my-container .
Sending build context to Docker daemon  2.048kB
Step 1/3 : FROM busybox
latest: Pulling from library/busybox
9758c28807f2: Pull complete
Digest: sha256:a9286defaba7b3a519d585ba0e37d0b2cbee74ebfe590960b0b1d6a5e97d1e1d
Status: Downloaded newer image for busybox:latest
 ---> f0b02e9d092d
Step 2/3 : RUN echo "building a docker image"
 ---> Running in 1ec98c10d7d4
building a docker image
Removing intermediate container 1ec98c10d7d4
 ---> 8b4d5802aed5
Step 3/3 : CMD echo "hello from the container!"
 ---> Running in 7c4e7b0aa45b
Removing intermediate container 7c4e7b0aa45b
 ---> 2cd663381747
Successfully built 2cd663381747
Successfully tagged my-container:latest

镜像被成功构建,并在Visual Studio Code和Docker Dashboard中显示出来。

dockerfile visualstudiocode

images in docker dashboard

让我们看一下这里发生了什么。我们看到的第一件事是 "向Docker守护进程发送构建环境"。这是Docker客户端向Docker守护进程发送构建上下文。该构建上下文是Docker文件所在的整个目录。在这之后,Docker将打印出构建过程的每个步骤。在三个步骤中的第一步,我们有以下内容。

Step 1/3 : FROM busybox
latest: Pulling from library/busybox
9758c28807f2: Pull complete
Digest: sha256:a9286defaba7b3a519d585ba0e37d0b2cbee74ebfe590960b0b1d6a5e97d1e1d
Status: Downloaded newer image for busybox:latest
 ---> f0b02e9d092d

这一步只是从busybox下载并创建一个镜像,这个镜像非常小,里面只有一个shell。然后构建过程进入第三步中的第二步。

Step 2/3 : RUN echo "building a docker image"
 ---> Running in 1ec98c10d7d4
building a docker image
Removing intermediate container 1ec98c10d7d4
 ---> 8b4d5802aed5

在第二步中,它从该镜像开始,创建一个容器,并运行提供的命令。从那个运行的容器中,运行 "构建简单的docker镜像 "的回声。在这个步骤中,8b4d5802aed5镜像被创建。有一个1ec98c10d7d4的中间值,它被用来运行我们的构建步骤。Docker很聪明,知道这个中间容器不会在Docker文件的其他地方使用,所以它清理了这个中间容器。这就把我们带到了第3步。

Step 3/3 : CMD echo "hello from the container!"
 ---> Running in 7c4e7b0aa45b
Removing intermediate container 7c4e7b0aa45b
 ---> 2cd663381747
Successfully built 2cd663381747
Successfully tagged my-container:latest

在最后一步中,Docker设置了CMD,以便在这个容器启动时运行。它正在更新镜像的状态。这一步产生了另一个中间容器7c4e7b0aa45b,它被临时使用,以便提交给最终的镜像。和前面的步骤一样,一旦不再需要它,它就被移除。所以最终的结果是2cd663381747图像。这是它的图像ID。由于我们在构建过程中对图像进行了标记(阅读-命名),我们可以看到 **my-container**是指 2cd663381747 图像。


运行你的镜像

上面的部分展示了如何通过使用Docker文件Docker中构建最基本的镜像。构建成功后,我们现在在本地注册表中看到了这个镜像。好了,镜像是用来运行的,所以让我们继续前进,从我们新构建的镜像中启动一个容器吧!

dockerfile-tutorial> docker container run my-container
hello from the container!

WORKDIR和COPY实例

现在让我们建立另一个镜像,但让它更有趣一些。我们将构建并运行一个Nginx容器,同时在构建过程中把我们自己的自定义HTML和CSS添加到容器中。这样,我们就可以看到我们自己的HTML页面,而不是默认的Nginx闪亮页面。在练习目录中添加这两个文件。

css.css

body {
    font-family: Verdana;
    display: flex;
    height: 200px;
    align-items: center;
    justify-content: center;
}

index.html

<html>
<head>
<title>Containers!</title>
<link rel="stylesheet" href="css.css" type="text/css" />
</head>

<body>
    <h1>It works!</h1>
</body>
</html>

Docker文件

FROM nginx:latest

WORKDIR /usr/share/nginx/html

COPY css.css index.html ./

这里的Dockerfile在FROM命令中使用了nginx:latest。我们没有建立我们自己的Nginx,因为已经存在一个官方的镜像,它是经过测试的,可以使用。在你的Docker文件中使用官方镜像,将使你的Docker文件随着时间的推移更容易维护。你可以很容易地在现有的基础镜像上增加功能,这就是Docker的作用,也是它的工作方式。现在我们看到WORKDIR命令的作用。你几乎可以认为WORKDIR就像运行 "cd /the/directory/path"。然而,在Docker文件中,只要你需要设置目录路径,就应该使用WORKDIR。在这个例子中,我们将工作目录设置为Nginx的默认根HTML文件夹。我们运行的最后一条命令是COPY命令,用于将自己的源代码文件复制到容器镜像中。在我们的例子中,我们只是把css.css和index.html,然后放在./目录下(指的是/usr/share/nginx/html),因为我们已经用WORKDIR设置了路径。现在,你可以看到,这个Docker文件的结尾缺少一个必要的CMD命令。这是因为FROM镜像(nginx)中已经有一个CMD。使用FROM意味着你继承了该基础镜像中的所有内容。你可以通过查看Docker hub上的图层,或使用docker history命令来了解这一点--两者都显示在这里。

在Docker Hub上查看镜像层。
view docker image layers

查看nginx镜像历史。
docker history nginx

在我们使用这个Docker文件构建镜像之前,让我们确认目前系统上没有镜像。
docker no images on disk

现在我们可以构建镜像了,我们将使用-t来命名它,这样我们就有一个漂亮的名字可以使用。

dockerfile-tutorial> docker build -t customnginx .
Sending build context to Docker daemon  4.096kB
Step 1/3 : FROM nginx:latest
latest: Pulling from library/nginx
bb79b6b2107f: Pull complete
111447d5894d: Pull complete
a95689b8e6cb: Pull complete
1a0022e444c2: Pull complete
32b7488a3833: Pull complete
Digest: sha256:ed7f815851b5299f616220a63edac69a4cc200e7f536a56e421988da82e44ed8
Status: Downloaded newer image for nginx:latest
 ---> f35646e83998
Step 2/3 : WORKDIR /usr/share/nginx/html
 ---> Running in a3c878027088
Removing intermediate container a3c878027088
 ---> 92aebe9e2888
Step 3/3 : COPY css.css index.html ./
 ---> 30abd7dc1d52
Successfully built 30abd7dc1d52
Successfully tagged customnginx:latest

构建过程很顺利,我们在Docker Desktop中看到了新的镜像。
new image on disk docker desktop

在这一点上,我们已经构建了一个镜像,但还没有一个正在运行的容器。让我们确认一下。
docker getting started

让我们运行我们的新容器吧!

dockerfile-tutorial> docker container run -dp 80:80 customnginx
d5e78a8c4d4947cc51773152c6040c13b82c6fd43a2f3dbf483b894b17e1f8b0

好的,容器运行得很好!
customnginx container running

现在访问http://localhost,你就可以看到新的自定义nginx容器了
the docker container works


常用的Dockerfile命令

在这一部分,让我们看看最常用的Dockerfile命令。最需要注意的是FROMENVRUNEXPOSECMD


该 **FROM**声明

FROM语句只是说要从哪个镜像开始运行。这应该永远是Docker文件中的第一个表达式。实际上,在Dockerfile中放多个表达式也是可以的。这意味着Dockerfile产生了不止一个镜像。

使用方法。

  • FROM :
  • FROM :
  • @中获取

信息。

  • FROM必须是Docker文件中的第一个非逗号指令。
  • FROM可以在一个Docker文件中多次出现,以便创建多个镜像。只需在每个新的FROM指令前记下提交所输出的最后一个镜像ID。
  • 标签或摘要的值是可选的。如果你省略了其中任何一个,构建器默认会假定一个最新的。如果不能匹配标签值,构建器会返回一个错误。

声明 **RUN**声明

RUN 语句表示通过 shell 运行一个命令。例如,RUN解压缩install.zip文件到这个目录或RUN hello docker。

使用方法。

  • RUN (shell形式,命令在shell中运行,默认情况下,Linux上是/bin/sh -c,Windows上是cmd /S /C)
  • RUN ["<执行>", "", ""] (执行形式)

信息。

  • exec形式使得避免shell字符串的混合成为可能,并且可以使用不包含指定shell可执行文件的基本镜像来运行命令。
  • shell形式的默认shell可以用SHELL命令来改变。
  • 当使用exec形式时,正常的shell处理不会发生。例如,RUN ["echo", "HOME"\] 不会对HOME进行变量替换。

声明 **CMD**声明

这是必须的,也是每次从映像中启动新容器时要运行的最后一条命令。当你重新启动一个停止的容器时,它也是如此。

使用方法。

  • CMD ["","",""] (执行形式,这是首选形式)
  • CMD ["",""] (作为ENTRYPOINT的默认参数)
  • CMD (shell形式)

信息。

  • CMD的主要目的是为一个正在执行的容器提供默认值。这些默认值可以包括一个可执行文件,也可以省略可执行文件,在这种情况下,你必须同时指定一个ENTRYPOINT指令。
  • 一个Docker文件中只能有一条CMD指令。如果你列出一个以上的CMD,那么只有最后一个CMD会生效。
  • 如果CMD被用来为ENTRYPOINT指令提供默认参数,那么CMD和ENTRYPOINT指令都应该用JSON数组格式指定。
  • 如果用户指定参数给docker run,那么它们将覆盖CMD中指定的默认参数。
  • 使用exec形式时,正常的shell处理不会发生。例如,CMD ["echo", "HOME"\] 将不会对HOME进行变量替换。

LABEL语句

使用方法。

  • LABEL = [= ...]

信息。

  • LABEL指令将元数据添加到图像中。
  • 要在LABEL值中包含空格,请使用引号和反斜线,就像你在命令行解析中一样。
  • 标签是累加的,包括FROM镜像中的LABEL。
  • 如果Docker遇到一个已经存在的标签/键,新的值会覆盖以前任何具有相同键的标签。
  • 要查看一个图像的标签,请使用docker inspect命令。它们会在 "标签 "的JSON属性下。

聲明 **ENV**声明

环境语句设置环境变量,如ENV DB_HOST=db.production.example.com或DB_PORT为5432。

使用方法。

  • ENV <键> <值
  • ENV = [= ...] 。

信息。

  • ENV指令将环境变量设置为的值。
  • 该值将出现在所有 "后代 "Dockerfile命令的环境中,并且也可以被内联替换。
  • 使用ENV设置的环境变量将在容器从生成的镜像运行时持续存在。
  • 第一种形式是将单个变量设置为一个值,第一个空格后的整个字符串被视为 - 包括空格和引号等字符。

聲明 **EXPOSE**声明

默认情况下,容器内没有开放任何tcp或udp端口。它不会将容器中的任何东西暴露给虚拟网络,除非它在这里被列出。EXPOSE命令并不意味着这些端口会在你的主机上自动打开,这就是-p命令在docker容器运行中的作用。

使用方法。

  • EXPOSE <端口> [<端口> ...]

信息。

  • 通知Docker,容器在运行时监听指定的网络端口。
  • EXPOSE不会使容器的端口被主机访问。

COPY语句

用法。

  • COPY [ ...] [ ...
  • COPY ["", ... ""](这种形式对于含有空白的路径是必需的)

信息。

  • 从复制新的文件或目录,并将它们添加到路径的映像的文件系统中。
  • 可以包含通配符,匹配将使用Go的filepath.Match规则进行。
  • 必须是相对于正在构建的源目录(构建的背景)。
  • 是一个绝对路径,或相对于WORKDIR的路径。
  • 如果不存在,它将和其路径中所有缺失的目录一起被创建。

ADD语句

ADD语句是一个非常有用的表达式,你可以用它来添加一个本地文件并执行其他任务。它也可以添加存档中的内容。所以,如果你说ADD project.tar.gz到/install,它不会把tar.gz文件复制到该目录,它注意到它是一个压缩的归档文件,并把该归档文件中的所有文件解压缩到该目录。它也适用于URL。你可以说把你从这个大的URL下载的东西添加到/project。这将导致project.rpm被下载并放在project/project.rmp中。

使用方法。

  • ADD [ ...]
  • ADD ["", ... ""] (这种形式对于含有空白的路径是必需的)

信息。

  • 从复制新的文件、目录或远程文件URL,并将它们添加到路径的图像文件系统中。
  • 可以包含通配符,匹配将使用Go的filepath.Match规则进行。
  • 如果是一个文件或目录,那么它们必须是相对于正在构建的源目录(构建的上下文)。
  • 是一个绝对路径,或者是一个相对于WORKDIR的路径。
  • 如果不存在,它将和其路径中所有缺失的目录一起被创建。

WORKDIR语句

使用方法。

  • WORKDIR </path/to/workdir>。

信息。

  • 为任何RUN、CMD、ENTRYPOINT、COPY和ADD指令设置工作目录。
  • 它可以在一个Docker文件中多次使用。如果提供了一个相对路径,它将是相对于前一个WORKDIR指令的路径。

VOLUME语句

使用方法。

  • VOLUME ["", ...]
  • VOLUME [ ...]。

用指定的名称创建一个挂载点,并将其标记为容纳来自本地主机或其他容器的外部挂载卷。


ENTRYPOINT语句

ENTRYPOINT 与 CMD 类似,但它指定了启动容器时要使用的表达式的开头,并允许你在结尾处添加更多内容。因此,如果你的容器有一个LS的入口点,那么当你说Docker运行我的镜像名称时,你输入的任何东西都会被当作LS命令的参数。CMD指定了要运行的整个命令,如果这个人在运行容器时,在Docker run image name之后输入了一些东西,那就会代替CMD被运行。当人们向你的容器添加参数时,ENTRYPOINT会被加入,当人们向你的容器添加参数时,CMD会被替换。实际上,你可以同时使用它们两个。如果你同时拥有它们,它们会被串联起来,一个接一个。一般来说,如果你想做一个看起来像程序的东西,并且你想让人们不关心它是在Docker容器中运行的,ENTRYPOINT是用来让你的容器看起来像正常的程序。CMD是更常用的方法。

使用方法。

  • ENTRYPOINT ["<可执行程序>", "", ""] (执行形式,首选)
  • ENTRYPOINT (shell形式)

信息。

  • 允许你配置一个将作为可执行文件运行的容器。
  • docker run 的命令行参数将被附加在exec形式的ENTRYPOINT的所有元素之后,并将覆盖所有使用CMD指定的元素。
  • shell形式阻止任何CMD或运行命令行参数的使用,但ENTRYPOINT将通过shell启动。这意味着可执行程序不会是PID 1,也不会收到UNIX信号。预先添加exec来绕过这个缺点。
  • 只有Docker文件中的最后一条ENTRYPOINT指令才有效果。

USER语句

USER语句说,我希望在这个容器中运行的命令是以用户John的身份运行的,也可能是以用户的编号标识的,USER 5000。如果你有共享的网络目录,并假定有一个固定的用户名或固定的用户号,这可能很有用。

使用方法。

  • USER <用户名|UID

USER指令设置了运行镜像时使用的用户名或UID,以及Docker文件中任何RUN、CMD和ENTRYPOINT指令。


ONBUILD语句

使用方法。

  • ONBUILD 。

信息。

  • 在映像中添加一条触发指令,以便在稍后的时间执行,当该映像被用作另一个构建的基础。触发器将在下游构建的上下文中执行,就像它在下游Dockerfile中的FROM指令之后被立即插入一样。
  • 任何构建指令都可以被注册为一个触发器。
  • 触发器只被 "子 "构建所继承。换句话说,它们不会被 "孙子 "构建所继承。
  • ONBUILD指令不能触发FROM、MAINTAINER或ONBUILD指令。

ARG语句

使用方法。

  • ARG <名称>[=<默认值>] 。

信息。

  • 定义一个变量,用户可以在构建时使用-build-arg =标志将其传递给docker构建器。
  • 可以通过多次指定ARG来定义多个变量。
  • 不建议使用构建时变量来传递秘密,如github密钥、用户凭证等。构建时变量的值对任何使用docker history命令的镜像用户都是可见的。
  • 使用ENV指令定义的环境变量总是覆盖同名的ARG指令。
  • Docker有一组预定义的ARG变量,你可以在Dockerfile中不使用相应的ARG指令。
    • HTTP_PROXY和http_proxy
    • HTTPS_PROXY 和 https_proxy
    • FTP_PROXY和ftp_proxy
    • NO_PROXY和no_proxy

SHELL语句

使用方法。

  • SHELL ["<可执行文件>", "", ""]

信息。

  • 允许覆盖用于命令的shell形式的默认shell。
  • 每条SHELL指令都会覆盖所有之前的SHELL指令,并影响所有后续指令。
  • 允许使用另一种shell,如zsh、csh、tcsh、powerhell等。

HEALTHCHECK语句

用法。

  • HEALTHCHECK [] CMD (通过在容器内运行一个命令来检查容器的健康状况)
  • HEALTHCHECK NONE(禁用从基本镜像继承的任何健康检查)

信息。

  • 告诉Docker如何测试一个容器以检查它是否仍在工作
  • 每当健康检查通过,它就成为健康的。在连续失败一定次数后,它就变得不健康了。
  • 可以出现的<选项>有...
    • -interval= (默认:30s)
    • -timeout= (默认:30s)
    • -retries= (默认: 3)
  • 健康检查将在容器启动后的间隔秒内首次运行,然后在之前的每次检查完成后的间隔秒内再次运行。如果检查的单次运行时间超过了超时秒数,那么检查就被认为是失败的。容器需要连续重试健康检查失败才会被认为是不健康的。
  • 一个Docker文件中只能有一条HEALTHCHECK指令。如果你列出多个,那么只有最后一个HEALTHCHECK才会生效。
  • 可以是一个shell命令,也可以是一个exec JSON数组。
  • 该指令的退出状态表示容器的健康状态。
    • 0: 成功 - 容器是健康的并且可以使用
    • 1: 不健康 - 容器不能正常工作
    • 2: 保留 - 不要使用这个退出代码
  • 来自的stdout和stderr的前4096字节被存储起来,可以用docker inspect来查询。
  • 当一个容器的健康状态发生变化时,会产生一个带有新状态的health_status事件。

STOPSIGNAL 语句

使用方法。

  • STOPSIGNAL 指令

STOPSIGNAL 指令设置了将被发送给容器退出的系统调用信号。这个信号可以是一个有效的无符号数字,与内核的系统调用表中的一个位置相匹配,例如9,或者是一个格式为SIGNAME的信号名称,例如SIGKILL。