七叶笔记 » golang编程 » 编写Dockerfiles的最佳实践

编写Dockerfiles的最佳实践

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

Docker通过从 docker file(按顺序包含构建给定镜像所需的所有命令的文本文件)读取命令来自动构建镜像。Dockerfile遵循特定的格式和一组命令,您可以在Dockerfile reference中找到这些命令。

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

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

每一个指令会创建一个层:

· FROM从docker image ubuntu:18.04 创建层 。

· COPY从Docker 客户端添加文件到当前目录。

· RUN使用make 命令构建应用。

· CMD 指定在容器里运行的命令。

当您运行一个镜像并生成一个容器时,您将在底层之上添加一个新的可写层(“容器层”)。对正在运行的容器所做的所有更改,例如写入新文件、修改现有文件和删除文件,都被写入这个可写容器层。

通用概览和建议

创建临时容器

Dockerfile定义的镜像应该生成尽可能”短暂”的容器。所谓”临时性”,是指容器可以停止和销毁,然后用绝对最小的设置和配置重新构建和替换。

理解构建上下文

当您发出docker构建命令时,当前工作目录称为构建上下文。默认情况下, Docker file 在当前目录,但是您可以使用file标志(-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 .
 

将Dockerfile和hello移到单独的目录中,并构建镜像的第二个版本(不依赖于上一个构建的 缓存 )。使用-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
 

通过stdin使用Dockerfile 管道

Docker能够通过使用本地或远程构建上下文通过stdin管道传输Dockerfile来构建镜像。通过stdin管道传输Dockerfile对于执行一次性构建非常有用,不需要将Dockerfile写入磁盘,或者在生成Dockerfile的情况下,不应该在生成后保存Dockerfile。

为了方便起见,本节中的示例使用here文档【】,但是可以使用在stdin上提供Dockerfile的任何方法。

例如: 下面的命令是等价的:

echo -e 'FROM busybox\nRUN echo "hello world"' | docker build -
docker build -<< EOF 
FROM busybox
RUN echo "hello world"
EOF
 

您可以用您喜欢的方法或者最适合您用例的方法来替代这些例子。

使用STDIN中的DOCKERFILE构建镜像,而不发送构建上下文

使用此语法可以从stdin中用Dockerfile构建映像,而不需要发送额外的文件作为构建上下文。连字符(-)占据路径的位置,指示Docker从stdin而不是目录中读取构建上下文(其中只包含Dockerfile):

docker build [OPTIONS] –
 

下面的示例使用通过stdin传递的Dockerfile构建一个镜像。没有文件作为构建上下文发送到守护进程。

docker build -t myimage:latest -<<EOF
FROM busybox
RUN echo "hello world"
EOF
 

在Dockerfile不需要将文件复制到镜像中的情况下,省略构建上下文是非常有用的,并且可以提高构建速度,因为没有文件被发送到守护进程。

注意:如果使用这种语法,尝试构建使用COPY或ADD的Dockerfile将会失败。下面的例子说明了这一点:

# create a directory to work in
mkdir example
cd example
# create an example file
touch somefile.txt
docker build -t myimage:latest -<<EOF
FROM busybox
COPY somefile.txt .
RUN cat /somefile.txt
EOF
# observe that the build fails
...
Step 2/3 : COPY somefile.txt .
COPY failed: stat /var/lib/docker/ tmp /docker-builder249218248/somefile.txt: no such file or directory
 

使用STDIN中的DOCKERFILE从本地构建上下文构建

使用此语法可以使用本地文件系统上的文件构建映像,但要使用stdin中的Dockerfile。语法使用(-f或–file)选项指定要使用的Dockerfile,使用连字符(-)作为文件名,指示Docker从stdin中读取Dockerfile:

docker build [OPTIONS] -f- PATH
 

下面这个例子我们用当前目录作为构建上下文,并且构建镜像用到的Dockerfile是通过stdin传进去的。例子在这里【】

# create a directory to work in
mkdir example
cd example
# create an example file
touch somefile.txt
# build an image using the current directory as context, and a Dockerfile passed through stdin
docker build -t myimage:latest -f- . <<EOF
FROM busybox
COPY somefile.txt .
RUN cat /somefile.txt
EOF
 

使用STDIN中的DOCKERFILE从远程构建上下文构建

使用此语法,使用来自远程git存储库的文件(使用来自stdin的Dockerfile)构建一个镜像。语法使用(-f或–file)选项指定要使用的Dockerfile,使用连字符(-)作为文件名,指示Docker从stdin中读取Dockerfile:

docker build [OPTIONS] -f- PATH
 

当您希望从不包含Dockerfile的存储库构建镜像,或者希望使用自定义Dockerfile构建镜像,而不需要维护存储库的分支时,这种语法非常有用。

下面的示例使用来自stdin的Dockerfile构建一个镜像,并添加hello.c文件从git 仓里库【】

docker build -t myimage:latest -f-  <<EOF
FROM busybox
COPY hello.c .
EOF
Note:
 

当使用远程Git存储库作为构建上下文构建镜像时,Docker在本地 执行存储库的Git clone,并将这些文件作为构建上下文发送给守护进程。该特性要求git安装在运行docker构建命令的主机上。

使用.dockerignore忽略不需要的文件

要排除与构建不相关的文件(不需要调整资源库),请使用.dockerignore文件。该文件支持类似于.gitignore文件的排除模式。 更多信息请查看【#dockerignore-file】

使用多级构建

多阶段构建允许您大幅度减小最终映像的大小,而不必费力地减少中间层和文件的数量。

因为镜像是在构建过程的最后阶段构建的,所以可以通过利用构建缓存最小化镜像层。【#leverage-build-cache】

例如,如果您的构建包含多个层,您可以将它们排序从更改频率较低的层(以确保构建缓存可重用)到更改频率较高的层:

· 安装构建应用程序所需的工具

· 安装或者更改依赖的库

· 生成应用

下面是一个构建golang应用的Dockerfile 文件:

FROM golang:1.11-alpine 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"]
 

不安装不必要的包

为了减少复杂、依赖、文件尺寸和构建时间,避免安装额外的和不需要的包。一个高水准的Dockerfile必须要注意这些细节。

解耦

每个容器应该只有一个关注点。将应用程序解耦到多个容器可以更容易地水平伸缩和重用容器。例如,web应用程序栈可能由三个独立的容器组成,每个容器都有自己独特的镜像,以解耦的方式管理web应用程序、数据库和内存缓存。

限制每个容器只运行一个进程是一个很好的经验法则。但是,这并不准确。因为很多应用都会有很多进程。比如,Celery就会有很多worker进程。Apache每个request就会有一个进程。容器自己也有init进程。

所以,用你的严谨和专业来保持容器尽可能的干净和模块化。如果容器彼此依赖,可以使用Docker容器网络来确保这些容器能够通信。

保存最小数量的层

在老一点的docker版本中,保持层数的最少是非常重要的,因为要保证性能。

为了减少这样的限制,增加了一下的特性:

· 只有指令RUN,COPY,ADD创建层。其他指令创建临时中间镜像,并且不增加构建的大小

· 在可能的情况下,使用多阶段构建,并且只将您需要的工件复制到最终镜像中。这允许您在中间构建阶段包含工具和调试信息,而不需要增加最终映像的大小。

命令行参数排序

只要方便,可以通过对多行参数进行字母数字排序来简化后面的更改。这有助于避免包的重复,并使列表更容易更新。这也使得PRs更容易阅读和审查。在反斜杠(\)之前添加空格也有帮助。

下面是一个参数排列的例子:

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

利用构建缓存

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

如果,你就是不想用cache,可以使用—no-cache=true来关闭在执行docker build的时候。当然,如果你开启了cacha,docker 在构建是找到缓存,如果没有匹配到,就创建新的镜像。 Docker遵循的基本规则如下:

· 从缓存中已经存在的父镜像开始,将下一条指令与从该基本镜像派生的所有子镜像进行比较,看看其中一条是否使用完全相同的指令构建。否则,缓存将无效

· 在大多数情况下,只需将Dockerfile中的指令与其中一个子镜像进行比较就足够了。然而,某些指示需要更多的检查和解释。

· 对于ADD和COPY指令,将检查镜像中文件的内容,并且检查和校验每个文件 。最后修改时间和最后访问时间不会被校验。在缓存查找期间,将校验和与现有镜像中的校验和进行比较。如果文件中有任何更改,比如内容和元数据,那么缓存将无效。

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

一旦缓存失效,所有后续的Dockerfile命令都会生成新的镜像,而缓存则不被使用。

Dockerfile 指令

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

FROM

只要可能,使用当前的官方镜像作为你的镜像的基础镜像。我们推荐Alpine镜像【】,因为编写这个镜像是非常严格的,并且很小(目前小于5 MB),但仍然是一个完整的Linux发行版。

LABEL

您可以将标签添加到镜像中,以帮助按项目组织镜像、记录许可信息、帮助实现自动化或出于其他原因。对于每个标签,用LABEL标记开始,用一个或者多个键值对 。下面的示例显示了不同的可接受格式。解释性注释是内联的。

必须引用带空格的字符串,否则必须转义空格。内部引号字符(“)也必须转义。

# Set one or more individual labels
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之前,建议将所有标签合并到一个标签指令中,以防止创建额外的层。这不再需要,但是仍然支持组合标签。

# Set multiple labels on one line
LABEL com.example.version="0.0.1-beta" com.example.release-date="2015-02-12"
 

上面的这个例子还可以写成下面这样:

# Set multiple labels at once, using line-continuation characters to break long lines
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"
RUN
 

使用反斜杠(\) 来分隔独立的命令行可以使RUN命令更有可读性、易于维护。

APT-GET

Apt-get 命令是很多Docker经常使用的命令。因为,他是安装各种包必须使用的命令。

避免运行apt-get升级和distl -upgrade,因为来自父镜像的许多”基本”包无法在非特权容器中升级。如果父镜像中包含的包过期了,请联系它的维护人员。如果您知道有一个特定的包foo需要更新,那么使用apt-get install -y foo自动更新。

始终将RUN apt-get update与apt-get install组合在同一个RUN语句中。例如:

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

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

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

当构建完镜像后,所有的层都已经被缓存了,假设之后你修改了apt-get install 增加了其他的包:

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

Docker将初始指令和修改后的指令视为相同的,并重用前面步骤中的缓存。因此,apt-get更新不会执行,因为构建使用缓存的版本。由于apt-get更新没有运行,您的构建可能会得到一个过时版本的curl和nginx包。

使用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.*
 

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

下面是一个格式良好的运行指令,演示了所有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指定了一个新的版本。如果之前的镜像安装的是一个旧的版本。apt-get update 会导致缓存失效,从而安装新的版本。

在这样的条件下,当你清除apt缓存并且移除/var/lib/apt/lists 目录,来减小文件尺寸。当RUN 声明以apt-get update开始,在执行apt-get install的时候,缓存依然会被刷新。

注:

Debian和ubuntu的官方镜像会自动运行apt-get clecn命令。所以不需要显示调用。

使用管道

有些运行命令依赖于使用管道字符(|)将一个命令的输出管道到另一个命令的能力,如下例所示:

RUN wget -O -  | wc -l > /number
 

Docker使用/bin/sh -c解释器执行这些命令,解释器只计算管道中最后一个操作的退出代码来确定是否成功。在上面的示例中,只要wc -l命令成功,即使wget命令失败,这个构建步骤就会成功并生成一个新映像。

如果您希望命令在管道中的任何阶段由于错误而失败,请预先设置 -o pipefail && ,以确保意外错误防止构建意外成功。例如:

RUN set -o pipefail && wget -O -  | wc -l > /number
 

注:

不是所有的 shell 都支持 –o pipfail 选项

在基于debian的镜像上使用dash shell的情况下,可以考虑使用exec形式的RUN显式地选择一个支持pipefail选项的shell。例如:

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

CMD

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 – python这样的东西时,您将被放入一个可用的shell中,准备就绪。CMD应该很少与ENTRYPOINT一起以CMD [“param”, “param”]的方式使用,除非您和您的预期用户已经非常熟悉ENTRYPOINT的工作方式。

EXPOSE

EXPOSE指令指示容器监听连接的端口。因此,您应该为您的应用程序使用公共的、传统的端口。例如,包含Apache web服务器的镜像使用 80端口,而包含MongoDB的映像将使用 27017 端口,以此类推。

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

ENV

为了使新软件更容易运行,可以使用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  | 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
$ docker run --rm test sh -c '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 test sh -c 'echo $ADMIN_USER'
 

ADD 或者COPY

虽然ADD和COPY在功能上是相似的,但是一般来说,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获取包;您应该使用curl或wget来代替。这样,你可以删除你不再需要的文件后,他们已经被提取出来,你不需要添加另一层在您的镜像。例如,你应该避免做以下事情:

ADD  /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  \
| tar -xJC /usr/src/things \
&& make -C /usr/src/things all
 

如果不需要提取tar (文件、目录)的话,应该始终使用COPY。

ENTRYPOINT

ENTRYPOINT的最佳用法是设置镜像的主命令,允许像运行该命令一样运行该镜像(然后使用CMD作为默认标志)。

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

ENTRYPOINT [“s3cmd”]

CMD [“–help”]

现在,这个镜像可以像这样运行:

$ docker run s3cmd
 

也可以传参数执行:

$ docker run s3cmd ls s3://mybucket
 

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

ENTRYPOINT指令也可以与helper脚本结合使用,允许它以类似于上面命令的方式运行,即使在启动工具时可能需要不止一个步骤。

例如,Postgres官方镜像使用以下脚本作为其入口点:

#!/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,这样,PG会结构linux的任何信号。

helper脚本被复制到容器中,并在容器开始时通过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
 

VOLUME

卷指令应该用于公开由docker容器创建的任何数据库存储区域、配置存储或文件/文件夹。强烈建议对镜像的任何可变和或用户可服务的部分使用VOLUME。

USER

如果服务可以在没有特权的情况下运行,请使用USER将其更改为非root用户。首先在Dockerfile中创建用户和组,使用类似于RUN groupadd -r postgres && useradd——no-log-init -r -g postgres postgres的东西。

镜像中的用户和组被分配一个不确定的UID/GID,因为”下一个”UID/GID被分配,而不考虑镜像的重建。因此,如果它是必须要使用的,您应该分配一个显式的UID/GID。

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

避免安装或使用sudo,因为它具有不可预知的TTY和信号转发行为,可能会导致问题。如果您绝对需要类似于sudo的功能,比如将守护进程初始化为根进程,但以非根进程的形式运行它,那么可以考虑使用”gosu”。

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

WORKER

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

ONBUILD

ONBUILD命令在当前Dockerfile构建完成后执行。ONBUILD在从当前镜像派生的任何子镜像中执行。将ONBUILD命令看作是父Dockerfile给子Dockerfile的一条指令。

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

ONBUILD对于将从给定镜像构建的镜像非常有用。例如,您可以对一个语言堆栈镜像使用ONBUILD,该镜像可以在Dockerfile中构建用该语言编写的任意用户软件,正如您可以在Ruby的ONBUILD变体中看到的那样。

使用ONBUILD构建的镜像应该有一个单独的标记,例如:ruby:1.9-onbuild或ruby:2.0-onbuild。

在ONBUILD中添加或复制时要小心。如果新构建的上下文缺少正在添加的资源,则”onbuild”镜像将灾难性地失败。如上面建议的那样,添加一个单独的标记,通过允许Dockerfile作者做出选择,可以帮助缓解这种情况。

相关文章