编写Dockerfiles的优秀实践

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

编写Dockerfiles的优秀实践

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

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

  1. FROM ubuntu:18.04 
  2. COPY . /app 
  3. RUN make /app 
  4. 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。从构建上下文中构建镜像(.):

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

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

  1. mkdir -p dockerfiles context 
  2. mv Dockerfile dockerfiles && mv hello context 
  3. docker build --no-cache -t helloapp:v2 -f dockerfiles/Dockerfile context 

无意中包含了构建镜像所不需要的文件,会导致构建上下文和镜像大小变大。这可以增加构建镜像的时间、拖放镜像的时间和容器运行时大小。要查看构建上下文的大小,请在构建Dockerfile时查看类似这样的消息:

  1. Sending build context to Docker daemon 187.8MB 

通过stdin使用Dockerfile 管道

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

为了方便起见,本节中的示例使用here文档【http://tldp.org/LDP/abs/html/here-docs.html】,但是可以使用在stdin上提供Dockerfile的任何方法。

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

  1. echo -e 'FROM busybox\nRUN echo "hello world"' | docker build - 
  2. docker build -<<EOF 
  3. FROM busybox 
  4. RUN echo "hello world" 
  5. EOF 

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

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

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

  1. docker build [OPTIONS] – 

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

  1. docker build -t myimage:latest -<<EOF 
  2. FROM busybox 
  3. RUN echo "hello world" 
  4. EOF 

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

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

  1. create a directory to work in 
  2. mkdir example 
  3. cd example 
  4. create an example file 
  5. touch somefile.txt 
  6. docker build -t myimage:latest -<<EOF 
  7. FROM busybox 
  8. COPY somefile.txt . 
  9. RUN cat /somefile.txt 
  10. EOF 
  11. # observe that the build fails 
  12. ... 
  13. Step 2/3 : COPY somefile.txt . 
  14. 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:

  1. docker build [OPTIONS] -f- PATH 

下面这个例子我们用当前目录作为构建上下文,并且构建镜像用到的Dockerfile是通过stdin传进去的。例子在这里【http://tldp.org/LDP/abs/html/here-docs.html】

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

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

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

  1. docker build [OPTIONS] -f- PATH 

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

下面的示例使用来自stdin的Dockerfile构建一个镜像,并添加hello.c文件从git 仓里库【https://github.com/docker-library/hello-world】

  1. docker build -t myimage:latest -f- https://github.com/docker-library/hello-world.git <<EOF 
  2. FROM busybox 
  3. COPY hello.c . 
  4. EOF 
  5. Note: 

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

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

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

使用多级构建

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

因为镜像是在构建过程的最后阶段构建的,所以可以通过利用构建缓存最小化镜像层。【https://docs.docker.com/develop/develop-images/dockerfile_best-practices/#leverage-build-cache】

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

  • 安装构建应用程序所需的工具
  • 安装或者更改依赖的库
  • 生成应用

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

  1. FROM golang:1.11-alpine AS build 
  2. # Install tools required for project 
  3. # Run `docker build --no-cache .` to update dependencies 
  4. RUN apk add --no-cache git 
  5. RUN go get github.com/golang/dep/cmd/dep 
  6. # List project dependencies with Gopkg.toml and Gopkg.lock 
  7. # These layers are only re-built when Gopkg files are updated 
  8. COPY Gopkg.lock Gopkg.toml /go/src/project/ 
  9. WORKDIR /go/src/project/ 
  10. # Install library dependencies 
  11. RUN dep ensure -vendor-only 
  12. # Copy the entire project and build it 
  13. # This layer is rebuilt when a file changes in the project directory 
  14. COPY . /go/src/project/ 
  15. RUN go build -o /bin/project 
  16. # This results in a single layer image 
  17. FROM scratch 
  18. COPY --from=build /bin/project /bin/project 
  19. ENTRYPOINT ["/bin/project"
  20. CMD ["--help"

不安装不必要的包

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

解耦

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

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

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

保存最小数量的层

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

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

  • 只有指令RUN,COPY,ADD创建层。其他指令创建临时中间镜像,并且不增加构建的大小
  • 在可能的情况下,使用多阶段构建,并且只将您需要的工件复制到最终镜像中。这允许您在中间构建阶段包含工具和调试信息,而不需要增加最终映像的大小。

命令行参数排序

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

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

  1. RUN apt-get update && apt-get install -y \ 
  2. bzr \ 
  3. cvs \ 
  4. git \ 
  5. mercurial \ 
  6. 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镜像【https://hub.docker.com/_/alpine/】,因为编写这个镜像是非常严格的,并且很小(目前小于5 MB),但仍然是一个完整的Linux发行版。

LABEL

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

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

  1. Set one or more individual labels 
  2. LABEL com.example.version="0.0.1-beta" 
  3. LABEL vendor1="ACME Incorporated" 
  4. LABEL vendor2=ZENITH\ Incorporated 
  5. LABEL com.example.release-date="2015-02-12" 
  6. LABEL com.example.version.is-production="" 

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

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

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

  1. Set multiple labels at once, using line-continuation characters to break long lines 
  2. LABEL vendor=ACME\ Incorporated \ 
  3. com.example.is-beta= \ 
  4. com.example.is-production="" \ 
  5. com.example.version="0.0.1-beta" \ 
  6. com.example.release-date="2015-02-12" 
  7. 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语句中。例如:

  1. RUN apt-get update && apt-get install -y \ 
  2. package-bar \ 
  3. package-baz \ 
  4. package-foo 

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

  1. FROM ubuntu:18.04 
  2. RUN apt-get update 
  3. RUN apt-get install -y curl 

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

  1. FROM ubuntu:18.04 
  2. RUN apt-get update 
  3. RUN apt-get install -y curl nginx 

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

使用RUN apt-get update && apt-get install -y确保您的Dockerfile安装最新的包版本,而无需进一步编码或手动干预。这种技术称为"缓存破坏"。还可以通过指定包版本来实现缓存崩溃。这就是所谓的版本固定,例如:

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

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

下面是一个格式良好的运行指令,演示了所有apt-get 的优秀实践。

  1. RUN apt-get update && apt-get install -y \ 
  2. aufs-tools \ 
  3. automake \ 
  4. build-essential \ 
  5. curl \ 
  6. dpkg-sig \ 
  7. libcap-dev \ 
  8. libsqlite3-dev \ 
  9. mercurial \ 
  10. reprepro \ 
  11. ruby1.9.1 \ 
  12. ruby1.9.1-dev \ 
  13. s3cmd=1.1.* \ 
  14. && 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命令。所以不需要显示调用。

使用管道

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

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

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

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

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

注:

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

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

  1. 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还可以用来设置常用的版本号,以便更容易维护版本,如下例所示:

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

类似于在程序中使用常量变量(而不是硬编码值),这种方法允许您更改单个ENV指令,从而自动地在容器中神奇地弹出软件版本。

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

  1. FROM alpine 
  2. ENV ADMIN_USER="mark" 
  3. RUN echo $ADMIN_USER > ./mark 
  4. RUN unset ADMIN_USER 
  5. $ docker run --rm test sh -c 'echo $ADMIN_USER' 
  6. mark 

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

  1. FROM alpine 
  2. RUN export ADMIN_USER="mark" \ 
  3. && echo $ADMIN_USER > ./mark \ 
  4. && unset ADMIN_USER 
  5. CMD sh 
  6. 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步骤使用与上下文不同的文件,请分别复制它们,而不是一次全部复制。这确保只有在特定需要的文件发生更改时,每个步骤的构建缓存才会失效(强制重新运行该步骤)。

例如:

  1. COPY requirements.txt /tmp/ 
  2. RUN pip install --requirement /tmp/requirements.txt 
  3. COPY . /tmp/ 

将COPY . /tmp/放到RUN前面,会使缓存失效???

由于镜像的大小很重要,因此强烈反对使用ADD从远程url获取包;您应该使用curl或wget来代替。这样,你可以删除你不再需要的文件后,他们已经被提取出来,你不需要添加另一层在您的镜像。例如,你应该避免做以下事情:

  1. ADD http://example.com/big.tar.xz /usr/src/things/ 
  2. RUN tar -xJf /usr/src/things/big.tar.xz -C /usr/src/things 
  3. RUN make -C /usr/src/things all 
  4. 我们用下面的命令取代: 
  5. RUN mkdir -p /usr/src/things \ 
  6. && curl -SL http://example.com/big.tar.xz \ 
  7. | tar -xJC /usr/src/things \ 
  8. && make -C /usr/src/things all 

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

ENTRYPOINT

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

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

ENTRYPOINT ["s3cmd"]

CMD ["--help"]

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

  1. $ docker run s3cmd 

也可以传参数执行:

  1. $ docker run s3cmd ls s3://mybucket 

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

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

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

  1. #!/bin/bash 
  2. set -e 
  3. if [ "$1" = 'postgres' ]; then 
  4. chown -R postgres "$PGDATA" 
  5. if [ -z "$(ls -A "$PGDATA")" ]; then 
  6. gosu postgres initdb 
  7. fi 
  8. exec gosu postgres "$@" 
  9. fi 
  10. exec "$@" 

注:

设置应用的PID为1,这样,PG会结构linux的任何信号。

helper脚本被复制到容器中,并在容器开始时通过ENTRYPOINT运行:

  1. COPY ./docker-entrypoint.sh / 
  2. ENTRYPOINT ["/docker-entrypoint.sh"
  3. CMD ["postgres"

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

它可以简单地启动Postgres:

  1. $ docker run postgres 

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

  1. $ docker run postgres postgres –help 

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

  1. $ 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变体中看到的那样。

相关推荐