You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

291 lines
16 KiB
Markdown

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 46 | 如何制作Docker镜像
你好,我是孔令飞。
要落地云原生架构其中的一个核心点是通过容器来部署我们的应用。如果要使用容器来部署应用那么制作应用的Docker镜像就是我们绕不开的关键一步。今天我就来详细介绍下如何制作Docker镜像。
在这一讲中我会先讲解下Docker镜像的构建原理和方式然后介绍Dockerfile的指令以及如何编写Dockerfile文件。最后介绍下编写Dockerfile文件时要遵循的一些最佳实践。
## Docker镜像的构建原理和方式
首先我们来看下Docker镜像构建的原理和方式。
我们可以用多种方式来构建一个Docker镜像最常用的有两种
* 通过`docker commit`命令,基于一个已存在的容器构建出镜像。
* 编写Dockerfile文件并使用`docker build`命令来构建镜像。
上面这两种方法中镜像构建的底层原理是相同的都是通过下面3个步骤来构建镜像
1. 基于原镜像启动一个Docker容器。
2. 在容器中进行一些操作,例如执行命令、安装文件等。由这些操作产生的文件变更都会被记录在容器的存储层中。
3. 将容器存储层的变更commit到新的镜像层中并添加到原镜像上。
下面我们来具体讲解这两种构建Docker镜像的方式。
### 通过`docker commit`命令构建镜像
我们可以通过`docker commit`来构建一个镜像,命令的格式为`docker commit [选项] [<仓库名>[:<标签>]]`。
下图中我们通过4个步骤构建了Docker镜像`ccr.ccs.tencentyun.com/marmotedu/iam-apiserver-amd64:test`
![图片](https://static001.geekbang.org/resource/image/23/11/23619b513ff043792c7374acf7781d11.png?wh=1920x344)
具体步骤如下:
1. 执行`docker ps`获取需要构建镜像的容器ID `48d1dbb89a7f`
2. 执行`docker pause 48d1dbb89a7f`暂停`48d1dbb89a7f`容器的运行。
3. 执行`docker commit 48d1dbb89a7f ccr.ccs.tencentyun.com/marmotedu/iam-apiserver-amd64:test`基于容器ID `48d1dbb89a7f`构建Docker镜像。
4. 执行`docker images ccr.ccs.tencentyun.com/marmotedu/iam-apiserver-amd64:test`,查看镜像是否成功构建。
这种镜像构建方式通常用在下面两个场景中:
* 构建临时的测试镜像;
* 容器被入侵后,使用`docker commit`,基于被入侵的容器构建镜像,从而保留现场,方便以后追溯。
除了这两种场景,我不建议你使用`docker commit`来构建生产现网环境的镜像。我这么说的主要原因有两个:
* 使用`docker commit`构建的镜像包含了编译构建、安装软件,以及程序运行产生的大量无用文件,这会导致镜像体积很大,非常臃肿。
* 使用`docker commit`构建的镜像会丢失掉所有对该镜像的操作历史,无法还原镜像的构建过程,不利于镜像的维护。
下面,我们再来看看如何使用`Dockerfile`来构建镜像。
### 通过`Dockerfile`来构建镜像
在实际开发中,使用`Dockerfile`来构建是最常用,也最标准的镜像构建方法。`Dockerfile`是Docker用来构建镜像的文本文件里面包含了一系列用来构建镜像的指令。
`docker build`命令会读取`Dockerfile`的内容,并将`Dockerfile`的内容发送给Docker引擎最终Docker引擎会解析`Dockerfile`中的每一条指令,构建出需要的镜像。
`docker build`的命令格式为`docker build [OPTIONS] PATH | URL | -`。`PATH`、`URL`、`-`指出了构建镜像的上下文contextcontext中包含了构建镜像需要的`Dockerfile`文件和其他文件。默认情况下Docker构建引擎会查找context中名为`Dockerfile`的文件,但你可以通过`-f, --file`选项,手动指定`Dockerfile`文件。例如:
```bash
$ docker build -f Dockerfile -t ccr.ccs.tencentyun.com/marmotedu/iam-apiserver-amd64:test .
```
使用Dockerfile构建镜像本质上也是通过镜像创建容器并在容器中执行相应的指令然后停止容器提交存储层的文件变更。和用`docker commit`构建镜像的方式相比,它有三个好处:
* Dockerfile 包含了镜像制作的完整操作流程,其他开发者可以通过 Dockerfile 了解并复现制作过程。
* Dockerfile 中的每一条指令都会创建新的镜像层,这些镜像可以被 Docker Daemnon 缓存。再次制作镜像时Docker 会尽量复用缓存的镜像层using cache而不是重新逐层构建这样可以节省时间和磁盘空间。
* Dockerfile 的操作流程可以通过`docker image history [镜像名称]`查询,方便开发者查看变更记录。
这里,我们通过一个示例,来详细介绍下通过`Dockerfile`构建镜像的流程。
**首先,**我们需要编写一个`Dockerfile`文件。下面是iam-apiserver的[Dockerfile](https://github.com/marmotedu/iam/blob/v1.0.8/build/docker/iam-apiserver/Dockerfile)文件内容:
```dockerfile
FROM centos:centos8
LABEL maintainer="<colin404@foxmail.com>"
RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
RUN echo "Asia/Shanghai" > /etc/timezone
WORKDIR /opt/iam
COPY iam-apiserver /opt/iam/bin/
ENTRYPOINT ["/opt/iam/bin/iam-apiserver"]
```
这里选择`centos:centos8`作为基础镜像,是因为`centos:centos8`镜像中包含了基本的排障工具,例如`vi`、`cat`、`curl`、`mkdir`、`cp`等工具。
**接着,**执行`docker build`命令来构建镜像:
```bash
$ docker build -f Dockerfile -t ccr.ccs.tencentyun.com/marmotedu/iam-apiserver-amd64:test .
```
执行`docker build`后的构建流程为:
**第一步**`docker build`会将context中的文件打包传给Docker daemon。如果context中有`.dockerignore`文件,则会从上传列表中删除满足`.dockerignore`规则的文件。
这里有个例外,如果`.dockerignore`文件中有`.dockerignore`或者`Dockerfile``docker build`命令在排除文件时会忽略掉这两个文件。如果指定了镜像的tag还会对repository和tag进行验证。
**第二步**`docker build`命令向Docker server发送HTTP请求请求Docker server构建镜像请求中包含了需要的context信息。
**第三步**Docker server接收到构建请求之后会执行以下流程来构建镜像
1. 创建一个临时目录并将context中的文件解压到该目录下。
2. 读取并解析Dockerfile遍历其中的指令根据命令类型分发到不同的模块去执行。
3. Docker构建引擎为每一条指令创建一个临时容器在临时容器中执行指令然后commit容器生成一个新的镜像层。
4. 最后将所有指令构建出的镜像层合并形成build的最后结果。最后一次commit生成的镜像ID就是最终的镜像ID。
为了提高构建效率,`docker build`默认会缓存已有的镜像层。如果构建镜像时发现某个镜像层已经被缓存,就会直接使用该缓存镜像,而不用重新构建。如果不希望使用缓存的镜像,可以在执行`docker build`命令时,指定`--no-cache=true`参数。
Docker匹配缓存镜像的规则为遍历缓存中的基础镜像及其子镜像检查这些镜像的构建指令是否和当前指令完全一致如果不一样则说明缓存不匹配。对于`ADD`、`COPY`指令还会根据文件的校验和checksum来判断添加到镜像中的文件是否相同如果不相同则说明缓存不匹配。
这里要注意,缓存匹配检查不会检查容器中的文件。比如,当使用`RUN apt-get -y update`命令更新了容器中的文件时,缓存策略并不会检查这些文件,来判断缓存是否匹配。
最后,我们可以通过`docker history`命令来查看镜像的构建历史,如下图所示:
![图片](https://static001.geekbang.org/resource/image/f3/06/f32a0e2f90a36995a561747519897806.png?wh=1854x345)
### 其他制作镜像方式
上面介绍的是两种最常用的镜像构建方式,还有一些其他的镜像创建方式,这里我简单介绍两种。
1. 通过`docker save`和`docker load`命令构建
`docker save`用来将镜像保存为一个tar文件`docker load`用来将tar格式的镜像文件加载到当前机器上例如
```bash
# 在 A 机器上执行,并将 nginx-v1.0.0.tar.gz 复制到 B 机器
$ docker save nginx | gzip > nginx-v1.0.0.tar.gz
# 在 B 机器上执行
$ docker load -i nginx-v1.0.0.tar.gz
```
通过上面的命令我们就在机器B上创建了`nginx`镜像。
2. 通过`docker export`和`docker import`命令构建
我们先通过`docker export` 保存镜像,再通过`docker import` 加载镜像,具体命令如下:
```bash
# 在 A 机器上执行,并将 nginx-v1.0.0.tar.gz 复制到 B 机器
$ docker export nginx > nginx-v1.0.0.tar.gz
# 在 B 机器上执行
$ docker import - nginx:v1.0.0 nginx-v1.0.0.tar.gz
```
通过`docker export`导出的镜像和通过`docker save`保存的镜像相比,会丢失掉所有的镜像构建历史。在实际生产环境中,我不建议你通过`docker save`和`docker export`这两种方式来创建镜像。我比较推荐的方式是在A机器上将镜像push到镜像仓库在B机器上从镜像仓库pull该镜像。
## Dockerfile指令介绍
上面我介绍了一些与Docker镜像构建有关的基础知识。在实际生产环境中我们标准的做法是通过Dockerfile来构建镜像这就要求你会编写Dockerfile文件。接下来我就详细介绍下如何编写Dockerfile文件。
Dockerfile指令的基本格式如下
```dockerfile
# Comment
INSTRUCTION arguments
```
`INSTRUCTION`是指令不区分大小写但我的建议是指令都大写这样可以与参数进行区分。Dockerfile中`#` 开头的行是注释,而在其他位置出现的 `#` 会被当成参数,例如:
```dockerfile
# Comment
RUN echo 'hello world # dockerfile'
```
一个Dockerfile文件中包含了多条指令这些指令可以分为5类。
* 定义基础镜像的指令:**FROM**
* 定义镜像维护者的指令:**MAINTAINER**(可选);
* 定义镜像构建过程的指令:**COPY**、ADD、**RUN**、USER、**WORKDIR**、ARG、**ENV**、VOLUME、**ONBUILD**
* 定义容器启动时执行命令的指令:**CMD**、**ENTRYPOINT**
* 其他指令EXPOSE、HEALTHCHECK、STOPSIGNAL。
其中加粗的指令是编写Dockerfile时经常用到的指令需要你重点了解下。我把这些常用Dockerfile指令的介绍放在了GitHub上你可以看看这个[Dockerfile指令详解](https://github.com/marmotedu/geekbang-go/blob/master/Dockerfile%E6%8C%87%E4%BB%A4%E8%AF%A6%E8%A7%A3.md)。
下面是一个Dockerfile示例
```dockerfile
# 第一行必须指定构建该镜像所基于的容器镜像
FROM centos:centos8
# 维护者信息
MAINTAINER Lingfei Kong <colin404@foxmail.com>
# 镜像的操作指令
RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
RUN echo "Asia/Shanghai" > /etc/timezone
WORKDIR /opt/iam
COPY iam-apiserver /opt/iam/bin/
# 容器启动时执行指令
ENTRYPOINT ["/opt/iam/bin/iam-apiserver"]
```
Docker会顺序解释并执行Dockerfile中的指令并且第一条指令必须是`FROM``FROM` 用来指定构建镜像的基础镜像。接下来,一般会指定镜像维护者的信息。后面是镜像操作的指令,最后会通过`CMD`或者`ENTRYPOINT`来指定容器启动的命令和参数。
## Dockerfile最佳实践
上面我介绍了Dockerfile的指令但在编写Dockerfile时只知道这些指令是不够的还不能编写一个合格的Dockerfile。我们还需要遵循一些编写 Dockerfile的最佳实践。这里我总结了一份编写 Dockerfile的最佳实践清单你可以参考。
1. 建议所有的Dockerfile指令大写这样做可以很好地跟在镜像内执行的指令区分开来。
2. 在选择基础镜像时尽量选择官方的镜像并在满足要求的情况下尽量选择体积小的镜像。目前Linux镜像大小有以下关系`busybox < debian < centos < ubuntu`。最好确保同一个项目中使用一个统一的基础镜像。如无特殊需求,可以选择使用`debian:jessie`或者`alpine`。
3. 在构建镜像时,删除不需要的文件,只安装需要的文件,保持镜像干净、轻量。
4. 使用更少的层,把相关的内容放到一个层,并使用换行符进行分割。这样可以进一步减小镜像的体积,也方便查看镜像历史。
5. 不要在Dockerfile中修改文件的权限。因为如果修改文件的权限,Docker在构建时会重新复制一份,这会导致镜像体积越来越大。
6. 给镜像打上标签,标签可以帮助你理解镜像的功能,例如:`docker build -t="nginx:3.0-onbuild"`。
7. `FROM`指令应该包含tag,例如使用`FROM debian:jessie`,而不是`FROM debian`。
8. 充分利用缓存。Docker构建引擎会顺序执行Dockerfile中的指令,而且一旦缓存失效,后续命令将不能使用缓存。为了有效地利用缓存,需要尽量将所有的Dockerfile文件中相同的部分都放在前面,而将不同的部分放在后面。
9. 优先使用`COPY`而非`ADD`指令。和`ADD`相比,`COPY` 功能简单,而且也够用。`ADD`可变的行为会导致该指令的行为不清晰,不利于后期维护和理解。
10. 推荐将`CMD`和`ENTRYPOINT`指令结合使用,使用execl格式的`ENTRYPOINT`指令设置固定的默认命令和参数,然后使用`CMD`指令设置可变的参数。
11. 尽量使用Dockerfile共享镜像。通过共享Dockerfile,可以使开发者明确知道Docker镜像的构建过程,并且可以将Dockerfile文件加入版本控制,跟踪起来。
12. 使用`.dockerignore`忽略构建镜像时非必需的文件。忽略无用的文件,可以提高构建速度。
13. 使用多阶段构建。多阶段构建可以大幅减小最终镜像的体积。例如,`COPY`指令中可能包含一些安装包,安装完成之后这些内容就废弃掉。下面是一个简单的多阶段构建示例:
```dockerfile
FROM golang:1.11-alpine AS build
# 安装依赖包
RUN go get github.com/golang/mock/mockgen
# 复制源码并执行build此处当文件有变化会产生新的一层镜像层
COPY . /go/src/iam/
RUN go build -o /bin/iam
# 缩小到一层镜像
FROM busybox
COPY --from=build /bin/iam /bin/iam
ENTRYPOINT ["/bin/iam"]
CMD ["--help"]
```
## 总结
如果你想使用Docker容器来部署应用,那么就需要制作Docker镜像。今天,我介绍了如何制作Docker镜像。
你可以使用这两种方式来构建Docker镜像:
* 通过 `docker commit` 命令,基于一个已存在的容器构建出镜像。
* 通过编写Dockerfile文件,并使用 `docker build` 命令来构建镜像。
这两种方法中,镜像构建的底层原理是相同的:
1. 基于原镜像启动一个Docker容器。
2. 在容器中进行一些操作,例如执行命令、安装文件等,由这些操作产生的文件变更都会被记录在容器的存储层中。
3. 将容器存储层的变更commit到新的镜像层中,并添加到原镜像上。
此外,我们还可以使用 `docker save` / `docker load` `docker export` / `docker import` 来复制Docker镜像。
在实际生产环境中,我们标准的做法是通过Dockerfile来构建镜像。使用Dockerfile构建镜像,就需要你编写Dockerfile文件。Dockerfile支持多个指令,这些指令可以分为5类,对指令的具体介绍你可以再返回复习一遍。
另外,我们在构建Docker镜像时,也要遵循一些最佳实践,具体你可以参考我给你总结的最佳实践清单。
## 课后练习
1. 思考下,为什么在编写Dockerfile时,“把相关的内容放到一个层,使用换行符 `\` 进行分割”可以减小镜像的体积?
2. 尝试一下,为你正在开发的应用编写Dockerfile文件,并成功构建出Docker镜像。
欢迎你在留言区与我交流讨论,我们下一讲见。