编写 Dockerfile 的五个最佳实践

此文适合Docker初学入门读者,大师请绕行!,遵守最佳实践可少踩坑、提升性能体验及可移植性,期望对读者有所帮助!

什么是Dockerfile

Dockerfile 是一个文本文件,里面包含了打包Docker镜像所需要用到的命令。Docker 可以通过读取 Dockerfile 里面的命令来自动化地构建Docker镜像。通过执行 docker build 就可以启动这样的一个自动化流程。

$ docker build -f Dockerfile .
Sending build context to Docker daemon  2.048kB
Step 1/1 : FROM nginx:latest
 ---> f895b3fb9e30
Successfully built f895b3fb9e30

容器镜像层

Docker镜像由只读层组成,每个层都代表一个Dockerfile指令,这些层是堆叠的,每一层都是前一层变化的增量。运行镜像并生成容器时,可以在基础镜像的顶部添加新的可写层(“容器层”)。对正在运行的容器所做的所有更改(例如写入新文件,修改现有文件和删除文件)都将写入此可写容器层。

这里写图片描述

01 了解Docker Build上下文(Context)

Docker build有一个重要参数context,默认是当前目录,采用.代替,但为了避免将不必要的文件打包到镜像,造成context及镜像大小过大,也可以用指定的目录作为context。context过大会造成docker build很耗时,镜像过大则会造成docker pull/push性能变差以及运行时容器体积过大浪费空间资源。

对于Docker17.05及以上版本,有一个remote context的特性,即可以将远程资源作为context,如下所示:

$ docker build -t nginx:v1 https://gitlab.com/fuhui/docker-lab.git
Sending build context to Docker daemon  50.18kB
Step 1/1 : FROM nginx:latest
 ---> f895b3fb9e30
Successfully built f895b3fb9e30
Successfully tagged nginx:v1

02 分阶段构建

构建镜像最大的挑战莫过于防止镜像过大,造成实际运行时由于并发而导致拉取性能问题。为了应对这个挑战,很多转型到容器的团队采用两个Dockerfile,一个负责开发环境的镜像构建,一个负责生产环境的镜像构建。开发镜像包含了代码构建所需要的环境,镜像大小自然比较大,生产镜像仅包含应用运行所需要的内容,是很精简的体积很小的镜像。

一个开发镜像Dockerfile示例如下:

FROM golang:1.7.3
WORKDIR /go/src/github.com/alexellis/href-counter/
COPY app.go .
RUN go get -d -v golang.org/x/net/html \
  && CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app .

一个生产镜像Dockerfile示例如下:

FROM alpine:latest  
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY app .
CMD ["./app"] 

这时候整个过程分为三步:1.根据开发镜像启动一个容器来构建app 2. 构建结束后的app二进制拷贝到主机 3. 根据生产镜像启动一个容器,将主机上的app二进制copy到生产镜像中。如下脚本可自动化执行该过程。

#!/bin/sh
echo Building alexellis2/href-counter:build

docker build --build-arg https_proxy=$https_proxy --build-arg http_proxy=$http_proxy \  
    -t alexellis2/href-counter:build . -f Dockerfile.build

docker container create --name extract alexellis2/href-counter:build  
docker container cp extract:/go/src/github.com/alexellis/href-counter/app ./app  
docker container rm -f extract

echo Building alexellis2/href-counter:latest

docker build --no-cache -t alexellis2/href-counter:latest .
rm ./app

这么做会大量占用主机的空间资源,并且运行一段时间后,遗留很多二进制app文件在主机上,后期清理还需要额外的维护工作,显然不是一个很好的实践。

Docker分阶段构建就可以很好地解决这个问题,使用如下的Dockerfile即可:

FROM golang:1.7.3 as builder
WORKDIR /go/src/github.com/alexellis/href-counter/
RUN go get -d -v golang.org/x/net/html  
COPY app.go    .
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app .

FROM alpine:latest  
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY --from=builder /go/src/github.com/alexellis/href-counter/app .
CMD ["./app"]  

整个Dockerfile流程很清晰,也不在需要额外的shell脚本来支持整个流程,并且我们可以指定执行的stage,具体命令如下:

$ docker build --target builder -t alexellis2/href-counter:latest .

03 使用构建缓存

构建映像时,Docker会逐步按指定的顺序执行Dockerfile中的每个指令。在检查每条指令时,Docker会在其缓存中查找可以重用的现有镜像,而不是重复创建新的镜像。

如果不想使用缓存,可以在docker build时加上--no-cache=true参数。然后,使用缓存会使得整个构建过程更高效,因此了解什么样的情形下可以利用缓存来提升效率很重要。具体来说有下面几个情形需要了解:

  • 从已经在缓存中的父镜像开始,将下一条指令与从该基本镜像导出的所有子镜像进行比较,以查看它们中的一个是否使用完全相同的指令构建。如果不是,则缓存无效。

  • 在大多数情况下,只需将Dockerfile中的指令与其中一个子镜像进行比较即可。但是,某些Dockerfile命令需要更深入的检查。对于ADD和COPY指令,将检查镜像中文件的内容,并为每个文件计算校验和。在这些校验和中不考虑文件的最后修改时间和最后访问时间。在缓存查找期间,将校验和与现有映像中的校验和进行比较。如果文件中有任何更改(例如内容和元数据),则缓存无效。

  • 除了ADD和COPY命令之外,缓存检查不会查看容器中的文件以确定缓存匹配。例如,在处理RUN apt-get -y update命令时,不检查容器中更新的文件以确定是否存在缓存命中。在这种情况下,只需使用命令字符串本身来查找匹配项。

04 给镜像设置标签(Label)

每一个对象都包含相应的元数据信息,比如Docker镜像就包含作者、大小等等元数据信息。在使用镜像的时候,往往会通过这些元数据信息来查找适合的镜像来用于开发或测试,而不单单只是通过名字去检索。

在Dockerfile中可以使用Label命令来为镜像增加Label,示例如下:

FROM nginx:latest
LABEL version=2.0

我们可以查看到Label及使用Label进行筛选:

$ docker build -t nginx:2.0 https://gitlab.com/fuhui/docker-lab.git
Sending build context to Docker daemon  50.18kB
Step 1/2 : FROM nginx:latest
 ---> f895b3fb9e30
Step 2/2 : LABEL version 2.0
 ---> Running in 721d056eec21
 ---> 5af7bc144cb6
Removing intermediate container 721d056eec21
Successfully built 5af7bc144cb6
Successfully tagged nginx:2.0
$ docker inspect 5af7bc144cb6
...
    "Labels": {
        "maintainer": "NGINX Docker Maintainers <[email protected]>",
        "version": "2.0"
    },
...
$ docker images --filter "label=version=2.0"
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
nginx               2.0                 5af7bc144cb6        3 minutes ago       108MB

05 CMD和ENTRYPOINT指令结合

官方关于CMD和ENTRYPOINT指令的说明如下

CMD
The main purpose of a CMD is to provide defaults for an executing container.
ENTRYPOINT
An ENTRYPOINT helps you to configure a container that you can run as an executable.

简而言之,就是CMD提供运行时的动态覆盖参数机制,而ENTRYPOINT只是容器启动时的执行入口。

假设有如下Dockerfile:

FROM debian
ENTRYPOINT ["/bin/ping"]
CMD ["localhost"]

当不指定任何参数时,情形如下:

$ docker run -it ping-test
PING localhost (127.0.0.1): 48 data bytes
56 bytes from 127.0.0.1: icmp_seq=0 ttl=64 time=0.096 ms
56 bytes from 127.0.0.1: icmp_seq=1 ttl=64 time=0.088 ms
56 bytes from 127.0.0.1: icmp_seq=2 ttl=64 time=0.088 ms

当指定一个ip地址,如192.168.137.4时,情况如下:

$ docker run -it ping-test 192.168.137.4
56 bytes from 192.168.137.4: icmp_seq=0 ttl=55 time=32.583 ms
56 bytes from 192.168.137.4: icmp_seq=2 ttl=55 time=30.327 ms
56 bytes from 192.168.137.4: icmp_seq=4 ttl=55 time=46.379 ms

这种机制意味着更好的可移植性,用户可在执行时动态注入变量,如当前环境类型、认证信息等等,便于服务的迁移、扩容等场景。

总结

Dockerfile 在实际基于docker的开发中使用非常普遍,很多开发者都掌握了基本的编写技巧,但对于一些优化的策略、方法掌握比较少,那么以上这些实践能够帮助大家节省时间、提升效率和性能,甚至提升应用的移植灵活性等。

猜你喜欢

转载自blog.csdn.net/afandaafandaafanda/article/details/81937466