Dockerfile最佳实践

Dockerfile最佳实践

    Docker通过读取Dockerfile中的指令自动生成镜像,Dockerfile是一个文本文件,它包含生成给定镜像所需的所有命令。
    Docker镜像由只读层组成,每一层代表一条Dockerfile指令。这些层是堆叠的,每一层都是前一层变化的增量。
    以下面的Dockerfile为例:

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

    每条指令创建一个层:
    FROM 从ubuntu:18.04的镜像创建一个层;
    COPY 从Dockerfile所在的目录向容器目标地址进行复制。在本例中,就是将该Dockerfile目录下的所有文件和目录全部复制到新建容器的/app目录下;
    RUN 使用make命令构建应用程序;
    CMD 指定要在容器中运行的命令。在本例中,新建容器后,要运行的命令是python /app/app.py 。
    运行镜像并生成容器时,将在基础层的顶部添加新的可写层。对运行的容器所做的所有更改,例如编写新文件、修改现有文件和删除文件,都会写入这个薄的可写容器层。
在这里插入图片描述

1、Dockerfile一般准则和建议

1. 创建临时容器

    容器的生命周期应该很短暂。所谓“短暂”,意思是容器可以停止和销毁,然后重建和替换为一个低的配置。

2. 了解构建环境

    发出docker build命令时,当前工作目录称为构建环境(build context)。默认情况下,Dockerfile位于此处,但可以使用 -f 指定其他位置。不管Dockerfile实际位于何处,当前目录中文件和目录的所有递归内容都将作为构建环境发送到Docker守护进程。
    示例:
    创建一个文件夹作为构建环境。将“hello”写入一个名为hello的文本文件中,并创建一个运行cat hello命令的Dockerfile。从构建环境(.)中生成镜像:

mkdir myproject && cd myproject
echo "hello" > hello
echo -e "FROM busybox\nCOPY /hello /\nRUN cat /hello" > Dockerfile
docker build -t helloapp:v1 .

    构建过程如下

# docker build -t helloapp:v1 .
Sending build context to Docker daemon 3.072 kB
Step 1/3 : FROM busybox
...
Successfully built 8cb12b6b9921

    将一个与项目无关的文件复制到改目录,重新构建

# ls
Dockerfile  hello  linux-1.0.tar.gz
# docker build --no-cache -t helloapp:v2 .
Sending build context to Docker daemon 1.263 MB
Step 1/3 : FROM busybox
 ...
Successfully built 625d2185effb

    无意中包含构建镜像不需要的文件会导致更大的构建环境。这会增加构建镜像的时间、拉取和推送镜像的时间以及容器运行时大小。要查看构建环境有多大,请在构建Dockerfile时查找这样的消息:

Sending build context to Docker daemon  187.8MB

    从两次构建的过程,可以看出,第一次构建环境大小为3.072kB,第二次构建环境大小为1.263MB。当然,两次构建的镜像大小是一样的:

# docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
helloapp            v2                  625d2185effb        10 seconds ago      1.22 MB
helloapp            v1                  8cb12b6b9921        25 minutes ago      1.22 MB
3. 通过标准输入传输Dockerfile

    Docker可以通过在标准输入中使用本地或远程构建环境来生成镜像。将Dockerfile通过标准输入进行传输,在临时生成镜像而不生成Dockerfile的情况下非常有用,但不应形成习惯。
    示例:
    以下命令是等效的

echo -e 'FROM busybox\nRUN echo "hello world"' | docker build -
docker build -<<EOF
FROM busybox
RUN echo "hello world"
EOF
3.1 使用标准输入中的Dockerfile生成镜像,而不使用构建环境

    此语法可使用标准输入传输的Dockerfile生成镜像,而不需要附加其他文件作为构建环境。连字符(-)占据构建路径的位置,并指示Docker从标准输入而不是目录中读取构建环境(构建环境中只包含Dockerfile):

docker build [OPTIONS] -

    示例:

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

    在Dockerfile不需要将文件复制到镜像中的情况下,省略构建环境可能会很有用,并提高构建速度,因为没有文件发送到守护进程。
    注意:如果使用此语法,尝试构建使用COPY或ADD的Dockerfile将失败。以下示例说明了这一点。

# mkdir example
# cd example
# touch somefile.txt

# docker build -t myimage:latest -<<EOF
> FROM busybox
> COPY somefile.txt .
> RUN cat /somefile.txt
> EOF
Sending build context to Docker daemon 2.048 kB
Step 1/3 : FROM busybox
 ---> 020584afccce
Step 2/3 : COPY somefile.txt .
lstat somefile.txt: no such file or directory
3.2 使用本地/远程环境构建,使用标准输入的Dockerfile

    此语法可以使用本地文件系统上的文件,但使用标准输入的Dockerfile来构建镜像。语法使用-f(或-‌-file)选项指定Dockerfile,使用连字符(-)作为文件名表示Docker从标准输入读取Dockerfile:

docker build [OPTIONS] -f- PATH

    示例:
    使用当前目录(.)作为构建环境,并使用Dockerfile生成镜像。
    备注:写到这里,我多次尝试实验都失败了。centos7的环境,至今没找到原因,当然,显而易见,docker build指令并没有把 -f- 解析成标准输入,但应该如何修复,如果大家有好方法请告知。

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

unable to prepare context: unable to evaluate symlinks in Dockerfile path: lstat /var/www/html/myproject/example/-: no such file or directory

# docker build -t myimage:latest -f- https://github.com/docker-library/hello-world.git <<EOF
FROM busybox
COPY hello.c .
EOF
unable to prepare context: unable to evaluate symlinks in Dockerfile path: lstat /tmp/docker-build-git753482777/-: no such file or directory

    当使用远程Git存储库作为构建环境构建映像时,Docker在本地计算机上执行存储库的Git克隆,并将这些文件作为构建环境发送给守护进程。此功能要求在运行docker build命令的主机上安装git。

4. 用.dockerignore排除

    若要排除与生成无关的文件(不重新构造源存储库),请使用.dockerignore文件。此文件支持类似于.gitignore文件的排除模式。
    在docker CLI将构建环境发送到docker守护进程之前,它会在构建环境的根目录中查找名为.dockerginore的文件。如果该文件存在,CLI修改构建环境以排除相匹配的文件和目录。这可以避免将大型或敏感的文件和目录发送到守护进程,但同时可以使用ADD或COPY将它们添加到镜像。
    如果.dockerignore文件中的一行以#开头,则该行将被视为注释。
.dockerignore示例:

# comment
*/temp*
*/*/temp*
temp?

    说明如下:

*/temp*   排除在根目录的任何直接子目录中,名称以temp开头的文件和目录。例如,排除了纯文件/somedir/temporary.txt,排除了目录/somedir/temp。
*/*/temp* 从根目录下两级的任何子目录中,排除以temp开头的文件和目录。例如,排除了/somedir/subdir/temporary.txt。
temp?     排除根目录中,名称是temp的单字符扩展名的文件和目录。例如,/tempa和/tempb被排除在外。
5. 使用多阶段构建

    对于多阶段构建,可以在Dockerfile中使用多个FROM语句。每个FROM指令都可以使用不同的镜像,并且每个FROM都将开始一段新的构建。您可以有选择地将一个阶段的执行结果复制到另一个构建阶段,并在最终镜像留下想要的所有内容。
    例如,如果构建包含多个层,则可以将它们从更改频率较低的层(以确保生成缓存可重用)排序到更改频率较高的层:
    安装构建应用程序所需的工具
    安装或更新库依赖项
    生成应用程序
    示例:Go应用程序的Dockerfile

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"]
6. 不要安装不必要的软件包

    为了减少复杂性、依赖性、文件大小和构建时间,避免安装额外的或不必要的包。例如,不需要在数据库镜像中包含文本编辑器。

7. 分离应用程序

    每个容器应该只做一件事情。将应用程序分离到多个容器可以更容易地横向扩展和重用容器。例如,一个web应用程序栈可能由三个独立的容器组成,每个容器都有自己的镜像,以解耦的方式管理web应用程序、数据库和内存缓存。
    将每个容器限制为一个进程是一个很好的经验法则,但这不是一个硬性规定。例如,不仅可以用init进程生成容器,一些程序还可以自动生成其他进程。例如,Celery可以生成多个工作进程,而Apache可以为每个请求创建一个进程。
    保持容器尽可能的干净和模块化。如果容器相互依赖,可以使用Docker容器网络来确保容器间通信。

8. 最小化层数

    只有指令RUN,COPY,ADD才创建层。其他指令会创建临时中间镜像,并且不会增加构建的大小。
    在可能的情况下,使用多阶段构建,只将需要的阶段结果复制到最终镜像。这允许在中间构建阶段添加工具和调试信息,而不扩大最终镜像。

9. 排序多行参数

    只要有可能,通过按字母数字顺序排列多行参数来简化以后的更改。这有助于避免包的重复,并使列表更易于更新。在反斜杠(\)前添加空格也有帮助。

RUN apt-get update && apt-get install -y \
  bzr \
  cvs \
  git \
  mercurial \
  subversion
10. 利用构建缓存

    当构建一个镜像时,Docker会逐步执行Dockerfile中的指令,并按照指定的顺序执行每个指令。在检查每个指令时,Docker在其缓存中寻找可重用的现有镜像,而不是创建新的(重复的)镜像。
    如果根本不想使用缓存,可以在docker build命令中使用-‌-no cache=true选项。但是,如果你让Docker使用它的缓存,那么很重要的一点是了解它什么时候可以,什么时候不能找到匹配的镜像。    Docker遵循的基本规则概述如下:
    从已在缓存中的父镜像开始,将下一条指令与从该基础镜像派生的所有子镜像进行比较,以查看是否使用完全相同的指令生成了其中一条镜像。如果没有,缓存失效。
    在大多数情况下,只需将Dockerfile中的指令与其中一个子镜像进行比较就足够了。然而,某些指示需要更多的检查和解释。
    对于ADD和COPY指令,将检查镜像中文件的内容,并为每个文件计算校验和。这些校验和不考虑文件的最后修改和最后访问时间。在高速缓存查找期间,将校验和与现有镜像中的校验和进行比较。如果文件中有任何更改(如内容和元数据),则缓存将失效。
    除了ADD和COPY指令之外,缓存检查不会查看容器中的文件来确定缓存匹配。例如,在处理 RUN apt-get -y update 命令时,不检查容器中更新的文件,以确定是否存在缓存命中。在这种情况下,只使用命令字符串本身来查找匹配项。
    一旦缓存失效,所有后续的Dockerfile命令都会生成新镜像,并且缓存不会被使用。

2、Dockerfile指令

FROM

格式:

FROM <image> [AS <name>]

FROM <image>[:<tag>] [AS <name>]

FROM <image>[@<digest>] [AS <name>]

    FROM指令初始化一个新的构建阶段,并为后续指令设置基础镜像。因此,有效的Dockerfile必须以FROM指令开头。
    FROM可以在一个Dockerfile中多次出现,以创建多个镜像或将一个构建阶段用作另一个构建阶段的依赖项。docker build实现Dockerfile到Docker镜像的构建,而对于单条Dockerfile中的命令(如命令RUN apt-get update),则是通过针对Docker容器的commit操作,实现将其构建为单层镜像,也就是大家熟悉的docker commit操作。回过头来,使用FROM命令创建其他构建阶段的依赖,只需在执行每条新的FROM指令之前记录docker commit输出的最后一个镜像ID。每条FROM指令都清除以前指令创建的任何状态。
    通过将AS name添加到FROM指令中,为新的构建阶段指定一个名称。该名称可以在后续的FROM和COPY -‌-from=<name|index>指令中使用,以引用在此阶段中生成的镜像。
    tag或digest是可选的。如果忽略其中任何一个,则镜像生成器默认采用latest作为标记。
    尽可能使用官方镜像作为基础镜像。

LABEL

    格式

LABEL <key>=<value> <key>=<value> <key>=<value> ...

    LABEL翻译为“标签”,TAG也翻译为“标签”。我个人理解为:LABEL好像描述一个人的性别,身高,爱好等等信息,每个人的LABEL都是真实准确的;TAG好像这个人的网名,每个人可以有很多个,每个人的TAG可以灵活更改,只是为了方便找到这个人。不知道这样理解是不是准确。
    一个镜像可以有多个标签。在Docker1.10之前,建议将所有标签组合为一个LABEL指令,以防止创建额外的层。组合标签现在已不再必要,但仍然是受支持的。

RUN

    可能RUN最常见的用例是apt-get的应用程序。因为它安装了包,所以RUN apt get命令有几个小问题需要注意。

APT-GET

    1.避免运行apt-get -upgrade和dist-upgrade,因为来自父镜像的许多极为重要的基本包无法在非特权容器中升级。如果父镜像中包含的包已过期,请与其维护人员联系。如果您知道有一个特定的叫foo的包需要更新,请使用apt get install-y foo自动更新。
    2.始终将RUN apt-get update与apt-get install合并在同一RUN语句中。例如:

RUN apt-get update && apt-get install -y \
    package-bar \
    package-baz \
    package-foo

    在RUN语句中单独使用apt-get update会导致缓存问题和后续apt-get安装指令失败。例如,假设您有一个Dockerfile:

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包的过时版本。
    3.使用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.*

    版本固定会强制生成检索特定版本,而不管缓存中有什么。此技术还可以减少由于所需包中的意外更改而导致的故障。
    下面是一个格式良好的运行指令,它演示了所有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的缓存崩溃,并确保安装新版本。在每行列出包还可以防止包复制中的错误。
    此外,当您通过删除/var/lib/apt/lists来清理apt缓存时,它会减小镜像大小,因为apt缓存不存储在层中。由于RUN语句以apt-get update开头,因此包缓存总是在apt-get install之前刷新。
    备注:官方Debian和Ubuntu映像会自动运行apt-get clean,因此不需要显式调用。

使用管道符

    某些运行命令需要使用管道字符(|)将一个命令的输出变成另一个命令的输入,例如:

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 选项。
    在基于Debian的镜像上的dash shell等情况下,考虑使用RUN的exec形式并选择一个支持pipefail选项的shell。
    例如:

RUN ["/bin/bash", "-c", "set -o pipefail && wget -O - https://some.site | wc -l > /number"]
CMD

    CMD有三种形式:

CMD ["可执行文件","参数1","参数2"]

    首选该格式

CMD ["参数1","参数2"]

    作为ENTRYPOINT的默认参数

CMD command 参数1 参数2

    shell 格式
    Dockerfile中只能有一条CMD指令。如果列出多个命令,则只有最后一个命令才会生效。
    CMD的主要目的是为正在执行的容器提供默认值。这些默认值可以包括可执行文件,也可以省略可执行文件,在这种情况下,还必须明确ENTRYPOINT指令。
    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中去执行。除非用户已经非常熟悉ENTRYPOINT的工作原理,否则很少将CMD [“param”, “param”]与ENTRYPOINT一起使用。

EXPOSE

    EXPOSE指令指示容器侦听连接的端口。因此,您应该为应用程序使用通用的传统端口。例如,包含Apache web服务器的映像将使用EXPOSE 80,而包含MongoDB的映像将使用EXPOSE 27017等等。
    对于外部访问,用户可以使用指示如何将指定端口映射到所选端口的标志来执行docker run。对于容器链接,Docker为从接收容器返回到源的路径(即MYSQL_PORT_3306_TCP)提供环境变量。

ENV

    为了使新软件更易于运行,可以使用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 http://example.com/postgres-$PG_VERSION.tar.xz | tar -xJC /usr/src/postgress && …
ENV PATH /usr/local/postgres-$PG_MAJOR/bin:$PATH

    每个ENV行创建一个新的中间层,就像RUN命令一样。这意味着,即使您在未来的层中取消设置环境变量,它仍然存在于该层中,并且其值可以被转储。
    示例如下:
    创建一个Dockerfile

FROM alpine
ENV ADMIN_USER="mark"
RUN echo $ADMIN_USER > ./mark
RUN unset ADMIN_USER

    构建镜像并执行。结果变量ADMIN_USER值为mark,证明Dockerfile中的unset操作无法影响已经生成的镜像层。

# docker build .
Sending build context to Docker daemon 2.048 kB
Step 1/4 : FROM alpine
...
Successfully built 7e74bdf26d90
# docker run 7e74bdf26d90 sh -c 'echo $ADMIN_USER'         
mark

    要防止这种情况,并真正取消设置环境变量,请将RUN命令与shell命令一起使用,以便在单个层中设置、使用和取消设置变量。您可以用;或 && 分隔命令。建议使用第二种方法,其中一个命令失败,docker构建也会失败。使用 \ 作为Linux Dockerfile的行继续字符可以提高可读性。您还可以将所有命令放入shell脚本,并让RUN命令只运行该shell脚本。
    调整后的Dockerfile文件如下:

FROM alpine
RUN export ADMIN_USER="mark" \
    && echo $ADMIN_USER > ./mark \
    && unset ADMIN_USER
CMD sh

    执行构建并运行:

# docker build .
Sending build context to Docker daemon 2.048 kB
Step 1/3 : FROM alpine
...
Successfully built 1bfe759e4c6d
# docker run 1bfe759e4c6d sh -c 'echo $ADMIN_USER'

#

    额,不行了,翻译了好几天都没翻译完,现在又很饿,先翻译到这。后期有时间再补充剩下的几个命令吧。

参考文档

https://docs.docker.com/develop/develop-images/dockerfile_best-practices/
https://collabnix.com/understanding-docker-container-image/
https://12factor.net/zh_cn/
http://tldp.org/LDP/abs/html/here-docs.html
https://docs.docker.com/develop/develop-images/multistage-build/
http://guide.daocloud.io/dcs/docker-commit-9153991.html

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值