本文就Docker下构建镜像的两种方式作相关介绍
docker commit命令
先创建一个Ubuntu 18.04的容器
1 | docker pull ubuntu:18.04 |
在ubuntu-1容器中安装tree命令
1 | # 进入 ubuntu-1 容器 |
ubuntu-1容器中使用tree命令效果如下,说明tree命令安装成功
使用docker commit命令以ubuntu-1容器来构建镜像,如下所示。其中,-m用于描述提交信息,-a用于描述作者。用户仓库的命名由用户名、仓库名两部分组成,例如aaron1995/custom-ubuntu
1 | # 创建镜像aaron1995/custom-ubuntu, 其中tag为1.0 |
效果如下所示
至此一个包含tree命令的镜像就已经构建完毕了,后续我们就可以直接通过该镜像来创建容器进行使用。而不必每次都利用ubuntu:18.04镜像创建容器,然后再在容器中安装tree命令
对于我们自行构建的镜像,使用过程也并无二异,命令如下所示
1 | docker run -it -d \ |
测试效果如下符合预期
推送至Docker Hub
推送至Docker Hub
我们还可以将我们的镜像推送到Docker Hub,以方便共享给他人。现在Docker Hub当中建立一个名为custom-ubuntu的仓库。需要注意的是,仓库名称(aaron1995/custom-ubuntu)中的用户名(aaron1995)必须和Docker Hub账号的用户名保持一致,否则会推送失败。具体地,推送流程如下所示
1 | # 登录Docker Hub, 并输入账号密码 |
具体地,如下所示
1 | # 登陆 Docker Hub 账号,并输入账号、密码 |
在Docker Hub中查看,符合预期
Dockerfile
Demo
事实上,我们更推荐使用Dockerfile来构建镜像。其通过一系列指令来描述镜像的构建过程,下面即是一个简单的通过Dockerfile构建镜像的示例
1 | # 通过FROM指令 指定以 ubuntu:18.04 作为基础镜像 |
效果如下所示
然后通过docker build命令对该Dockerfile文件构建镜像,该命令需在Dockerfile文件所在目录下执行
1 | # 对当前目录下的Dockerfile文件构建镜像,-t选项设置镜像名称、tag |
效果如下符合预期
现在我们来创建一个该镜像的容器,验证下
1 | docker run -d \ |
测试结果如下符合预期
指令详解
FROM
该指令用于指定我们自定义镜像的基础镜像,故第一条指令必须是FROM指令
1 | # 通过FROM指令 指定以 ubuntu:18.04 作为基础镜像 |
MAINTAINER
该指令用于描述作者信息。目前更推荐使用LABEL指令来定义作者信息等元数据
1 | # 通过MAINTAINER指令 设置作者信息 |
RUN
该指令用于描述镜像构建时需要执行的命令,其支持shell、exec两种形式的语法。示例如下
1 | # 通过RUN指令安装tree命令,其支持shell格式语法 |
由于每次RUN指令都会建立一个新的镜像层,导致最终镜像体积膨胀。所以对于shell格式的多次RUN指令,推荐使用&&连接并利用反斜杠(\)进行换行。示例如下所示
1 | # 优化前: 多条RUN指令 |
CMD
该指令和RUN指令很类似,都是用于运行命令的。只不过后者用于指定镜像构建时需要运行的命令,而前者则指定容器被启动时需要运行的命令。Docker推荐使用exec格式语法,例如上文中我们通过CMD指令设置设置Nginx前台运行
需要注意的是:
- 如果Dockerfile文件中存在多条CMD指令,则只有最后一条CMD指令才会生效
- docker run中如果指定了命令,则其会覆盖Dockerfile中的CMD指令,导致后者失效。这里我们尝试创建一个新的容器,并在docker run中添加一个ls命令,如果可以覆盖Dockerfile中的CMD指令,则该容器创建后一会儿就会结束退出。因为Nginx是以后台的方式运行的
1 | # docker run中指定了要执行的命令ls |
测试结果如下,符合预期
也正是因为此,很多时候我们容器创建过程中无需显式指定需要执行程序/命令,就是因为该镜像通过CMD指令设置了默认行为。例如我们通过docker inspect查看下redis的镜像信息,可以看到该镜像通过CMD指令设置了容器默认执行redis-server命令
故下述两种创建redis容器的方式,本质是一样的
1 | # 方式1: 创建redis容器, 显式执行 redis-server 命令 |
EXPOSE
该指令用于声明镜像所使用的端口,用于帮助镜像使用者了解该镜像所使用的端口信息。但并不会对外暴露相关端口,端口的映射需要在创建容器过程中通过-p、-P选项实现
1 | # 声明端口及协议, 如果不指定协议默认为TCP |
VOLUME
定义匿名数据卷。即在镜像中创建一个挂载目录,默认使用docker管理的匿名数据卷,也可通过docker run命令的-v选项挂载到宿主机上的指定目录或数据卷。与docker run命令的-v选项不同,Dockerfile中不能指定宿主机目录
1 | # 指定镜像的挂载目录, 如果目录不存在会自动创建 |
dockerfile中定义了两个挂载目录,启动该容器后。效果如下所示,docker run命令中由于未使用-v选项,故其默认挂载匿名数据卷
WORKDIR
定义工作目录。一方面,其会自动创建相应目录;另一方面,其设置后续指令(RUN、CMD、COPY等)的工作目录,类似于Linux的cd命令效果
1 | WORKDIR <路径> |
WORKDIR指令可以在一个Dockerfile中使用多次。如果使用了相对路径,它将相对于前一条WORKDIR指令的路径。例如:
1 | WORKDIR /a |
最终pwd命令将会输出/a/b/c,同时进入该容器也会发现存在/a/b/c路径
COPY
复制宿主机文件到容器内。首先要求源文件位于Dockerfile所在的目录下,如下所示
然后通过COPY指令进行复制
1 | # 复制game.txt文件到/home/down1/目录中 |
容器内效果如下所示,符合预期。与此同时对于目标目录而言,如果不存在则会自动进行创建
LABEL
通过该指令添加元数据。如果值中包含空格,可使用引号或反斜杠(\)
1 | # 通过LABEL指令添加元数据 |
我们可通过docker inspect命令来查看容器的元数据,效果如下所示
ENV
通过该指令定义环境变量。类似地,如果值中包含空格,可使用引号或反斜杠(\)
1 | # 通过ENV指令定义环境变量 |
事实上,创建容器时还可以通过 docker run —env
1 | docker run -itd --env MY_CAT="Bob Tony" \ |
测试结果如下,符合预期
ARG
通过该指令用于声明镜像构建过程中的变量,然后在Dockerfile中通过${paramName}方式来进行引用。如下所示
1 | # 声明fileName变量并设置默认值为testService |
在镜像构建过程中,通过 —build-arg 来传入变量最终的值即可。如果存在多个变量,需要多个 —build-arg 选项来进行设置。显然镜像一旦构建完成后,通过ARG命令声明的变量就不复存在了
1 | docker build -t="aaron1995/dockerfile-demo:1.0" \ |
Note
- 在docker build命令的最后,我们还指定了一个目录。如下图所示,其中小圆点.表示的是当前路径。因为Docker是以C/S架构运行的,在构建过程中需要将指定目录中的所有文件一起打包发送给Server端,即Docker引擎。故不要在Dockerfile所在的目录中存放无用文件,避免导致构建过程过长
Dockerfile文件无需添加文件类型后缀
Dockerfile文件支持注释,以#开头的行即会视作为注释
Docker镜像在构建过程中利用了缓存机制。一旦有某个指令在缓存中未命中(即没有该指令对应的镜像层),则后续的整个构建过程都不会再使用缓存。故在编写Dockerfile过程中,尽量将易于发生变化的指令置于Dockerfile文件的后方执行,以便最大程度地利用缓存
参考文献
- 第一本Docker书·修订版 James Turnbull著
- 深入浅出Docker [英]Nigel Poulton著