使用Dockerfile创建镜像
- 3、创建镜像
- 3.1、命令选项
- 3.2、选择父镜像
- 3.3、使用.dockerignore文件
- 3.4、多步骤创建
- 4、最佳实践
- 5、本章小结
3、创建镜像
在Dockerfile编写完毕之后,读者可以使用docker[image]build命令来创建镜像,基本格式为:
此命令将读取指定路径(包括子目录)下的Dockerfile,并将该路径下的所有数据作为上下文(Context)发送给Docker服务端。Docker服务端在验证Dockerfile格式无误后,会逐条执行其中定义的指令。当遇到ADD、COPY和RUN指令时,会生成一层新的镜像。最终,若镜像创建成功,系统将返回最终镜像的ID。
请注意,如果上下文过大,将导致向服务端发送大量数据,从而延长创建过程。因此,除非文件是生成镜像所必需的,否则请勿将其放在上下文路径下。如果需要使用非上下文路径下的Dockerfile,则可以通过-f选项来指定其路径。要为生成的镜像指定标签信息,可以使用-t选项。可以多次使用此选项,以一次性为镜像添加多个名称。
例如,若上下文路径为/tmp/docker_builder/,且希望生成的镜像标签为builder/first_image:1.0.0,则可以使用以下命令:
3.1、命令选项
docker [image] build命令支持一系列选项,可以调整创建镜像过程的行为。选项介绍如表所示。
3.2、选择父镜像
通常生成新的镜像时都需要通过FROM指令来指定父镜像。父镜像是所生成镜像的基础,会直接影响镜像的大小和功能。
用户可以选择两种镜像作为父镜像,一种是基础镜像(baseimage),另一种是普通镜像(往往由第三方创建,基于基础镜像)。
基础镜像比较特殊,它的Dockerfile中往往不存在FROM指令,或者基于scratch镜像(FROM scratch),这意味着基础镜像在整个镜像树中处于根的位置。
下面的Dockerfile定义了一个简单的基础镜像,将用户提前编译好的二进制可执行文件binary复制到镜像中,运行容器时执行binary命令:
普通镜像也可以作为父镜像来使用,包括常见的busybox、debian、ubuntu等。
Docker不同类型镜像之间的继承关系如图所示。
3.3、使用.dockerignore文件
可以通过.dockerignore文件(每一行添加一条匹配模式)来让Docker忽略匹配路径或文件,避免在创建镜像时将无关数据发送到服务端。例如,下面的示例中包括了6行忽略的模式(第一行为注释):
.dockerignore文件中的模式语法支持Golang风格的路径匹配规则,其中,“*”表示任意多个字符;“?”代表单个字符;“!”表示不匹配(即不忽略指定的路径或文件)。
3.4、多步骤创建
Docker支持多步骤镜像创建(Multi-stage build)特性,可以精简最终生成的镜像大小。
对于需要编译的应用(如C、Go或Java语言等),通常情况下至少需要准备两个环境的Docker镜像:
- 编译环境镜像:包括完整的编译引擎、依赖库等,往往比较庞大,作用是将应用编译为二进制文件。
- 运行环境镜像:利用编译好的二进制文件,运行应用,由于不需要编译环境,因此体积比较小。
使用多步骤创建,可以在保证最终生成的运行环境镜像保持精简的情况下,使用单一的Dockerfile,降低维护复杂度。
以Go语言应用为例。创建干净目录,进入目录中,创建main.go文件,内容为:
创建Dockerfile,使用golang:1.20镜像编译应用二进制文件为app,使用精简的镜像alpine:latest作为运行环境。Dockerfile的完整内容为:
执行如下命令创建镜像,并运行应用:
查看生成的最终镜像,大小只有6.55MB:
4、最佳实践
最佳实践的目标是根据需求来定制适合自己并且高效、便捷的镜像。
首先,读者要深入了解每个指令的含义和执行效果,亲自编写一些简单的示例进行测试,确保充分理解后再编写正式的Dockerfile。此外,Docker Hub官方仓库中提供了大量优秀的镜像和对应的Dockerfile,读者可以通过阅读它们来学习如何编写高效的Dockerfile。
下面是一些构建镜像的经验。读者在生成镜像的过程中可以参考借鉴,以完善所生成的镜像。
- 精简镜像用途:尽量让每个镜像的用途都比较集中、单一,避免构造庞大而复杂的多功能镜像。
- 选择合适的基础镜像:容器的核心是应用。选择过大的父镜像(如Ubuntu系统镜像)会造成最终生成应用镜像的臃肿,通常建议选用精简的应用镜像(如node:slim),或者较为轻量级的系统镜像(如alpine、busybox或debian)。
- 提供注释和维护者信息:Dockerfile也是一种代码,需要考虑方便后续扩展和他人使用。
- 正确使用版本号:使用明确的版本号信息,如1.0、2.0,而非依赖于默认的latest。通过版本号可以避免环境不一致导致的问题。
- 减少镜像层数:为了生成尽量少的镜像层数,可以尝试合并RUN、ADD和COPY指令。通常情况下,多条RUN指令可以合并为一条RUN指令。
- 恰当使用多步骤创建(17.05+版本支持):通过多步骤创建,可以将编译和运行过程分开,确保最终生成的镜像仅包含运行应用所需的最小化环境。当然,读者也可以分别构建编译镜像和运行镜像来达到类似的效果,但这种方式需要维护多个Dockerfile。
- 使用.dockerignore文件:利用该文件可以标记在执行docker build时忽略的路径和文件,避免发送不必要的数据内容,从而加快整个镜像的创建过程。
- 及时删除临时文件和缓存文件:例如在执行apt-get指令后,/var/cache/apt目录下可能会缓存一些安装包,需要及时清理。
- 提高生成速度:例如,合理使用缓存、减少内容目录下的文件,或使用.dockerignore文件指定等,以加快镜像创建速度。
- 调整合理的指令顺序:在开启缓存的情况下,内容不变的指令尽量放在前面,以便最大限度地复用缓存。
- 减少外部源的干扰:如果确实需要从外部引入数据,则务必指定持久且带版本信息的地址,以便其他人能够重复使用而不会出错。
遵循这些最佳实践,读者将能够创建出高效、易于维护和使用的Docker镜像,从而优化容器部署和应用管理过程。
5、本章小结
本章重点讲解使用Dockerfile创建镜像的整个过程。内容包括Dockerfile的基本结构、支持的内部指令、使用Dockerfile创建镜像的基本流程,以及合理构建镜像的最佳实践。
通过学习Dockerfile构建镜像的过程,读者将能领略到Docker镜像在使用中所展现的灵活性,尤其是在进行修改时,能够实现“一处修改,全局更新”。
然而,编写一个高质量的Dockerfile并非易事,这需要不断地进行学习和实践。
在本书的第二部分,笔者精选了多个具有代表性的Dockerfile实例,帮助读者更好地理解镜像构建原理并参考实例进行实践。