翻译·原文地址

本文档介绍了构建高效镜像推荐的最佳实践和方法。

Docker 通过读取 Dockerfile 中的指令自动构建镜像 - Dockerfile 是一个文本文件,按顺序包含构建指定镜像所需的所有命令。Dockerfile 遵循特定的格式和指令集,你可以在 Dockerfile 参考 中查看相关内容。

Docker 镜像由只读层组成,每个层都代表一个 Dockerfile 指令。这些层是堆叠的,每一层都是前一层变化的增量。考虑这个 Dockerfile:

FROM ubuntu:15.04
COPY . /app
RUN make /app
CMD python /app/app.py

每条指令创建一个层:

  • FROM 创建一个来自 ubuntu:15.04 Docker 镜像的层。
  • COPY 从你的 Docker 客户端的当前目录添加一些文件。
  • RUN 使用 make 构建你的应用。
  • CMD 指定在容器内运行的命令。

运行镜像并生成容器时,你在基础层的顶部添加了新的可写层(“容器图层”)。对正在运行的容器所做的所有更改(例如写入新文件,修改现有文件和删除文件)都将写入此可写容器层。

有关镜像层的更多信息(以及 Docker 如何构建和存储镜像),参阅关于存储驱动器

一、常规准则和建议

1.1 创建短暂的容器

Dockerfile 定义的镜像应该生成尽可能短暂的容器。依靠“短暂”,我们的意思是容器可以被停止和销毁,然后重建,并用最小的设置和配置进行替换。

参阅十二要素应用方法下的进程,以了解以无状态方式运行容器的动机。

1.2 理解构建上下文

译注:“构建上下文”中的“构建”是名词,不是动词)

发出 docker build 命令时,当前工作目录被称为构建上下文。默认情况下,假定 Dockerfile 文件位于其中,但你可以使用文件标记(-f)指定其它位置。无论 Dockerfile 实际存在于何处,当前目录中的所有文件和目录的递归内容都将作为构建上下文发送到 Docker 守护程序。

构建上下文示例

为构建上下文创建一个目录并 cd 进去。将 “hello” 写入名为 hello 的文本文件中,并创建一个 cat 它的 Dockerfile。从构建上下文(.)中构建镜像:

$ mkdir myproject && cd myproject
$ echo "hello" > hello
$ echo -e "FROM busybox\nCOPY /hello /\nRUN cat /hello" > Dockerfile
$ docker build -t helloapp:v1 .

Dockerfilehello 移动到单独的目录中并构建镜像的第二个版本(不依赖于上一个构建的缓存)。使用 -f 指向 Dockerfile 并指定构建上下文的目录:

$ mkdir -p dockerfiles context
$ mv Dockerfile dockerfiles && mv hello context
$ docker build --no-cache -t helloapp:v2 -f dockerfiles/Dockerfile context

无意中包含构建镜像不需要的文件会导致更大的构建上下文和更大的镜像大小。这会增加构建镜像的时间,拉取和推送镜像的时间以及容器运行时大小。要查看构建上下文有多大,在构建 Dockerfile 时查找类似这样的消息:

Sending build context to Docker daemon  187.8MB

1.3 通过 stdin 传递 Dockerfile

Docker 17.05 增加了使用本地或远程构建上下文并通过 stdin 传递 Dockerfile 来构建镜像的功能。在早期版本中,使用 stdin 中的 Dockerfile 构建镜像不会发送构建上下文。

Docker 17.04 及更低

$ docker build -t foo -<<EOF
FROM busybox
RUN echo "hello world"
EOF

Docker 17.05 及更高 (本地构建上下文)

$ docker build -t foo . -f-<<EOF
FROM busybox
RUN echo "hello world"
COPY . /my-copied-files
EOF

Docker 17.05 及更高 (远程构建上下文)

$ docker build -t foo https://github.com/thajeztah/pgadmin4-docker.git -f-<<EOF
FROM busybox
COPY LICENSE config_local.py /usr/local/lib/python2.7/site-packages/pgadmin4/
EOF

1.4 使用 .dockerignore 排除

要排除与构建无关的文件(不重构源 Repository),使用 .dockerignore 文件。此文件支持类似于 .gitignore 文件的排除模式。有关创建信息,参阅 .dockerignore 文件

1.5 使用多阶段构建

多阶段构建(在 Docker 17.05 或更高版本中)允许你大幅减小最终镜像的大小,而不必费力地减少中间层和文件的数量。

由于镜像是在构建过程的最后阶段构建的,因此可以通过利用构建缓存来最小化镜像层。

例如,如果你的构建包含多层,则可以从较不频繁更改(以确保构建缓存可重用)到更频繁更改的顺序对它们进行排序:

  • 安装构建你的应用所需的工具
  • 安装依赖的库
  • 生成你的应用

一个 Go 应用的 Dockerfile 可能如下所示:

FROM golang:1.9.2-alpine3.6 AS build

# Install tools required for project
# Run `docker build --no-cache .` to update dependencies
RUN apk add --no-cache git
RUN go get github.com/golang/dep/cmd/dep

# List project dependencies with Gopkg.toml and Gopkg.lock
# These layers are only re-built when Gopkg files are updated
COPY Gopkg.lock Gopkg.toml /go/src/project/
WORKDIR /go/src/project/
# Install library dependencies
RUN dep ensure -vendor-only

# Copy the entire project and build it
# This layer is rebuilt when a file changes in the project directory
COPY . /go/src/project/
RUN go build -o /bin/project

# This results in a single layer image
FROM scratch
COPY --from=build /bin/project /bin/project
ENTRYPOINT ["/bin/project"]
CMD ["--help"]

1.6 不要安装不必要的软件包

为了降低复杂性,依赖性,文件大小和构建时间,避免安装额外的或不必要的软件包。例如,你不需要在数据库镜像中包含文本编辑器。

1.7 应用解耦

每个容器应该只有一个职责。将应用程序解耦到多个容器中可以更容易地水平扩展和重用容器。例如,Web 应用程序堆栈可能包含三个独立的容器,每个容器都有自己特定的镜像,以解耦的方式管理 Web 应用程序,数据库和内存缓存。

将每个容器限制为一个进程是一个很好的经验法则,但它并不是一个严格的规则。例如,不仅可以使用 init 进程生成容器,而且某些程序可能会自行生成其它进程。例如,Celery 可以生成多个工作进程,Apache 可以为每个求创建一个进程。

使用你的最佳判断,尽可能保持容器干净和模块化。如果容器彼此依赖,则可以使用 Docker 容器网络来确保这些容器可以进行通信。

1.8 最小化层数

在旧版本的 Docker 中,最大限度地减少镜像中的层数以确保它们具有高性能非常重要。以下功能被添加以减少此限制:

  • 在 Docker 1.10 及更高版本中,只有 RUNCOPYADD 指令创建层。其它指令创建临时中间镜像,而不是直接增加构建的大小。
  • 在 Docker 17.05 及更高版本中,你可以执行多阶段构建,并仅将所需的工件复制到最终镜像中。这允许你在中间构建阶段中包含工具和调试信息,而不会增加最终镜像的大小。

1.9 对多行参数进行排序

只要有可能,通过按字母数字方式对多行参数进行排序,可以减轻以后的更改工作。这有助于避免重复安装包并使列表更容易更新。这也使 PR 们更容易阅读和审查。在反斜杠(\)之前添加空格也有用。

以下是buildpack-deps镜像中的示例:

RUN apt-get update && apt-get install -y \
  bzr \
  cvs \
  git \
  mercurial \
  subversion

1.10 利用构建缓存

构建镜像时,Docker 会逐条执行 Dockerfile 中的指令,按指定的顺序执行每个指令。在检查每条指令时,Docker 会在其缓存中查找可以重用的现有镜像,而不是创建新的(重复)镜像。

如果你完全不想使用缓存,可以在 docker build 命令中使用 --no-cache = true 选项。无论怎样,如果你想让 Docker 使用它的缓存,重要的是要了解它何时能和何时不能找到匹配的镜像。Docker 遵循的基本规则概述如下:

  • 从缓存中已存在的父镜像开始,将下一条指令与从该基础镜像派生出的所有子镜像进行比较,查看它们中是否有一个使用完全相同的指令构建。如果没有,则缓存无效。

  • 在大多数情况下,只需将 Dockerfile 中的指令与其中一个子镜像进行比较即可。但是,某些指令需要更多的检查和解释。

  • 对于 ADDCOPY 指令,将检查镜像中文件的内容,并为每个文件计算校验和。在这些校验和中不考虑文件的最后修改时间和最后访问时间。在缓存查找期间,将校验和与现有镜像中的校验和进行比较。如果文件中有任何更改(例如内容和元数据),则缓存无效。

  • 除了 ADDCOPY 命令之外,缓存检查不会查看容器中的文件来确定缓存匹配。例如,在处理 RUN apt-get -y update 命令时,不会检查容器中被更新的文件来确定是否存在缓存命中。在这种情况下,Docker 仅使用命令字符串本身来查找是否存在匹配缓存。

缓存无效后,所有后续 Dockerfile 命令都会生成新镜像,并且不使用缓存。

二、Dockerfile 指令

这些建议旨在帮助你创建高效且可维护的Dockerfile。

2.1 FROM

FROM 指令的 Dockerfile 参考

尽可能使用当前的官方 Repositories 作为镜像的基础。我们推荐 Alpine 镜像,因为它受到严格控制并且尺寸较小(目前小于 5 MB),同时仍然是完整的 Linux 发行版。

2.2 LABEL

理解对象标签

你可以为镜像添加标签,以帮助按项目组织镜像,记录许可信息,帮助实现自动化或出于其它原因。为每个标签,添加以 LABEL 开头并带有一个或多个键值对的行。以下示例显示了不同的可用格式。内容中包含解释性注释。

必须使用双引号包裹带有空格的字符串或转义空格。如果字符串本身带有引号字符(")也必须转义。

# 设置一个或多个个性化标签
LABEL com.example.version="0.0.1-beta"
LABEL vendor1="ACME Incorporated"
LABEL vendor2=ZENITH\ Incorporated
LABEL com.example.release-date="2015-02-12"
LABEL com.example.version.is-production=""

一个镜像可以有多个标签。在 Docker 1.10 之前,建议将所有标签组合到单个 LABEL 指令中,以防止创建额外的层。现在不再必须这样,但仍然支持组合标签。

#在一行上设置多个标签
LABEL com.example.version =“0.0.1-beta”com.example.release-date =“2015-02-12”

以上也可以写成:

# 一次设置多个标签, 使用续行符打断长行
LABEL vendor=ACME\ Incorporated \
      com.example.is-beta= \
      com.example.is-production="" \
      com.example.version="0.0.1-beta" \
      com.example.release-date="2015-02-12"

有关可接受的标签键和值的指导,参阅理解对象标签。有关查询标签的信息,参阅管理对象标签中与过滤相关的条目。另参见 Dockerfile 参考中的 LABEL

2.3 RUN

RUN 指令的 Dockerfile 参考

使用反斜杠拆分长或复杂的 RUN 语句为多行,使你的 Dockerfile 更具可读性,可理解性和可维护性。

2.3.1 APT-GET

RUN 最常见的用例可能是 apt-get 的应用。因为它安装了软件包,所以 RUN apt-get 命令有几个需要注意的问题。

避免 RUN apt-get upgradedist-upgrade,因为父镜像中的许多“基础”包无法在非特权容器内升级。如果父镜像中软件包已过期,与其维护人员联系。如果你知道需要更新的特定包 foo,使用 apt-get install -y foo 自动更新。

始终将 RUN apt-get updateapt-get install 联结在同一个 RUN 语句中。例如:

RUN apt-get update && apt-get install -y \
    package-bar \
    package-baz \
    package-foo

RUN 语句中单独使用 apt-get update 会导致缓存问题和后续的 apt-get install 指令失败。例如,假设你有一个Dockerfile:

FROM ubuntu:14.04
RUN apt-get update
RUN apt-get install -y curl

构建镜像后,所有层都在 Docker 缓存中。假设你稍后通过添加额外的包来修改apt-get install:

FROM ubuntu:14.04
RUN apt-get update
RUN apt-get install -y curl nginx

Docker 将初始和修改后的指令视为相同,并重用前面步骤中的缓存。因此,会执行 apt-get update,因为构建使用了缓存的版本。由于 apt-get update 未运行,因此你的构建可能会获得 curlnginx 的过时版本。

使用 RUN apt-get update && apt-get install -y 可确保你的 Dockerfile 安装最新的软件包版本,无需进一步编码或手动干预。这种技术称为“缓存清除”。你还可以通过指定软件包版本来实现缓存清除。这称为版本固定,例如:

RUN apt-get update && apt-get install -y \
    package-bar \
    package-baz \
    package-foo=1.3.*

版本固定会强制构建检索特定版本,而不管缓存中的内容是什么。此技术还可以减少由于所需包中的意外更改而导致的故障。

下面是一个结构良好的 RUN 指令,它演示了所有 apt-get 建议。

RUN apt-get update && apt-get install -y \
    aufs-tools \
    automake \
    build-essential \
    curl \
    dpkg-sig \
    libcap-dev \
    libsqlite3-dev \
    mercurial \
    reprepro \
    ruby1.9.1 \
    ruby1.9.1-dev \
    s3cmd=1.1.* \
 && rm -rf /var/lib/apt/lists/*

s3cmd 参数指定版本为 1.1。*。如果镜像以前使用的是旧版本,则指定新版本会导致 apt-get update 缓存失效,并确保安装新版本。列出每行的包也可以防止包重复中的错误。

此外,当你通过删除 /var/lib/apt/lists 清理 apt 缓存时,它会减小镜像大小,因为 apt 缓存不存储在层中。由于 RUN 语句以 apt-get update 开头,因此始终比 apt-get install 优先刷新软件包缓存。

官方 Debian 和 Ubuntu 镜像自动运行 apt-get clean,因此不需要显式调用。

2.3.2 使用管道

某些 RUN 命令依靠使用管道符(|)将一个命令的输出传递到另一个命令,如下例所示:

RUN wget -O - https://some.site | wc -l > /number

Docker 使用 /bin/sh -c 解释器执行这些命令,该解释器仅评估管道中最后一个操作的退出代码以确定成功。在上面的示例中,只要 wc -l 命令成功,即使 wget 命令失败,此构建步骤也会成功并生成新镜像。

如果你希望命令因管道中任何阶段的错误而失败,前置 set -o pipefail && 以确保意外错误阻止构建无意中成功。例如:

RUN set -o pipefail && wget -O - https://some.site | wc -l > /number

并非所有 shell 都支持 -o pipefail 选项。

在这种情况下(例如 dash shell,它是基于 Debian 镜像上的默认 shell),考虑使用 RUNexec 形式来显式选择支持 pipefail 选项的 shell。例如:

RUN ["/bin/bash", "-c", "set -o pipefail && wget -O - https://some.site | wc -l > /number"]

2.4 CMD

CMD 指令的 Dockerfile 参考

CMD 指令应该用于运行镜像包含的软件以及跟着的任何参数。CMD 应该几乎总是以 CMD ["executable", "param1", "param2"…] 的形式使用。因此,如果镜像用于服务,例如 Apache 和 Rails,则可以运行类似 CMD ["apache2","-DFOREGROUND"] 的内容。实际上,建议将这种形式的指令用于任何基于服务的镜像。

在大多数其它情况下,应该给 CMD 一个交互式的 shell,比如 bash,python 和 perl。例如,CMD ["perl", "-de0"]CMD ["python"]CMD ["php", "-a"]。使用这种形式意味着当你执行像 docker run -it python 时,你将进入一个可用的 shell 中,进行接下来的工作。CMD 应该很少以 CMD ["param", "param"] 的方式与 ENTRYPOINT 一起使用,除非你和你的预期用户已经非常熟悉 ENTRYPOINT 的工作原理。

2.5 EXPOSE

EXPOSE 指令的 Dockerfile 参考

EXPOSE 指令指示容器侦听连接的端口。因此,你应该为你的应用使用通用的传统端口。例如,包含 Apache Web服务器的镜像将使用 EXPOSE 80,而包含 MongoDB 的镜像将使用 EXPOSE 27017 等。

对于外部访问,你的用户可以使用标记执行 docker run ,该标记指示如何将指定端口映射到他们选择的端口。对于容器链接,Docker 为从接收容器返回源的路径提供环境变量(即 MYSQL_PORT_3306_TCP)。

2.6 ENV

ENV 指令的 Dockerfile 参考

为了使新软件更易于运行,你可以使用 ENV 更新容器安装的软件的 PATH 环境变量。例如,ENV PATH /usr/local/nginx/bin:$PATH 确保 CMD ["nginx"] 正常工作。

ENV 指令对于提供特定于你想要集装箱化的服务的必需环境变量也很有用,例如 Postgres 的 PGDATA

最后,ENV 还可用于设置常用的版本号,以便版本变更更容易维护,如下例所示:

ENV PG_MAJOR 9.3
ENV PG_VERSION 9.3.4
RUN curl -SL http://example.com/postgres-$PG_VERSION.tar.xz | tar -xJC /usr/src/postgress && …
ENV PATH /usr/local/postgres-$PG_MAJOR/bin:$PATH

与在程序中使用常量变量(与硬编码值相反)类似,此方法允许你更改单个 ENV 指令以自动变更容器中的软件版本。

每行 ENV 都会创建一个新的中间层,就像 RUN 命令一样。这意味着即使你在将来的图层中取消设置环境变量,它仍然会在此图层中保留,并且可以转储其值。你可以通过创建如下所示的Dockerfile来测试它,然后构建它。

FROM alpine
ENV ADMIN_USER="mark"
RUN echo $ADMIN_USER > ./mark
RUN unset ADMIN_USER
CMD sh
$ docker run --rm -it test sh echo $ADMIN_USER

mark

要防止这种情况,并且确实取消设置环境变量,使用带有 shell 命令的 RUN 命令,在单个图层中设置,使用和取消设置所有变量。你可以使用 ;&& 将命令分开。如果你使用第二种方法,并且其中一个命令失败,则 docker 构建也会失败。这通常是一个好主意: 使用 \ 作为 Linux Dockerfiles 的续行符可以提高可读性。你还可以将所有命令放入 shell 脚本中,并使用 RUN 命令运行该 shell 脚本。

FROM alpine
RUN export ADMIN_USER="mark" \
    && echo $ADMIN_USER > ./mark \
    && unset ADMIN_USER
CMD sh
$ docker run --rm -it test sh echo $ADMIN_USER

2.7 ADD 或 COPY

尽管 ADDCOPY 在功能上相似,但一般来说,COPY 是首选的。那是因为它比 ADD 更易理解。COPY 仅支持将本地文件基本复制到容器中,而 ADD 具有一些功能(如仅限本地的 tar 提取和远程 URL 支持),这些功能并不是很容易理解。因此,ADD 的最佳用途是将本地 tar 文件自动提取到镜像中,如 ADD rootfs.tar.xz / 所示。

如果你有多个使用上下文中不同文件的 Dockerfile 步骤,单独复制它们,而不是一次复制它们。这可确保每个步骤的构建缓存仅在特定所需文件更改时失效(强制重新执行该步骤)。

例如:

COPY requirements.txt /tmp/
RUN pip install --requirement /tmp/requirements.txt
COPY . /tmp/

与把 COPY . /tmp/ 放置在前面相比,RUN 步骤的失效缓存更少。

由于镜像大小很重要,因此强烈建议不要使用 ADD 从远程 URL 中获取包。你应该使用 curlwget 代替。这样,你可以删除提取后不再需要的文件,也不必在镜像中添加其它图层。例如,你应该避免做以下事情:

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

而是做一些像:

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 的 tar 自动提取功能的其它项目(文件,目录),应始终使用COPY。

2.8 ENTRYPOINT

ENTRYPOINT 指令的 Dockerfile 参考

ENTRYPOINT 的最佳用途是设置镜像的主命令,允许该镜像通过该命令运行(然后使用 CMD 作为默认标记)。

让我们从命令行工具 s3cmd 的镜像示例开始:

ENTRYPOINT ["s3cmd"]
CMD ["--help"]

现在可以像这样运行镜像来显示命令的帮助:(译注:也就是启动该容器时执行的命令为 s3cmd --helpCMD 中的内容被当做 ENTRYPOINT 命令的默认参数,CMD 指令的 Dockerfile 参考 中也有相关说明)

$ docker run s3cmd

或使用指定的参数执行命令:

$ docker run s3cmd ls s3://mybucket

这很有用,因为镜像名称可以兼作二进制文件的引用,如上面的命令所示。

ENTRYPOINT 指令也可以与辅助脚本结合使用,使其能够以与上述命令类似的方式运行,即使启动该工具可能需要多个步骤。

例如,Postgres 官方镜像使用以下脚本作为其 ENTRYPOINT

#!/bin/bash
set -e

if [ "$1" = 'postgres' ]; then
    chown -R postgres "$PGDATA"

    if [ -z "$(ls -A "$PGDATA")" ]; then
        gosu postgres initdb
    fi

    exec gosu postgres "$@"
fi

exec "$@"

将应用配置为 PID 1

此脚本使用 exec Bash 命令,以便最终运行的应用成为容器的 PID 1。这允许应用接收发送到容器的任何 Unix 信号。有关更多信息,参阅 ENTRYPOINT 参考

脚本被复制到容器中并通过容器启动时的 ENTRYPOINT 运行:

COPY ./docker-entrypoint.sh /
ENTRYPOINT ["/docker-entrypoint.sh"]
CMD ["postgres"]

该脚本允许用户以多种方式与 Postgres 交互。

它可以简单地启动 Postgres:

$ docker run postgres

或者,它可用于运行 Postgres 并将参数传递给服务器:

$ docker run postgres postgres --help

最后,它还可以用来启动一个完全不同的工具,比如 Bash:

$ docker run --rm -it postgres bash

2.9 VOLUME

VOLUME 指令的 Dockerfile 参考

VOLUME 指令应用于公开由 Docker 容器创建的任何数据库存储区域,配置存储或文件/目录。强烈建议你将 VOLUME 用于镜像的任何可变和供用户可用的部分。

2.10 USER

USER 指令的 Dockerfile 参考

如果服务可以在没有权限的情况下运行,使用 USER 更改为非 root 用户。首先在 Dockerfile 中创建用户和组,例如 RUN groupadd -r postgres && useradd --no-log-init -r -g postgres postgres

考虑一个明确的 UID/GID

镜像中的用户和组被分配了一个非确定性 UID/GID,因为无论镜像重建如何,都会分配“下一个” UID/GID。因此,如果它很重要,你应该分配一个显式的 UID/GID。

由于 Go 文件/压缩包处理稀疏文件时一个未解决的 bug,尝试在 Docker 容器内创建具有非常大的 UID 的用户可能导致磁盘耗尽,因为容器层中的 /var/log/faillog 填充了 NULL(\0) 个字符。解决方法是将 --no-log-init 标记传递给 useradd。Debian/Ubuntu adduser 包装器不支持此标志。

避免安装或使用 sudo,因为它具有不可预测的 TTY 和可能导致问题的信号转发行为。如果你绝对需要类似于 sudo 的功能,例如将守护程序初始化为 root 但将其作为非 root 运行,考虑使用“gosu”。

最后,为了减少层次和复杂性,避免频繁地来回切换 USER

2.11 WORKDIR

WORKDIR 指令的 Dockerfile 参考

为了清晰和可靠,你应该始终使用绝对路径的 WORKDIR。此外,你应该使用 WORKDIR 而不是像 RUN cd … && do-something 那样激增指令,这些指令难以阅读,排除故障和维护。

2.12 ONBUILD

ONBUILD 指令的 Dockerfile 参考

ONBUILD 命令的执行发生在当前 Dockerfile 构建完成后。ONBUILD 在从(FROM)当前镜像派生的任何子镜像中执行。将 ONBUILD 命令视为父 Dockerfile 为子 Dockerfile 提供的指令。

Docker 构建在子 Dockerfile 中的任何命令之前执行 ONBUILD 命令。

ONBUILD 对于以(FROM)给定镜像来构建的镜像非常有用。例如,你可以使用 ONBUILD 作为语言堆栈镜像,在 Dockerfile 中构建使用该语言编写的任意用户软件,如 Ruby 的 ONBUILD 变体中所示(译注:该链接已失效,目前很难找到含有 ONBUILD 的官方 Dockerfile)。

ONBUILD 构建的镜像应该获得一个单独的标记,例如:ruby:1.9-onbuildruby:2.0-onbuild

ADDCOPY 放入 ONBUILD 时要小心。如果新构建的上下文缺少正在添加的资源,则 “onbuild” 镜像将发生灾难性故障。如上所述,添加单独的标记有助于通过允许 Dockerfile 作者做出选择来缓解这种情况。

三、官方 Repositories 示例

这些官方 Repositories 具有示例性的 Dockerfiles:

四、其它资源:

(完)