# 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指令中指定的文件或目录，则构建会执行失败。


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://camelgemonion.gitbook.io/docker/dockerfile-zui-jia-shi-jian.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
