Docker 开发最佳实践
整理 Docker 下最佳的开发模式用于更好的构建应用程序。
编写 Dockerfile 最佳实践
本节涵盖了构建高效镜像的推荐最佳实践和方法。
Dockerfile
Docker 通过从一个包含构建给定镜像所需的所有命令的文本文件中读取指令来自动构建镜像。Dockerfile
遵循特定格式和指令集,可以在 Dockerfile 参考中查找。
一个 Docker 镜像由只读层组成,每个层代表一个 Dockerfile 指令。这些层是堆叠的,每一层都是前一层变更的增量。考虑以下 Dockerfile
:
# syntax=docker/dockerfile:1
FROM ubuntu:18.04
COPY . /app
RUN make /app
CMD python /app/app.py
每条指令创建一个层:
FROM
从ubuntu:18.04
Docker 父镜像创建一个层。COPY
从 Docker 客户端的当前目录添加文件。RUN
使用make
.CMD
指定要在容器中运行的命令。
当我们运行一个镜像并生成一个容器时,会在底层之上添加一个新的*可写层(“容器层”)。*对正在运行的容器所做的所有更改,例如写入新文件、修改现有文件和删除文件,都会写入此可写容器层。
有关镜像层(以及 Docker 如何构建和存储镜像)的更多信息,请参阅 关于存储驱动程序。
一般指南和建议
创建临时容器
由我们 Dockerfile
定义的镜像应该生成尽可能短暂的容器。“短暂”是指容器可以停止和销毁,然后用绝对最小的设置和配置重建和替换。
请参阅十二因素应用程序方法下的进程, 以了解以这种无状态方式运行容器的动机。
了解构建上下文
当我们发出 docker build
命令时,当前工作目录称为 build context - 构建上下文。默认情况下,假定 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 .
将
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
管道化来构建镜像。在不将 Dockerfile 写入磁盘的情况下,或者在生成 Dockerfile 且不应在之后持续存在的情况下,通过 stdin
将 Dockerfile
管道化对于执行一次性构建非常有用。
为方便起见,本节中的示例使用Here 文档,但也可以使用任何在 stdin 上提供 Dockerfile 的方法。
例如,以下命令是等效的:
echo -e 'FROM busybox\nRUN echo "hello world"' | docker build -
docker build -<<EOF FROM busybox RUN echo "hello world" EOF
你可以用自己喜欢的方法或最适合自己的用例的方法替换这些示例。
使用 Dockerfile 从 stdin 构建映像,而不发送构建上下文
使用此语法利用 Dockerfile
从 stdin
构建镜像,而无需发送其他文件作为构建上下文。连字符 (-
) 占据 PATH 的位置,并指示 Docker 从 stdin
而不是目录读取构建上下文(这里仅包含 Dockerfile):
docker build [OPTIONS] -
以下示例使用 Dockerfile 从 stdin
构建镜像。没有文件作为构建上下文发送到守护进程。
docker build -t myimage:latest -<<EOF
FROM busybox
RUN echo "hello world"
EOF
在 Dockerfile 不需要将文件复制到镜像中以提高构建速度的情况下,省略构建上下文可能很有用,因为没有文件需要发送到守护进程。
如果想通过从构建上下文中排除某些文件来提高构建速度,请参阅使用 .dockerignore 排除。
ℹ️ 注意:如果使用此语法,尝试构建使用
COPY
或ADD
的 Dockerfile 将失败。以下示例说明了这一点:# 创建一个工作目录 mkdir example cd example # 创建示例文件 touch somefile.txt docker build -t myimage:latest -<<EOF FROM busybox COPY somefile.txt ./ RUN cat /somefile.txt EOF # 观察构建失败 ... 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
构建镜像,该 Dockerfile 使用 here 文档通过 stdin 传递。
# 创建一个工作目录
mkdir example
cd example
# 创建示例文件
touch somefile.txt
# 使用当前目录作为上下文构建镜像,并通过 stdin 传递一个 Dockerfile
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 构建而不维护自己的存储库分支的情况下,此语法可能很有用。
docker build -t myimage:latest -f- https://github.com/docker-library/hello-world.git <<EOF
FROM busybox
COPY hello.c ./
EOF
使用 .dockerignore 排除
要排除与构建无关的文件,请使用 .dockerignore
文件。此文件支持类似于 .gitignore
文件的排除模式。有关如何创建的信息,请参阅 .dockerignore 文件。
使用多阶段构建
多阶段构建允许您大幅减小最终镜像的大小,而无需减少中间层和文件的数量。
由于镜像是在构建过程的最后阶段构建的,因此您可以通过利用构建缓存来最小化镜像层。
例如,如果您的构建包含多个层,您可以将它们按不太频繁更改(以确保构建缓存可重用)排序到更频繁更改:
- 安装构建应用程序所需的工具
- 安装或更新库依赖项
- 生成您的应用程序
Go 应用程序的 Dockerfile 可能如下所示:
# syntax=docker/dockerfile:1
FROM golang:1.16-alpine AS build
# 安装项目所需的工具
# 运行 `docker build --no-cache .` 以更新依赖项
RUN apk add --no-cache git
RUN go get github.com/golang/dep/cmd/dep
# 使用 Gopkg.toml 和 Gopkg.lock 列出项目依赖项
# 这些层仅在更新 Gopkg 文件时重新构建
COPY Gopkg.lock Gopkg.toml /go/src/project/
WORKDIR /go/src/project/
# 安装库依赖项
RUN dep ensure -vendor-only
# 复制整个项目并构建它
# 当项目目录中的文件更改时,将重建该层
COPY . /go/src/project/
RUN go build -o /bin/project
# 这会产生单层图像
FROM scratch
COPY --from=build /bin/project /bin/project
ENTRYPOINT ["/bin/project"]
CMD ["--help"]
不要安装不必要的软件包
为了减少复杂性、依赖关系、文件大小和构建时间,请避免安装额外或不必要的软件包。例如,您不需要在数据库镜像中包含文本编辑器。
解耦应用程序
每个容器应该只有一个关注点。将应用程序解耦到多个容器中,可以更轻松地水平扩展和重用容器。例如,一个 Web 应用程序技术栈可能由三个独立的容器组成,每个容器都有自己独特的镜像,以分离的方式管理 Web 应用程序、数据库和内存缓存。
将每个容器限制为一个进程是一个很好的经验法则,但这不是一个硬性规定。例如,不仅可以 使用 init 进程生成容器,某些程序可能会自行生成其他进程。例如,Celery 可以产生多个工作进程,而 Apache 可以为每个请求创建一个进程。
使用您的最佳判断来保持容器尽可能干净和模块化。如果容器相互依赖,您可以使用 Docker 容器网络来确保这些容器可以通信。
尽量减少层数
在旧版本的 Docker 中,尽量减少镜像中的层数以确保它们的性能非常重要。新版本添加了以下功能以减少此限制:
- 只有
RUN
,COPY
,ADD
指令创建层。其他指令创建临时中间镜像,并且不增加构建的大小。 - 在可能的情况下,使用多阶段构建,并且只将您需要的工件复制到最终镜像中。这允许您在中间构建阶段包含工具和调试信息,而不会增加最终镜像的大小。
对多行参数进行排序
只要有可能,通过按字母数字排序多行参数来简化以后的更改。这有助于避免重复包并使列表更容易更新。这也使 PR 更容易阅读和审查。在反斜杠 (\
) 前添加一个空格也有帮助。
下面是 buildpack-deps
镜像中的一个示例:
RUN apt-get update && apt-get install -y \
bzr \
cvs \
git \
mercurial \
subversion \
&& rm -rf /var/lib/apt/lists/*
利用构建缓存
构建镜像时,Docker 会逐步执行 Dockerfile
中的指令,并按照指定的顺序执行每个指令。在检查每条指令时,Docker 在其缓存中查找可以重用的现有镜像,而不是创建新的(重复)镜像。
如果您根本不想使用缓存,可以使用命令 docker build
上的选项 --no-cache=true
。但是,如果您确实让 Docker 使用它的缓存,重要的是要了解它何时可以找到匹配的镜像,何时不能找到匹配的镜像。Docker 遵循的基本规则概述如下:
- 从已经在缓存中的父镜像开始,将下一条指令与从该基础镜像派生的所有子镜像进行比较,以查看其中一个是否是使用完全相同的指令构建的。如果不是,则缓存无效。
- 在大多数情况下,只需将
Dockerfile
中的指令与其中一个子镜像进行比较就足够了。但是,某些指令需要更多的检查和解释。 - 对于
ADD
和COPY
指令,检查镜像中文件的内容并为每个文件计算校验和。这些校验和中不考虑文件的最后修改时间和最后访问时间。在缓存查找期间,将校验和与现有图像中的校验和进行比较。如果文件中有任何更改,例如内容和元数据,则缓存无效。 - 除了
ADD
和COPY
命令,缓存检查不会查看容器中的文件来确定缓存匹配。例如,在处理RUN apt-get -y update
命令时,不会检查容器中更新的文件以确定是否存在缓存命中。在这种情况下,只有命令字符串本身用于查找匹配项。
一旦缓存失效,所有后续 Dockerfile
命令都会生成新的镜像并且缓存不会被使用。
Dockerfile 指令集
这些建议旨在帮助您创建高效且可维护的 Dockerfile
。
FROM
尽可能使用当前的官方镜像作为镜像的基础。我们推荐使用 Alpine 镜像,因为它受到严格控制且体积小(目前低于 6 MB),同时仍然是一个完整的 Linux 发行版。
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。
RUN
将长或复杂的 RUN
语句拆分为用反斜杠分隔的多行,以使您的 Dockerfile
更具可读性、可理解性和可维护性。
apt-get
RUN
最常见的用例可能是 apt-get
应用程序。因为它会安装软件包,所以 RUN apt-get
命令有几个需要注意的问题。
始终在同一 RUN
语句中将 RUN apt-get update
与 apt-get install
结合使用。例如:
RUN apt-get update && apt-get install -y \
package-bar \
package-baz \
package-foo \
&& rm -rf /var/lib/apt/lists/*
在 RUN
语句中单独使用 apt-get update
会导致缓存问题和后续 apt-get install
指令失败。例如,假设您有一个 Dockerfile:
# syntax=docker/dockerfile:1
FROM ubuntu:18.04
RUN apt-get update
RUN apt-get install -y curl
构建镜像后,所有层都在 Docker 缓存中。假设您稍后通过 apt-get install
添加额外的包进行修改:
# syntax=docker/dockerfile:1
FROM ubuntu:18.04
RUN apt-get update
RUN apt-get install -y curl nginx
Docker 将初始指令和修改后的指令视为相同,并重用之前步骤中的缓存。结果,由于构建使用缓存版本,apt-get update
因此*未执行。*由于 apt-get update
没有运行,您的构建可能会获得 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.*
版本固定强制构建检索特定版本,而不管缓存中有什么。这种技术还可以减少由于所需包的意外更改而导致的故障。
下面是一个格式良好的 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
,因此不需要显式调用。
使用管道
某些 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
选项。在基于 Debian 的映像上的
dash
shell 等情况下,请考虑使用RUN
的 exec 形式显式选择支持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 -it python
,你会被放到一个可用的 shell 中,准备就绪。CMD
应该很少以 CMD ["param", "param"]
的方式与 ENTRYPOINT
一起使用,除非您和您的预期用户已经非常熟悉 ENTRYPOINT
的工作方式。
EXPOSE
该 EXPOSE
指令指示容器侦听连接的端口。因此,您应该为您的应用程序使用通用的传统端口。例如,包含 Apache Web 服务器的镜像将使用 EXPOSE 80
,而包含 MongoDB 的镜像将使用 EXPOSE 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 https://example.com/postgres-$PG_VERSION.tar.xz | tar -xJC /usr/src/postgres && …
ENV PATH=/usr/local/postgres-$PG_MAJOR/bin:$PATH
类似于在程序中使用常量变量(与硬编码值相反),这种方法允许您更改单个 ENV
指令以自动神奇地更改容器中的软件版本。
ENV
每行创建一个新的中间层,就像 RUN
命令一样。这意味着即使您在将来的层中取消设置环境变量,它仍然会保留在该层中,并且可以转储其值。您可以通过创建如下所示的 Dockerfile 来测试它,然后构建它。
# syntax=docker/dockerfile:1
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 build
命令也会失败。这通常是个好主意。使用 \
作为 Linux Dockerfiles 的续行符可以提高可读性。您还可以将所有命令放入一个 shell 脚本,并让 RUN
命令运行该 shell 脚本。
# syntax=docker/dockerfile:1
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 or COPY
虽然 ADD
和 COPY
在功能上相似,但一般来说,COPY
是优选的。那是因为它比 ADD
更透明。COPY
仅支持将本地文件基本复制到容器中,同时 ADD
具有一些并非立即显而易见的功能(如仅本地 tar 提取和远程 URL 支持)。因此,最好的用途 ADD
是将本地 tar 文件自动提取到镜像中,如 ADD rootfs.tar.xz /
。
如果您有多个 Dockerfile
步骤使用来自您的上下文的不同文件,请单独 COPY
它们,而不是一次全部复制。这确保了每个步骤的构建缓存仅在特定需要的文件发生更改时才失效(强制重新运行该步骤)。
例如:
COPY requirements.txt /tmp/
RUN pip install --requirement /tmp/requirements.txt
COPY . /tmp/
与在其前面放置 COPY . /tmp/
相比,该 RUN
步骤的缓存失效次数更少。
因为镜像大小很重要,所以强烈建议不要使用 ADD
从远程 URL 获取包;你应该使用 curl
或 wget
代替。这样,您可以在提取文件后删除不再需要的文件,而不必在镜像中添加另一层。例如,您应该避免执行以下操作:
ADD https://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 https://example.com/big.tar.xz \
| tar -xJC /usr/src/things \
&& make -C /usr/src/things all
对于不需要 ADD
的 tar 自动提取功能的其他项目(文件、目录),您应该始终使用 COPY
。
ENTRYPOINT
ENTRYPOINT
最好的用途是设置镜像的主命令,允许该镜像像该命令一样运行(然后使用 CMD
作为默认参数)。
让我们从命令行工具 s3cmd
的图像示例开始:
ENTRYPOINT ["s3cmd"]
CMD ["--help"]
现在可以像这样运行镜像以显示命令的帮助:
$ 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
此脚本使用 Bash
exec
命令 ,以便最终运行的应用程序成为容器的 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
VOLUME
该 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。因此,如果它很重要,您应该分配一个显式的 UID/GID。
由于 Go 存档 /tar 包处理稀疏文件的一个未解决的错误,尝试在 Docker 容器内创建具有非常大 UID 的用户可能会导致磁盘耗尽,因为
/var/log/faillog
在容器层中填充了 NULL (\0) 字符。一种解决方法是将--no-log-init
标志传递给 useradd。Debian/Ubuntuadduser
包装器不支持此标志。
避免安装或使用 sudo
,因为它具有不可预测的 TTY 和可能导致问题的信号转发行为。如果您绝对需要类似于 sudo
的功能,例如将守护程序初始化为 root
但以非 root
方式运行,请考虑使用“gosu”。
最后,为了减少层和复杂性,避免 USER
频繁来回切换。
WORKDIR
为了清晰和可靠,您应该始终为您的 WORKDIR
使用绝对路径。此外,您应该使用 WORKDIR
而不是像 RUN cd ... && do-something
之类的大量指令,这些指令难以阅读、排除故障和维护。
ONBUILD
当前 Dockerfile 构建完成后,将执行 ONBUILD
命令。 ONBUILD
在从 FROM
当前镜像派生的任何子镜像中执行。将 ONBUILD
命令视为父 Dockerfile
给子 Dockerfile
的指令。
Docker 构建在子 Dockerfile
中的任何命令之前执行 ONBUILD
命令。
ONBUILD
对于将从给定镜像构建的镜像很有用。例如,您可以将 ONBUILD
用于在 Dockerfile
中构建以该语言编写的任意用户软件的语言堆栈镜像,正如您在 Ruby 的 ONBUILD 变体中看到的那样。
使用 ONBUILD
构建的图像应该有一个单独的标签,例如:ruby:1.9-onbuild
或 ruby:2.0-onbuild
。
将 ADD
或 COPY
放入 ONBUILD
时要小心。如果新构建的上下文缺少要添加的资源,则“onbuild”映像会灾难性地失败。如上所述,添加一个单独的标签有助于通过允许 Dockerfile
作者做出选择来缓解这种情况。
Docker 官方镜像示例
这些官方镜像具有示例性 Dockerfile
: