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

最常见的用例可能RUNapt-get.由于该RUN apt-get命令会安装软件包,因此需要注意一些违反直觉的行为。

始终在同一个语句中结合RUN apt-get update使用 。例如:apt-get installRUN

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

使用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来显式选择支持该选项的 shell pipefail。例如:

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"]ENTRYPOINTENTRYPOINT

有关 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中的分支、标签和子目录的协议。wgettarADD

以下示例用于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-onbuildruby:2.0-onbuild

ADD放入或COPY放入时要小心ONBUILD。如果新构建的上下文缺少正在添加的资源,则 onbuild 映像会发生灾难性的失败。按照上面的建议添加单独的标签,可以让 Dockerfile 作者做出选择,从而有助于缓解这种情况。

有关 ONBUILD 指令的更多信息ONBUILD,请参阅 Dockerfile 参考。