# Dockerfile 最佳实践
# ENTRYPOINT vs CMD
ENTRYPOINT 和 CMD 都用来指定一个可执行程序,这个可执行程序在 container 启动后自动运行。如果想要让自己制作的镜像自动运行程序(不需要在 docker run 后面添加命令),就需要使用这两个命令。
大部分基础镜像例如 ubuntu
等都内置了 CMD 指令,一般是 CMD /bin/bash
或者 CMD /bin/sh
等。
在基于基础镜像构建新的镜像的时候,只要在 Dockerfile 里指定这两个命令就能覆盖掉原镜像中的命令。
# CMD
CMD 有三种形式:
- exec 形式,必须是一个JSON Array 的格式:
FROM ubuntu:14.04
CMD ["ping", "localhost"]
2
- 若
ENTRYPOINT
存在,作为其默认参数,必须是一个JSON Array 的格式:
FROM ubuntu:14.04
ENTRYPOINT ["ping", "localhost"]
CMD ["-c", "3"]
2
3
- shell 形式:
FROM ubuntu:14.04
CMD ping localhost
2
shell 形式在容器启动执行命令时会自动在指令前拼接 /bin/sh -c
,也就是 sh 进程会作为容器的 PID 1 进程。而 exec 进程它还会将用户指定的进程作为容器内的 PID 1 进程运行,这样方便传递信号给该进程。所以再编写 Dockerfile 文件时优先使用 exec 模式。
CMD 命令可以被 docker run image
后面的命令行参数替换掉:
- 对于没有
ENTRYPOINT
的情况,比如执行docker run demo ls
,将忽略原有的 CMD 命令执行ls
列出容器内的目录。 - 对于包含了
ENTRYPOINT
的情况,docker run demo
后面的参数将作为ENTRYPOINT
的参数覆盖掉默认的 CMD 中的默认参数。 - 注意,包含了
ENTRYPOINT
后,CMD 不能是一个 shell 形式,否则会自动拼接上/bin/sh -c
传给ENTRYPOINT
,显然是不对的。
# ENTRYPOINT
ENTRYPOINT 指令有两种形式:
- exec 模式:
FROM ubuntu:14.04
ENTRYPOINT ["ping", "localhost"]
2
以这种模式定义的镜像在容器启动时会自动执行 ENTRYPOINT
指令,并且会将 docker run image
后的命令行参数附加在 ENTRYPOINT
的指令之后,例如 docker run demo -c 3
,容器内实际执行的指令就是 ping localhost -c 3
。如果需要在命令行覆盖 ENTRYPOINT
指令,可以使用 docker run --entrypoint
选项。
- shell 模式:
FROM ubuntu:14.04
ENTRYPOINT ping localhost
2
# 注意事项
- 基础镜像里指定了
CMD
指令,新镜像基于该镜像构建,并且重新指定了ENTRYPOINT
,此时,基础镜像里的CMD
指令会失效,以ENTRYPOINT
为准。 - 基础镜像里指定了
ENTRYPOINT
指令,新镜像基于该镜像构建,并且重新指定了CMD
,此时,此时新镜像执行的命令依旧以ENTRYPOINT
为准,CMD
作为参数传入。
# 参考
# ADD vs COPY
# ADD
ADD
允许使用一个 URL 作为src
参数,例如:ADD http://foo.com/bar.go /tmp/main.go
,这句指令将从远程下载bar.go
文件,并拷贝成/tmp/main.go
。ADD http://foo.com/bar.go /tmp/
,这句指令将从远程下载bar.go
指令,并拷贝到/tmp/
目录下(文件名仍然是bar.go
)。Docker 会自动根据dst
的结尾是不是斜杠来推断拷贝到一个目录还是文件。
ADD
允许自动解压被压缩的文件,如果src
是一个被压缩的格式,比如tar
,gzip
,bzip2
等,将自动解压到dst
目录下。ADD /foo.tar.gz /tmp/
,这句将解压foo.tar.gz
到/tmp
目录下。
值得注意的是,
ADD
的从 URL 下载文件 和解压缩两个特性不能同时使用,如果从远程下载一个压缩文件,ADD
不会解压,只会直接拷贝。
# COPY
- 由于
ADD
功能的复杂性(做了太多事情)和行为的不可预测性,docker 增加了COPY
命令,COPY
不支持上面ADD
的那些功能,仅仅用来实现本地文件或者目录的拷贝。 - 在多阶段构建功能出现后,
COPY
又增加了一个功能是从构建镜像中拷贝文件到新镜像中。
# 使用场景
本地拷贝文件的场景都应该使用
COPY
。需要自动解压的场景可以选择
ADD
。从远程 URL 下载文件也不应该使用
ADD
,例如下面的例子:
这样会使用两条指令,一条下载,一条解压安装并清理无用文件。看起来没啥问题,但实际上这样做并不会减少镜像的大小,因为rm
操作在单独的镜像层。ADD http://foo.com/package.tar.bz2 /tmp/ RUN tar -xjf /tmp/package.tar.bz2 \ && make -C /tmp/package \ && rm /tmp/package.tar.bz2
1
2
3
4更好的做法是,将整个操作写在一行,这样只会在同一个镜像层处理。
RUN curl http://foo.com/package.tar.bz2 \ | tar -xjC /tmp/package \ && make -C /tmp/package
1
2
3
# 参考
# 编写 Dockerfile 的最佳实践
# 使用 .dockerignore
.dockerignore
文件可以用来排除那些不需要在构建镜像中复制的文件,类似于 .gitignore
。
# 使用 multi-stage 构建
对于 go,c 这种编译型语言,使用多阶段构建可以显著的减少最终生成的镜像的体积。下面是一个多阶段构建的例子,通过使用 COPY 命令从 build 镜像中拷贝编译好的二进制文件到一个崭新的基础镜像中运行,可以直接越过中间的各种编译的层,减少镜像的体积。
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"]
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 不要安装不必要的包
# 解耦应用
一个 docker 容器不应该做太多的事,可以使用多个 docker 容器来分别部署不相关的应用,让他们通过容器网络来交互。
# 对多行参数排序
举例如下,多个包的名字应该通过 \
来换行,并且和包名称前空一个空格,使得结构看起来更加清晰。
RUN apt-get update && apt-get install -y \
bzr \
cvs \
git \
mercurial \
subversion
2
3
4
5
6
# 利用构建缓存
Dockerfile 在构建时按照其中的指令顺序构建,每次构建都会生成一个中间镜像。下一条指令会根据上一个中间镜像的结果来构建。所以每次检查指令的时候, docker 会寻找是否有镜像缓存可以复用,这样就不需要创建一个新的中间镜像。如果不想使用缓存,可以在构建的时候使用 --no-cache
。
docker daemnon 通过 Dockerfile 构建镜像时,当发现即将新构建出的镜像与已有的某镜像重复时,可以选择放弃构建新的镜像,而是选用已有的镜像作为构建结果,也就是采取本地已经 cache 的镜像作为结果。
缓存的命中规则如下:
- 基本原则是从已在缓存中的父镜像开始,将下一条指令与从该基本镜像派生的所有子映像进行比较,以查看是否其中一个是使用完全相同的指令构建的。如果不是,则缓存无效。
- 对于
ADD
和COPY
指令,镜像中文件的内容被检查,然后对每个文件计算一个校验和,校验和中不关注上次修改时间和上次访问时间。在检查缓存的时候,将校验和和已有的镜像做比较,如果相等,则使用缓存,如果不相等,该镜像及后续所有的构建缓存全部无效。 - 除却这两个命令的其他命令,缓存都不会通过检查文件来确定缓存是否改变。举个例子,
RUN apt-get -y update
这个指令不会通过比较文件变化来确定缓存,仅仅通过比较指令的字符串有没有发生变化来确定是否使用缓存。 - 一旦缓存无效,dockerfile 中后续所有的指令都会生成新的镜像来构建。
# 利用缓存的最佳实践
由于 docker 缓存的机制,要将不长变更的改动放在 Dockerfile 的最前面,比如 RUN apt update
或者 RUN curl xxx
等,同时,针对不同语言,例如 nodejs,应该将 package.json 文件先拷贝进来再执行 npm install
,这样当 nodejs 里其他文件变更并不会导致 npm install
的执行,从而加快构建。对于 golang 来说,同样的道理,应该先拷贝 go.mod
和 go.sum
,再执行 go mod download
, 这样可以有效的缓存安装的依赖,加快构建。
# 不要在 Dockerfile 中映射公有端口
例如下面这种,这样会给应用的部署带来不确定性,应该通过 -p 来在运行时指定。
#private and public mapping
EXPOSE 80:8080
#private only
EXPOSE 80
2
3
4
5