Skip to content

Dockerfile 指令的最佳实践

原文:https://docs.docker.com/develop/develop-images/instructions/

这些建议旨在帮助您创建高效且易于维护的 Dockerfile。

FROM

只要有可能,就使用当前的官方镜像作为您的镜像的基础。Docker 推荐使用 Alpine 镜像,因为它控制严格且体积小(目前低于 6 MB),同时仍然是一个完整的 Linux 发行版。

有关 FROM 指令的更多信息,请参阅 Dockerfile 参考手册中的 FROM 指令

LABEL

您可以为镜像添加标签,以帮助按项目组织镜像,记录许可信息,协助自动化,或出于其他原因。对于每个标签,添加以 LABEL 开始的行,并包含一个或多个键值对。以下示例显示了不同的可接受格式。解释性评论已内联包含。

包含空格的字符串必须被引号引起来,或者必须转义空格。内部引号字符(")也必须被转义。例如:

dockerfile
# 设置一个或多个单独的标签
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 指令中,以防止创建额外的层。这仍然是不必要的,但组合标签仍然受支持。例如:

dockerfile
# 在一行中设置多个标签
LABEL com.example.version="0.0.1-beta" com.example.release-date="2015-02-12"

上面的示例也可以写为:

dockerfile
# 一次设置多个标签,使用行续行字符来断行
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 更易读、更易理解和维护。

有关 RUN 的更多信息,请参阅 Dockerfile 参考手册中的 RUN 指令

apt-get

RUN 的最常见用例可能是应用 apt-get。因为它安装包,RUN apt-get 命令有几个非直观的行为需要注意。

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

dockerfile
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 中将出现此问题:

dockerfile
# syntax=docker/dockerfile:1

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

构建镜像后,所有层都在 Docker 缓存中。假设您稍后修改 `apt-get

install`,添加额外的包,如以下 Dockerfile 所示:

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,您的构建可能会获得 curlnginx 包的过时版本。

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

dockerfile
RUN apt-get update && apt-get install -y \
    package-bar \
    package-baz \
    package-foo=1.3.*

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

下面是一个格式良好的 RUN 指令,演示了所有的 apt-get 建议。

dockerfile
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 命令依赖于将一个命令的输出通过管道(|)传递给另一个命令的能力,如下例所示:

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

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

如果您希望由于管道中的任何阶段的错误而导致命令失败,可以在命令前加上 set -o pipefail &&,以确保预期外的错误会阻止构建意外成功。例如:

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

注意

并非所有的 shell 都支持 -o pipefail 选项。

在如 Debian 基础镜像上的 dash shell 中,考虑使用 RUNexec 形式显式选择支持 pipefail 选项的 shell。例如:

dockerfile
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 的工作方式。

有关 CMD 的更多信息,请参见 Dockerfile 参考手册中的 CMD 指令

EXPOSE

EXPOSE 指令表明容器监听连接的端口。因此,您应该使用您的应用程序的常见、传统端口。例如,包含 Apache 网络服务器的镜像将使用 EXPOSE 80,而包含 MongoDB 的镜像将使用 EXPOSE 27017 等。

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

有关 EXPOSE 的更多信息,请参见 Dockerfile 参考手册中的 EXPOSE 指令

ENV

为了使新软件更易于运行,您可以使用 ENV 更新软件容器安装的 PATH 环境变量。例如,ENV PATH=/usr/local/nginx/bin:$PATH 确保 CMD ["nginx"] 可以正常工作。

ENV 指令也有助于提供您想

要容器化的服务所需的特定环境变量,例如 Postgres 的 PGDATA

最后,ENV 还可以用来设置常用的版本号,以便于版本提升更容易维护,如下面的示例:

dockerfile
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 来测试这一点,然后构建它。

dockerfile
# syntax=docker/dockerfile:1
FROM alpine
ENV ADMIN_USER="mark"
RUN echo $ADMIN_USER > ./mark
RUN unset ADMIN_USER
console
docker run --rm test sh -c 'echo $ADMIN_USER'

mark

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

dockerfile
# syntax=docker/dockerfile:1
FROM alpine
RUN export ADMIN_USER="mark" \
    && echo $ADMIN_USER > ./mark \
    && unset ADMIN_USER
CMD sh
console
docker run --rm test sh -c 'echo $ADMIN_USER'

有关 ENV 的更多信息,请参见 Dockerfile 参考手册中的 ENV 指令

ADD 或 COPY

ADDCOPY 在功能上类似。COPY 支持从 构建上下文多阶段构建的一个阶段复制文件到容器。ADD 支持从远程 HTTPS 和 Git URL 获取文件的功能,以及在从构建上下文添加文件时自动提取 tar 文件。

你大多数情况下会想使用 COPY 用于在多阶段构建中从一个阶段向另一个阶段复制文件。如果你需要临时添加来自构建上下文的文件到容器中以执行 RUN 指令,通常可以用绑定挂载来代替 COPY 指令。例如,为 RUN pip install 指令临时添加 requirements.txt 文件:

dockerfile
RUN --mount=type=bind,source=requirements.txt,target=/tmp/requirements.txt \
    pip install --requirement /tmp/requirements.txt

绑定挂载比使用 COPY 将文件从构建上下文包含到容器中更有效。注意,绑定挂载的文件只是临时为单个 RUN 指令添加,并不在最终镜像中持久存在。如果你需要在最终镜像中包含来自构建上下文的文件,请使用 COPY

ADD 指令最适合在构建过程中需要下载远程工件的情况。ADD 优于手动添加文件,如使用 wgettar,因为它确保了更精确的构建缓存。ADD 还内置支持远程资源的校验和验证,以及从 Git URL 解析分支、标签和子目录的协议。

以下示例使用 ADD 下载 .NET 安装程序。与多阶段构建结合使用,最终阶段只剩下 .NET 运行时,没有中间文件。

dockerfile
# 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

有关 ADDCOPY 的更多信息,请参见以下内容:

ENTRYPOINT

ENTRYPOINT 最佳用途是设置镜像的主命令,使得该镜像可以像运行那个命令一样被运行,并且使用 CMD 作为默认的参数。

以下是用于命令行工具 s3cmd 的镜像的一个例子:

dockerfile
ENTRYPOINT ["s3cmd"]
CMD ["--help"]

你可以使用以下命令来运行镜像并显示命令的帮助:

console
docker run s3cmd

或者,你可以使用正确的参数来执行一个命令,如下例:

console
docker run s3cmd ls s3://mybucket

这是有用的,因为镜像名称可以双重作为二进制文件的引用,如上面的命令所示。

ENTRYPOINT 指令还可以与帮助脚本一起使用,允许它以类似于上述命令的方式工作,即使启动工具可能需要多个步骤。

例如,Postgres 官方镜像使用以下脚本作为其 ENTRYPOINT

bash
#!/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 "$@"

这个脚本使用 Bash 命令 exec,以便最终运行的应用程序成为容器的 PID 1。这允许应用程序接收发送到容器的任何 Unix 信号。更多信息请见 ENTRYPOINT 参考

在以下示例中,一个帮助脚本被复制到容器中并通过 ENTRYPOINT 在容器启动时运行:

dockerfile
COPY ./docker-entrypoint.sh /
ENTRYPOINT ["/docker-entrypoint.sh"]
CMD ["postgres"]

这个脚本让你以多种方式与 Postgres 交互。

它可以简单地启动 Postgres:

console
docker run postgres

或者,你可以用它来启动 Postgres 并向服务器传递参数:

console
docker run postgres postgres --help

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

console
docker run --rm -it postgres bash

有关 ENTRYPOINT 的更多信息,请参见 Dockerfile 参考手册中的 ENTRYPOINT 指令

VOLUME

你应该使用 VOLUME 指令来暴露任何数据库存储区域、配置存储或由你的 Docker 容器创建的文件和文件夹。强烈建议你使用 VOLUME 暴露你镜像中的任何可变或用户可维护的部分。

有关 VOLUME 的更多信息,请参见 Dockerfile 参考手册中的 VOLUME 指令

USER

如果服务可以运行而无需特权,请使用 USER 切换到非 root 用户。从创建用户和组开始,在 Dockerfile 中使用如下示例:

dockerfile
RUN groupadd -r postgres && useradd --no-log-init -r -g postgres postgres

注意

考虑一个明确的 UID/GID。

镜像中的用户和组被分配一个非确定性的 UID/GID,即“下一个” UID/GID 被分配,无论镜像重建。所以,如果这是关键的,你应该分配一个明确的 UID/GID。

避免安装或使用 sudo,因为它具有不可预测的 TTY 和信号转发行为,可能

会引起问题。如果你绝对需要类似 sudo 的功能,比如以 root 初始化守护进程但以非 root 运行它,考虑使用 “gosu”

最后,为了减少层次和复杂性,避免频繁切换 USER

有关 USER 的更多信息,请参见 Dockerfile 参考手册中的 USER 指令

WORKDIR

为了清晰和可靠性,你应始终使用绝对路径作为你的 WORKDIR。此外,你应使用 WORKDIR 而不是繁殖类似 RUN cd … && do-something 的指令,这些指令难以阅读、故障排除和维护。

有关 WORKDIR 的更多信息,请参见 Dockerfile 参考手册中的 WORKDIR 指令

ONBUILD

ONBUILD 命令在当前 Dockerfile 构建完成后执行。ONBUILD 在从当前镜像派生的任何子镜像中执行。可以将 ONBUILD 命令视为父 Dockerfile 给子 Dockerfile 的指令。

Docker 构建时会先执行 ONBUILD 命令再执行子 Dockerfile 中的任何命令。

ONBUILD 对于将要被构建为基于一个给定镜像的镜像很有用。例如,你会在语言堆栈镜像中使用 ONBUILD,该镜像构建在该语言中编写的任意用户软件,包括在 Dockerfile 中,就像在 Ruby 的 ONBUILD 变体 中看到的那样。

使用 ONBUILD 构建的镜像应该获得单独的标签。例如,ruby:1.9-onbuildruby:2.0-onbuild

ONBUILD 中放置 ADDCOPY 时要小心。如果新构建的上下文缺失被添加的资源,onbuild 镜像会灾难性地失败。添加一个单独的标签,如上所建议的,有助于通过允许 Dockerfile 作者做出选择来减轻这种影响。

有关 ONBUILD 的更多信息,请参见 Dockerfile 参考手册中的 ONBUILD 指令