本文档介绍了构建高效镜像的最佳实践和方法。
Docker通过从Dockerfile(按顺序包含构建给定镜像所需的所有命令的文本文件)读取命令来自动构建镜像。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构建命令时,当前工作目录称为构建上下文。默认情况下, Dockerfile在当前目录,但是您可以使用file标志(-f)指定一个不同的位置。无论Dockerfile实际位于何处,当前目录中文件和目录的所有递归内容都作为构建上下文发送到Docker守护进程。
构建上下文:
为构建上下文创建一个目录并将cd放入其中。将”hello”写入一个名为hello的文本文件中,并创建一个运行cat的Dockerfile。从构建上下文中构建镜像(.):
mkdir myproject && cd myproject echo "hello" > hello echo -e "FROM busyboxnCOPY /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 busyboxnRUN 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- https://github.com/docker-library/hello-world.git <<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版本中,保持层数的最少是非常重要的s3cmd,因为要保证性能。
为了减少这样的限制,增加了一下的特性:
· 只有指令RUNs3cmd,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 - https://some.site | wc -l > /number
Docker使用/bin/sh -c解释器执行这些命令,解释器只计算管道中最后一个操作的退出代码来确定是否成功。在上面的示例中,只要wc -l命令成功,即使wget命令失败,这个构建步骤就会成功并生成一个新映像。
如果您希望命令在管道中的任何阶段由于错误而失败,请预先设置-o pipefail &&,以确保意外错误防止构建意外成功。例如:
RUN set -o pipefail && wget -O - https://some.site | wc -l > /number
注:
不是所有的shell都支持 –o pipfail 选项
在基于debian的镜像上使用dash shell的情况下,可以考虑使用exec形式的RUN显式地选择一个支持pipefail选项的shell。例如:
RUN ["/bin/bash", "-c", "set -o pipefail && wget -O - https://some.site | 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 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 $ 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 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
如果不需要提取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()字符。一个解决方案是将——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作者做出选择,可以帮助缓解这种情况。
限时特惠:本站每日持续更新海量设计资源,一年会员只需29.9元,全站资源免费下载
站长微信:ziyuanshu688