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 update 与 apt-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,您的构建可能会获得 curl 和 nginx 包的过时版本。

使用 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 中,考虑使用 RUN 的 exec 形式显式选择支持 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 ​

ADD 和 COPY 在功能上类似。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 优于手动添加文件,如使用 wget 和 tar,因为它确保了更精确的构建缓存。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

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

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-onbuild 或 ruby:2.0-onbuild。

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

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