> For the complete documentation index, see [llms.txt](https://camelgemonion.gitbook.io/docker/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://camelgemonion.gitbook.io/docker/dockerfile-zui-jia-shi-jian.md).

# Dockerfile最佳实践

Dockerfile包含一组指令集合。Docker镜像由每一条指令生成的只读层构成。镜像层是栈式的，并且每一层都基于上一层的变化。

```
FROM ubuntu:18.04
COPY . /app
RUN make /app
CMD python /app/app.py
```

* `FROM` 创建一个基于ubuntu:18.04的镜像层
* `COPY` 将当前目录下面的文件拷贝到镜像中
* `RUN` 运行make指令构建应用
* `CMD` 指定容器需要运行的命令

当使用Docker镜像生成一个docker容器时，会在容器的最上层添加一个可写层，这个可写层称为容器层。在容器中对文件的增删改都会写入容器层。

## 构建上下文

docker镜像的构建上下文与Dockerfile文件的位置无关。如果在构建镜像时，Dockerfile正好处于构建上下文之中，且文件的名字为Dockerfile，则可以在构建镜像时省略Dockerfile的指定。

例如：

```
docker build -t camelgem/test .
```

如果Dockerfile不在当前的构建上下文路径中，且Dockerfile文件的名字不为Dockerfile，则必须使用-f选项指定Dockerfile的路径和名称。

```
docker build -f MyDockerfile -t camelgem/test .
```

构建镜像时，不要将不需要的文件一起打包到镜像中，这些无用的文件增加镜像的大小以及镜像构建的速度。

当构建镜像时，终端会输出一条信息，提示当前构建镜像的构建上下文有多大。

```
Sending build context to Docker daemon  187.8MB
```

## 通过管道来构建镜像

除了通过创建Dockerfile文件来构建镜像以外，我们也可以使用管道来临时构建镜像。

例如下面的示例：

```
echo -e 'FROM busybox\nRUN echo "hello world"' | docker build -

docker build -<<EOF
FROM busybox
RUN echo "hello world"
EOF
```

使用下面的构建镜像的语法无需发送额外的文件给docker守护进程。`-` 让Docker从标准输入读取构建内容而不是目录。

```
docker build [OPTIONS] -
```

下面的示例展示了如何通过标准输入的Dockerfile来构建一个镜像：

```
docker build -t myimage:latest -<<EOF
FROM busybox
RUN echo "hello world"
EOF
```

上面的构建方式不会有文件作为构建内容发送给Docker守护进程。

当我们不需要拷贝文件到镜像中时，可以使用上面这种方式。它可以提高构建速度，并且不会有文件发送给Docker守护进程。

### 使用.dockerignore文件来指定要排除的文件，从而减少构建镜像的大小。

{% hint style="warning" %}
当使用标准输入构建镜像不支持COPY或ADD指令。
{% endhint %}

```
# create a directory to work in
mkdir example
cd example

# create an example file
touch somefile.txt

docker build -t myimage:latest -<<EOF
FROM busybox
COPY somefile.txt .
RUN cat /somefile.txt
EOF

# observe that the build fails
...
Step 2/3 : COPY somefile.txt .
COPY failed: stat /var/lib/docker/tmp/docker-builder249218248/somefile.txt: no such file or directory
```

### 使用来自标准输入的Dockerfile来构建一个使用本地构建上下文的镜像

使用下面这种语法可以实现这个目的：

```
docker build [OPTIONS] -f- PATH
```

下面的示例使用当前目录作为构建上下文，然后使用来自标准输入的Dockerfile构建镜像。

```
# create a directory to work in
mkdir example
cd example

# create an example file
touch somefile.txt

# build an image using the current directory as context, and a Dockerfile passed through stdin
docker build -t myimage:latest -f- . <<EOF
FROM busybox
COPY somefile.txt .
RUN cat /somefile.txt
EOF
```

### 使用来自标准输入的Dockerfile来构建一个使用远端github仓库构建上下文的镜像

见下面示例：

```
docker build -t myimage:latest -f- https://github.com/docker-library/hello-world.git <<EOF
FROM busybox
COPY hello.c .
EOF
```

{% hint style="info" %}
使用远端git仓库构建镜像时，Docker会执行一个git clonemingl来将git仓库克隆到本地机器上，然后将仓库内容作为构建上下文发送给Docker守护进程。

使用这个特性需要确保本地机器安装了git命令。
{% endhint %}

## 使用.dockerignore文件来排除不需要的文件。文件支持的排除语法类似于.gitignore文件。

## &#x20;使用多阶段构建

由于镜像是在构建过程的最后阶段构建的，因此我们可以利用构建缓存最大限度地减少镜像层。

例如，如果我们的构建包含多个层，则可以将这些需要构建层根据更改频率的高低，从低到高排列，确保生成缓存可重复使用：

* 安装构建应用程序所需的工具
* 安装或更新库依赖关系
* 生成应用程序

Go应用程序的多阶段构建看起来像下面这样：

```
FROM golang:1.11-alpine AS build

# Install tools required for project
# Run `docker build --no-cache .` to update dependencies
RUN apk add --no-cache git
RUN go get github.com/golang/dep/cmd/dep

# List project dependencies with Gopkg.toml and Gopkg.lock
# These layers are only re-built when Gopkg files are updated
COPY Gopkg.lock Gopkg.toml /go/src/project/
WORKDIR /go/src/project/
# Install library dependencies
RUN dep ensure -vendor-only

# Copy the entire project and build it
# This layer is rebuilt when a file changes in the project directory
COPY . /go/src/project/
RUN go build -o /bin/project

# This results in a single layer image
FROM scratch
COPY --from=build /bin/project /bin/project
ENTRYPOINT ["/bin/project"]
CMD ["--help"]
```

## 不要在构建镜像时安装不需要的包

## 解耦要构建的应用

每一个容器都应有保证职责的单一性。将应用解耦成多个不同的容器更容器水平扩展和复用容器。例如，一个应用栈可能由三个独立的容器构成，每一个容器都有自己独立的镜像。以解耦的方式来管理一个web应用，数据库和一个内存缓存。

限制容器只运行一个进程是好习惯，但是这并不是一成不变的规则。例如，一个容器可以spawn一个init进程，一些程序也会spawn多个进程。例如Apache可以为每一个请求创建一个进程。

根据自己的最佳判断，来尽可能保持容器的整洁和模块化。如果一个容器依赖其他容器，那么可以使用Docker网络来确保容器之间的通信。

## 最小化层的数量

在旧版本的`Docker`中，我们必须最大限度地减少镜像中的镜像层数量，以确保它们是高效的。添加了以下功能以减少此限制：

* 只有`RUN，COPY，ADD`指令会创建层。其他指令创建临时的中间镜像层，它们不会增加镜像的大小。
* 尽可能使用多阶段构建，只将需要的内容拷贝到最终的镜像中。我们可以在中间构建阶段来包含一些工具和调试信息，而不会增加最终镜像的大小。

## 使用按字母排序的多行参数

使用这种形式有助于使PR更加清晰易于阅读。也可以避免重复的包，使包列表的更新更容易。

见下面示例：

```
RUN apt-get update && apt-get install -y \
  bzr \
  cvs \
  git \
  mercurial \
  subversion \
  && rm -rf /var/lib/apt/lists/*
```

## 利用构建缓存

构建镜像时，`Docker`会根据`Dockerfile`中的指令按步骤和顺序执行。每一条指令都会被检查，`Docker`会从它可以重复使用的缓存中查找一个已经存在的镜像，而不是创建一个重复的镜像。

如果不想使用缓存，可以在构建镜像时使用`--no-cache=true`选项，如果想要让Docker使用自身的缓存，理解何时使用缓存，何时不能使用缓存非常重要。`Docker`遵循以下基本规则：

* 从已处于缓存中的父镜像开始，将下一个指令与从该基础镜像中提取的所有子镜像进行比较，以查看其中一个指令是否使用完全相同的指令构建。如果没有，缓存将失效。
* 在大部分情况下，将`Dockerfile`中的一条指令与子镜像中的其中一个比较完全够了。然而，有些特定的指令需要更多的检查和解释。
* 对于`ADD`和`COPY`指令，将检查镜像中文件的内容，并为每个文件计算校验和。文件的上次修改和上次访问时间不会被计算进校验和中。在缓存查找过程中，校验和与现有镜像中的校验和进行比较。如果文件（如内容和元数据）中发生了任何更改，则缓存将失效。
* 除了`ADD`和`COPY`指令外，缓存检查不会查看容器中的文件来确定缓存是否匹配。例如，在处理`RUN apt-get-y` 更新命令时，不会检查容器中更新的文件以确定是否存在缓存命中。在这种情况下，只有命令字符串本身用于查找匹配项。

## Dockerfile指令

### FROM

无论何时，都尽可能使用官方镜像来作为基础镜像。可以使用Alpine镜像。

### LABEL

在镜像中添加标签来组织和管理项目。对于每一个标签，添加一个以LABEL开头的行并且带有一对或多对键值对。下面的示例展示了不同的LABEL使用语法。

{% hint style="info" %}
带有空格的字符串必须加上引号或者对空格进行转义。内部引号字符也必须进行转义。
{% endhint %}

```
# 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=""
```

一个镜像可以有一个或多个标签。可以在单个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"
```

### RUN

将复杂的或者很长的RUN语句使用反斜线分隔成多个独立的行来使Dockerfile易读，易理解，易维护。

在使用RUN指令时，可能会经常使用apt-get来安装软件包。使用apt-get安装软件包时，有一些需要注意的地方。

避免使用RUN apt-get upgrade和dist-upgrade，因为许多来自父镜像的软件包无法在一个非特权容器中进行升级。如果一个包含在父镜像中的软件包过期了，则需要联系镜像维护人员。如果需要更新一个特定的软件包foo，我们可以使用 apt-get install -y foo 来更新软件包。

在安装软件包时，总是应该在同一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指令的失败。例如：

```
FROM ubuntu:18.04
RUN apt-get update
RUN apt-get install -y curl
```

构建完镜像以后，Docker会缓存所有的构建层。如果此时我们在修改了apt-get install来安装一个额外的软件包：

```
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指令的好的使用方式：

```
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指定了软件包的安装版本，因此会导致缓存破坏，从而始终安装最新的软件。在上面的示例中，我们将需要安装的软件包用单独的行列出来，一方面易于阅读和维护，另一方面可以防止重复指定安装相同的软件包。

安装完软件包以后，我们可以通过删除/var/lib/lists目录下的内容来减小镜像的大小。apt缓存不是保存在镜像层中的。由于RUN语句以apt-get update开头，因此，包缓存总是会在执行apt-get install之前被刷新。

{% hint style="info" %}
ubuntu和debin发行版默认会自动执行apt-get clean来清理缓存，因此不需要明确调用这个命令。
{% endhint %}

#### 使用管道

一些RUN指令依靠管道来将输出的结果传递给另外一个命令，使用管道符|来实现这个目的。

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

Docker使用/bin/sh -c解释器来执行这些命令，命令执行的成功与否取决于管道中最后一条命令执行的退出状态码。上面的例子中，即使wget命令执行失败了，此构建步骤也会成功并生成新的镜像。

如果希望命令在任何一个阶段出现错误都会失败，可以使用set -o pipefail && 来确保一个非预期的错误不会导致构建的意外成功。见下面示例：

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

{% hint style="info" %}
并不是所有shell都支持 -o pipefail选项。在这种情况下，可以考虑使用RUN指令来明确运行一个支持次选项的shell。

例如 RUN \["/bin/bash", "-c", "set -o pipefail && wget -O - <https://some.site> | wc -l > /number"]
{% endhint %}

### CMD

CMD指令应用于运行镜像像中带有参数的软件。CMD应该始终使用CMD \["executable"、"param1"、"param 2"...]的形式。因此，如果镜像用于运行一个服务，如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\["param1"、"param2"]的形式与ENTRYPOINT一起使用，除非用户已经非常熟悉ENTRYPOINT的工作原理。

### EXPOSE

EXPOSE指令指示容器侦听连接的端口。因此，我们应该让我们的应用使用常见的传统端口。例如，包含 Apache Web 服务器的镜像将使用EXPOSE 80，而包含MongoDB的镜像将使用EXPOSE 27017等。

对于外部访问，用户可以执行带有flag的docker run，指示如何将指定端口映射到自己选择的端口。对于容器链接，Docker为从接收容器返回源的路径（即MYSQL\_PORT\_3306\_TCP）提供环境变量。

### ENV

可以使用ENV来为容器中安装的软件更新环境变量，ENV=/usr/local/nginx/bin:$PATH，确保CMD \["nginx"]能正常运行。

ENV也可以用于为需要容器化的应用提供需要的环境变量，例如Postgre’s 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/postgress && …
ENV PATH=/usr/local/postgres-$PG_MAJOR/bin:$PATH
```

Dockerfile中设置的ENV变量值，也可以在命令行指定新的值来覆盖老的值，见示例：

```
FROM perl

ENV name=camelgem
RUN echo $name
```

�输出

```
CACHED [1/2] FROM docker.io/library/perl                                                                                                                                0.0s
 => [2/2] RUN echo camelgem   
```

通过命令行修改ENV设置的值

```
 docker % docker run -e name=kobe -it perl  /bin/sh
# echo $name
kobe # 值变为kobe
```

每一个ENV行会创建一个新的间接层，就是RUN指令那样。这意味着即使在后面的层unset 环境变量，它也依然会持久的保存在该层中，不会被丢弃。见下面示例：

```
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 Dockerfile的行继续字符，可提高可读性。我们还可以将所有命令放入外壳脚本中，并让 RUN 命令运行该shell脚本。

```
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或COPY

ADD和COPY功能非常类似，通常来说，优先使用COPY。这是因为COPY比ADD更加透明。COPY只支持基本的将本地文件拷贝到容器中，而ADD拥有其他的一些特性，例如本地解压缩包或支持远端URL。ADD最佳使用方式是将本地的压缩文件解压到容器镜像中。

如果需要拷贝多个文件到容器镜像中，使用COPY指令单独拷贝它们，不要使用单个COPY指令拷贝多个文件。这样当后续文件内容修改以后，只确保每一个构建步骤中的缓存失效，这样更加高效。

例如：

```
COPY requirements.txt /tmp/
RUN pip install --requirement /tmp/requirements.txt
COPY . /tmp/
```

### ENTRYPOINT

ENTRYPOINT的最佳使用方式是作为镜像的主命令。允许镜像指定里面指令的命令，并且以CMD中指定的参数作为默认参数。

见下面示例：

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

现在镜像可以像下面这样显示命令的使用帮助：

```
$ docker run s3cmd
```

或使用正确的参数执行命令：

```
$ docker run s3cmd ls s3://mybucket
```

这很有用，因为镜像名称可以作为对二进制文件的引用，如上面的命令所示。

ENTRYPOINT指令还可以与helper脚本结合使用，允许它以类似于上述命令的方式运行，即使启动工具可能需要多个步骤。

例如，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 "$@"
```

帮助脚本被拷贝容器镜像中，并通过ENTRYPOINT执行：

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

脚本为我们提供了简单的交互，可以像下面这样启动容器

```
$ docker run postgres
```

或者传递参数给它：

```
$ docker run postgres postgres --help
```

最后，也可以被用来启动一个不同的工具，比如Bash

```
$ docker run --rm -it postgres bash
```

### VOLUME

应使用卷OLUME指令来暴露docker容器创建的任何数据库存储区域、配置存储或文件/文件夹。强烈建议将VOLUME用于镜像的任何可变和/或用户服务部分。

### USER

如果一个应用不需要使用特权用户来运行，则可以使用USER来切换用户。在切换前，在Dockerfile中使用RUN指令来生成一个用户：

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

{% hint style="info" %}
镜像中的用户和组被分配一个不确定的UID/GID，因为无论镜像重新构建与否，都会分配“下一个”UID/GID。因此，有必要的话，我们应该指定一个明确的UID/GID。
{% endhint %}

### WORKDIR

为了清晰度和可靠性，我们应该始终使用WORKDIR绝对路径进行工作。此外，我们应该使用WORKDIR，而不是像RUN cd..&&之类的指令，这些指令很难读取、不利于排除故障和维护。

### ONBUILD

ONBUILD指令会在当前镜像构建完成以后被执行。ONBUILD会在使用当前镜像的子镜像中被执行。可以认为ONBUILD指令是父Dockerfile提供给子Dockerfile的指令。

ONBUILD会在任何子Dockerfile指令之前被执行。

ONBUILD对于那些将要使用FROM来指定一个镜像的镜像非常有用。例如，我们可以使用ONBUILD在Dockerfile中来构建一个基于特定语言的软件。

在ONBUILD中使用COPY或ADD指令时，需要特别注意，如果子镜像中的构建上下文中不存在COPY或ADD指令中指定的文件或目录，则构建会执行失败。
