Dockerfile

1. 镜像与容器的关系

docker 中镜像是层级结构的,我们可以通过命令 docker history <id/name> 查看镜像中的每一层的大小和内容。镜像是 readonly 的。容器从镜像启动时,docker 会在镜像的最上层创建一个可写层,镜像本身保持不变。删除容器只是删除容器创建的可写层,因此创建和删除容器都很快。

所以我们在创建应用时会利用 Dockerfile 将只读文件提前构建在镜像中来提高容器的效率。

2. 什么是 Dockerfile

Dockerfile 是一个文本文档,其中包含组装 Docker 映像的指令。当我们通过执行 docker build 命令告诉 Docker 构建我们的镜像时,Docker 会读取这些指令,执行它们,并因此创建一个 Docker 镜像。

3. 构建一个 Web 服务器

我们通过构建一个 python 版本的 Web 服务器来学习如何构建一个镜像。

3.1 Web 服务

使用 Flask 框架创建一个简单的 Python 应用程序。在本地创建一个名为 python-docker 的目录,然后按照下面的步骤创建一个简单的 Web 服务器。

er cd /path/to/python-docker
 pip3 install Flask
 pip3 freeze > requirements.txt
 touch app.py

然后在 app.py 文件中输入以下代码。

from flask import Flask
app = Flask(__name__)

@app.route('/')
def hello_world():
    return 'Hello, Docker!'

3.2 编写 Dockerfile

在项目的根目录中(上个步骤中创建的 python-docker 目录),创建一个名为的文件 Dockerfile 并在文本编辑器中打开该文件。

Dockerfile 的名称是什么?

用于 Dockerfile 的默认文件名是 Dockerfile(没有文件扩展名)。使用默认名称允许您运行 docker build 命令而无需指定其他命令标志。

某些项目可能需要不同的 Dockerfiles 用于特定目的。一个常见的约定是将这些命名为 Dockerfile.<something><something>.Dockerfile。然后可以通过 docker build 命令上的 --file(或 -f 速记)选项使用此类 Dockerfile。

一般情况建议使用默认文件名 Dockerfile

接下来,在 Dockerfile 中添加一行,告诉 Docker 我们希望为我们的应用程序使用什么基础镜像。

  FROM python:3.8-slim-buster

Docker 镜像可以从其他镜像继承。因此,我们将使用官方 Python 镜像,而不是创建我们自己的基础镜像,该镜像已经拥有运行 Python 应用程序所需的所有工具和包。

为了在运行其余命令时更容易,创建一个工作目录。这会指示 Docker 使用此路径作为所有后续命令的默认位置。通过这样做,我们不必输入完整的文件路径,而是可以使用基于工作目录的相对路径。

  WORKDIR /app

通常,下载了用 Python 编写的项目后要做的第一件事就是安装第三方包。这可确保您的应用程序已安装其所有依赖项。

我们需要将 requirements.txt 文件放入镜像中。使用 COPY 命令来执行此操作。COPY 命令有两个参数,第一个参数告诉 Docker 您想要复制到镜像中的文件,第二个参数告诉 Docker 您希望将该文件复制到何处。我们要把 requirements.txt 文件复制到我们的工作目录 /app 中。

  COPY requirements.txt requirements.txt

一旦在镜像中拥有 requirements.txt 文件,我们就可以使用 RUN 命令来执行命令 pip3 install。这与我们在本地机器上运行 pip3 install 完全相同,但这次模块安装到镜像中。

  RUN pip3 install -r requirements.txt

此时,我们有一个基于 Python 3.8 版的镜像,并且我们已经安装了我们的依赖项。下一步是将我们的源代码添加到镜像中。 我们将使用 COPY 命令,就像上面对 requirements.txt 文件所做的那样。

  COPY . .

COPY 命令获取当前目录中的所有文件并将它们复制到映像中。现在,我们要做的就是告诉 Docker,当我们的映像在容器中执行时,我们想要运行什么命令。我们使用 CMD 命令来做这个。注意,我们需要通过指定 ——host=0.0.0.0 使应用程序在外部可见(即在容器外部可见)。

  CMD [ "python3", "-m" , "flask", "run", "--host=0.0.0.0"]

下面是完整的 Dockerfile。

# syntax=docker/dockerfile:1

FROM python:3.8-slim-buster

WORKDIR /app

COPY requirements.txt requirements.txt
RUN pip3 install -r requirements.txt

COPY . .

CMD [ "python3", "-m" , "flask", "run", "--host=0.0.0.0"]

回顾一下,我们在本地机器上创建了一个名为 python-docker 的目录,并使用 Flask 框架创建了一个简单的 Python 应用程序。我们还使用 requirements.txt 文件来收集我们的需求,并创建了一个 Dockerfile,其中包含构建镜像的命令。Python 应用程序目录结构现在看起来像:

  python-docker
|____ app.py
|____ requirements.txt
|____ Dockerfile

3.3 构建镜像

我们已经创建了我们的 Dockerfile,现在使用 docker build 命令来构建我们的镜像。docker build 命令从 Dockerfile上下文 来构建 Docker 镜像。构建的 上下文 是指定的 PATH 或 URL 中的一组文件。Dokcer 构建过程可以访问位于此上下文中的任何文件。

docker build` 命令可接受一个 `--tag` 参数,用于设置镜像的名称和格式为 `name:tag

现在我们已经创建了我们的 Dockerfile,让我们构建我们的镜像。为此,我们使用 docker build 命令。该 docker build 命令从 Dockerfile 和“上下文”构建 Docker 镜像。构建的上下文是位于指定 PATH 或 URL 中的一组文件。Docker 构建过程可以访问位于此上下文中的任何文件。

标签用于设置图像的名称和格式为 name:tag 的可选标签。

build 命令可选地接受一个 --tag 标志。标签用于设置图像的名称和格式中的可选标签 name:tag。简单起见我们暂时不使用可选项 tag。如果您不传递 tag,Docker 将使用“latest”作为其默认标签。

让我们构建我们的第一个 Docker 镜像。

docker build --tag python-docker .
 [internal] load build definition from Dockerfile
 => transferring dockerfile: 203B
 [internal] load .dockerignore
 => transferring context: 2B
 [internal] load metadata for docker.io/library/python:3.8-slim-buster
 [1/6] FROM docker.io/library/python:3.8-slim-buster
 [internal] load build context
 => transferring context: 953B
 CACHED [2/6] WORKDIR /app
 [3/6] COPY requirements.txt requirements.txt
 [4/6] RUN pip3 install -r requirements.txt
 [5/6] COPY . .
 [6/6] CMD [ "python3", "-m", "flask", "run", "--host=0.0.0.0"]
 exporting to image
 => exporting layers
 => writing image sha256:8cae92a8fbd6d091ce687b71b31252056944b09760438905b726625831564c4c
 => naming to docker.io/library/python-docker

运行 docker images 命令查看我们当前本地的所有镜像。

$ docker images
REPOSITORY      TAG               IMAGE ID       CREATED         SIZE
python-docker   latest            8cae92a8fbd6   3 minutes ago   123MB
python          3.8-slim-buster   be5d294735c6   9 days ago      113MB

您应该看到至少列出了两个镜像。一个是 base 镜像 3.8-slim-buster,另一个是我们刚刚构建的镜像 python-docker:latest

接下来通过这个镜像我们启动一个容器

$ docker run -d -p 5000:5000 python-docker:latest
2810011fd21029b3cd9817229e0b6cd16733a6d50855b9e6e0c04c1397e4afe9

现在通过 http://127.0.0.1:5000 就可以访问这个服务器了 。

4. Dockerfile 指令详解

Dockerfile 的指令不区分大小写。但是,约定是将它们大写,以便更容易地将它们与参数区分开来。

FROM

Docker 按顺序运行 Dockerfile 指令。一个 Dockerfile 必须以 FROM 指令开始FROM 指令指定我们从哪个父镜像开始构建。例如:

  FROM python:3.8-slim-buster

表示构建需要的基础镜像是 python:3.8-slim-buster,后续的操作都是基于它。

RUN

RUN 用于执行命令行命令,有以下两种格式:

shell 命令行格式

  RUN <command>
# <command>等价于直接在终端执行shell命令

exec 格式

  RUN ["executable", "param1", "param2"]
# 例如
# RUN ["python3", "app.py"] 等价于 RUN python3 app.py

注意 exec 格式会以 JSON 数组的形式解析,所以必须使用双引号。

两种格式的主要区别是 shell 命令行格式默认调用命令 shell,所以原生的 shell 命令最好使用这种格式。当有外部可执行文件时,使用 exec 格式。

CMD

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

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

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

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

CMD 命令有三种格式:

  • CMD ["executable","param1","param2"] (exec 格式,推荐使用)
  • CMD ["param1","param2"] (该写法是为 ENTRYPOINT 指令指定的程序提供默认参数)
  • CMD command param1 param2 (shell 格式)

ENTRYPOINT

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

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

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

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

格式:

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

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

示例:

假设已通过 Dockerfile 构建了 python-docker:test 镜像:

# syntax=docker/dockerfile:1

FROM python:3.8-slim-buster

WORKDIR /app

COPY requirements.txt requirements.txt
RUN pip3 install -r requirements.txt

COPY . .
ENTRYPOINT ["python3", "-m" , "flask", "run"] # 定参
CMD ["--host=0.0.0.0", "--port=5000"] # 变参
  1. 不传参运行
  docker run python-docker:test

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

  python3 -m flask run --host=0.0.0.0 --port=5000
  1. 传参运行
  docker run python-docker:test --host=0.0.0.0 --port=5001

容器内部会运行一下命令,启动主进程。

  python3 -m flask run --host=0.0.0.0 --port=5001

注意:一个 Dockerfile 中至少要要有一个 CMD 或 ENTRYPOINT 命令。

EXPOSE

仅仅只是声明端口。

作用:

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

格式:

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

ENV

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

格式:

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

注意 ENV 设置的环境变量会一致存在于容器中,因此在某些情况下它会产生副作业,所以如果只是在构建过程中使用的环境变量可以使用 ARG。

ARG

ARG 也可以设置环境变量,不过与 ENV 的作用于不同,ARG 设置的环境变量仅对 Dockerfile 内有效,也就是说只有 docker build 的过程中有效,构建好的镜像内不存在此环境变量。

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

格式:

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

ADD

ADD 指令从上下文目录中拷贝文件,目录到镜像中。格式如下:

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

ADD 遵守如下规则:

  1. 源路径如果不是 URL 则必须在构建上下文目录下
  2. 如果源路径是可识别的压缩格式(identity、gzip、bzip2 或 xz)的本地 tar 归档文件,则会自动将其解压缩为一个目录。
  3. 如果目标路径以 / 结尾,目标路径被识别为目录,源路径内容会被拷贝到其中
  4. 当源路径有多个时,目标路径必须以 / 结尾
  5. 目标路径不以 / 结尾,它会被识别为普通文件
  6. 目标路径不存在时,它会自动创建

COPY

COPY 指令与 ADD 命令格式完全一致,最主要的区别是:

  1. ADD 中的源路径可以是 url(制定一个远程的文件或文件夹)这在 Dockerfile 是从标准输入中接收的时候非常有用。
  2. COPY 中的源路径必须是在构建上下文路径中。
  3. COPY 不会自动解压压缩文件

虽然 ADD 和 COPY 在功能上相似,但一般来说,COPY 是首选。这是因为它比 ADD 更透明。 COPY 仅支持将本地文件基本复制到容器中,而 ADD 有一些特性(如仅本地的 tar 提取和远程 URL 支持)不是很明显。因此,ADD 的最佳用途是将本地 tar 文件自动提取到映像中,如 ADD rootfs.tar.xz /。

如果您有多个 Dockerfile 步骤,它们使用来自您的上下文中不同的文件,请分别复制它们,而不是一次复制所有文件。这确保了只有当特定要求的文件发生更改时,每个步骤的构建缓存才会失效(强制重新运行该步骤)。

VOLUME

创建挂着点。在启动容器时忘记挂载数据卷,会自动挂载到匿名卷。

作用:

  • 避免重要的数据,因容器重启而丢失。
  • 避免容器不断变大。

格式:

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

在启动容器 docker run 的时候,我们可以通过 -v 参数指定卷。

WORKDIR

WORKDIR 指令为 Dockerfile 中跟随它的任何 RUN、CMD、ENTRYPOINT、COPY 和 ADD 指令设置工作目录。如果 WORKDIR 不存在,即使没有在任何后续 Dockerfile 指令中使用它,也会创建它。WORKDIR 指令可以在 Dockerfile 中多次使用。如果提供了相对路径,则它将相对于上一个 WORKDIR 指令的路径。For example:

WORKDIR /a
WORKDIR b
WORKDIR c
RUN pwd

这个 Dockerfile 中最后一个 pwd 命令的输出是/a/b/c。

更多命令见官方文档

上一篇 下一篇