Dockerfile 指令的最佳实践
这些建议旨在帮助您创建高效且可维护的 Dockerfile。
从
只要有可能,请使用当前的官方图像作为图像的基础。 Docker 推荐使用 Alpine 镜像,因为它受到严格控制且尺寸较小(目前低于 6 MB),同时仍然是一个完整的 Linux 发行版。
有关该FROM
指令的更多信息,请参阅
FROM 指令的 Dockerfile 参考。
标签
您可以向图像添加标签,以帮助按项目组织图像、记录许可信息、帮助实现自动化或出于其他原因。对于每个标签,添加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 之前,建议将所有标签合并到一条LABEL
指令中,以防止创建额外的层。这不再是必要的,但仍然支持组合标签。例如:
# 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"
有关可接受的标签键和值的指南,请参阅 了解对象标签。有关查询标签的信息,请参阅管理对象标签中与过滤相关的条目 。另请参阅 Dockerfile 参考中的LABEL 。
运行
将长或复杂的RUN
语句拆分为多行,并用反斜杠分隔,以使 Dockerfile 更易于阅读、理解和维护。
有关RUN 指令的更多信息RUN
,请参阅
Dockerfile 参考。
apt-get
最常见的用例可能RUN
是apt-get
.由于该RUN apt-get
命令会安装软件包,因此需要注意一些违反直觉的行为。
始终在同一个语句中结合RUN apt-get update
使用
。例如:apt-get install
RUN
RUN apt-get update && apt-get install -y \
package-bar \
package-baz \
package-foo \
&& rm -rf /var/lib/apt/lists/*
apt-get update
在语句中单独使用RUN
会导致缓存问题和后续apt-get install
指令失败。例如,此问题会出现在以下 Dockerfile 中:
# syntax=docker/dockerfile:1
FROM ubuntu:22.04
RUN apt-get update
RUN apt-get install -y curl
构建镜像后,所有层都在 Docker 缓存中。假设您稍后apt-get install
通过添加额外的包进行修改,如以下 Dockerfile 所示:
# syntax=docker/dockerfile:1
FROM ubuntu:22.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
并确保安装新版本。在每行列出包还可以防止包重复中的错误。
此外,当您通过删除 apt 缓存来清理/var/lib/apt/lists
它时,会减小图像大小,因为 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
选项。
dash
对于基于 Debian 的映像上的 shell等情况,请考虑使用exec形式RUN
来显式选择支持该选项的 shellpipefail
。例如:RUN ["/bin/bash", "-c", "set -o pipefail && wget -O - https://some.site | wc -l > /number"]
指令管理系统
该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
有关 CMD 指令的更多信息CMD
,请参阅
Dockerfile 参考。
暴露
该EXPOSE
指令指示容器侦听连接的端口。因此,您应该为您的应用程序使用通用的传统端口。例如,包含 Apache Web 服务器的映像将使用EXPOSE 80
,而包含 MongoDB 的映像将使用EXPOSE 27017
等等。
对于外部访问,您的用户可以docker run
使用指示如何将指定端口映射到他们选择的端口的标志来执行。对于容器链接,Docker 提供了从接收容器到源容器的路径的环境变量(例如,MYSQL_PORT_3306_TCP
)。
有关 EXPOSE 指令的更多信息EXPOSE
,请参阅
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 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
为了防止这种情况发生,并真正取消设置环境变量,请使用RUN
带有 shell 命令的命令,以在单个层中设置、使用和取消设置变量。您可以用;
或分隔命令&&
。如果您使用第二种方法,并且其中一个命令失败,则该命令docker build
也会失败。这通常是个好主意。用作\
Linux Dockerfile 的续行符可提高可读性。您还可以将所有命令放入 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'
有关 ENV 指令的更多信息ENV
,请参阅
Dockerfile 参考。
添加或复制
ADD
并且COPY
功能相似。支持从构建上下文或多阶段构建
中的阶段
COPY
将文件基本复制到容器中
。
支持从远程 HTTPS 和 Git URL 获取文件的功能,以及从构建上下文添加文件时自动提取 tar 文件的功能。ADD
您最想用于COPY
在多阶段构建中将文件从一个阶段复制到另一个阶段。如果您需要临时将文件从构建上下文添加到容器中以执行RUN
指令,通常可以COPY
使用绑定挂载来替换该指令。例如,要临时添加requirements.txt
指令文件RUN pip install
:
RUN --mount=type=bind,source=requirements.txt,target=/tmp/requirements.txt \
pip install --requirement /tmp/requirements.txt
COPY
绑定挂载比将构建上下文中的文件包含在容器中更有效。请注意,绑定安装的文件只是为单个RUN
指令临时添加的,并且不会保留在最终映像中。如果您需要在最终映像中包含构建上下文中的文件,请使用COPY
.
ADD
当您需要下载远程工件作为构建的一部分时,该说明最适合。比使用和ADD
等手动添加文件更好,因为它可以确保更精确的构建缓存。
还内置了对远程资源校验和验证的支持,以及用于解析
Git URL中的分支、标签和子目录的协议。wget
tar
ADD
以下示例用于ADD
下载 .NET 安装程序。结合多阶段构建,只有.NET运行时保留在最后阶段,没有中间文件。
# syntax=docker/dockerfile:1
FROM scratch AS src
ARG DOTNET_VERSION=8.0.0-preview.6.23329.7
ADD --checksum=sha256:270d731bd08040c6a3228115de1f74b91cf441c584139ff8f8f6503447cebdbb \
https://dotnetcli.azureedge.net/dotnet/Runtime/$DOTNET_VERSION/dotnet-runtime-$DOTNET_VERSION-linux-arm64.tar.gz /dotnet.tar.gz
FROM mcr.microsoft.com/dotnet/runtime-deps:8.0.0-preview.6-bookworm-slim-arm64v8 AS installer
# Retrieve .NET Runtime
RUN --mount=from=src,target=/src <<EOF
mkdir -p /dotnet
tar -oxzf /src/dotnet.tar.gz -C /dotnet
EOF
FROM mcr.microsoft.com/dotnet/runtime-deps:8.0.0-preview.6-bookworm-slim-arm64v8
COPY --from=installer /dotnet /usr/share/dotnet
RUN ln -s /usr/share/dotnet/dotnet /usr/bin/dotnet
有关ADD
或 的更多信息COPY
,请参阅以下内容:
入口点
最好的用途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 "$@"
该脚本使用
Bashexec
命令,以便最终运行的应用程序成为容器的 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
有关 ENTRYPOINT 指令的更多信息ENTRYPOINT
,请参阅
Dockerfile 参考。
体积
您应该使用该VOLUME
指令公开 Docker 容器创建的任何数据库存储区域、配置存储或文件和文件夹。强烈建议您使用VOLUME
图像的可变或用户可服务部分的任意组合。
有关 VOLUME 指令的更多信息VOLUME
,请参阅
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 archive/tar 包处理稀疏文件时存在未解决的错误,尝试在 Docker 容器内创建具有非常大的 UID 的用户可能会导致磁盘耗尽,因为
/var/log/faillog
容器层中充满了 NULL (\0) 字符。解决方法是将--no-log-init
标志传递给 useradd。 Debian/Ubuntuadduser
包装器不支持此标志。
避免安装或使用,sudo
因为它具有不可预测的 TTY 和信号转发行为,可能会导致问题。如果您绝对需要类似于的功能sudo
,例如将守护进程初始化为非root
守护进程root
,请考虑使用
“gosu”。
最后,为了减少层次和复杂性,避免USER
频繁来回切换。
有关USER 指令的更多信息USER
,请参阅
Dockerfile 参考。
工作目录
为了清晰和可靠,您应该始终为您的
WORKDIR
.另外,您应该使用WORKDIR
而不是使用大量的指令RUN cd … && do-something
,例如难以阅读、排除故障和维护的指令。
有关 WORKDIR 指令的更多信息WORKDIR
,请参阅
Dockerfile 参考。
建设
ONBUILD
当前 Dockerfile 构建完成后执行命令
。在派生当前图像ONBUILD
的任何子图像中执行。FROM
将该ONBUILD
命令视为父 Dockerfile 向子 Dockerfile 发出的指令。
Docker 构建ONBUILD
在子 Dockerfile 中的任何命令之前执行命令。
ONBUILD
对于要构建FROM
给定图像的图像很有用。例如,您可以使用ONBUILD
语言堆栈映像来构建在 Dockerfile 中以该语言编写的任意用户软件,正如您在
Ruby 的ONBUILD
变体中看到的那样。
使用构建的图像ONBUILD
应该有一个单独的标签。例如,
ruby:1.9-onbuild
或ruby:2.0-onbuild
。
ADD
放入或COPY
放入时要小心ONBUILD
。如果新构建的上下文缺少正在添加的资源,则 onbuild 映像会发生灾难性的失败。按照上面的建议添加单独的标签,可以让 Dockerfile 作者做出选择,从而有助于缓解这种情况。
有关 ONBUILD 指令的更多信息ONBUILD
,请参阅
Dockerfile 参考。