Docker之构建镜像
本文就Docker下构建镜像的两种方式作相关介绍
docker commit命令
先创建一个Ubuntu 18.04的容器
docker pull ubuntu:18.04
docker run -it -d \
--name ubuntu-1 \
ubuntu:18.04
在ubuntu-1容器中安装tree命令
# 进入 ubuntu-1 容器
docker exec -it ubuntu-1 /bin/bash
# 更新软件源
apt update
# 安装 tree 命令
apt -y install tree
# 退出 ubuntu-1 容器
exit
ubuntu-1容器中使用tree命令效果如下,说明tree命令安装成功
使用docker commit命令以ubuntu-1容器来构建镜像,如下所示。其中,-m用于描述提交信息,-a用于描述作者。用户仓库的命名由用户名、仓库名两部分组成,例如aaron1995/custom-ubuntu
# 创建镜像aaron1995/custom-ubuntu, 其中tag为1.0
docker commit -m "add tree 2 in ubuntu" \
-a "Aaron Zhu" \
ubuntu-1 aaron1995/custom-ubuntu:1.0
效果如下所示
至此一个包含tree命令的镜像就已经构建完毕了,后续我们就可以直接通过该镜像来创建容器进行使用。而不必每次都利用ubuntu:18.04镜像创建容器,然后再在容器中安装tree命令
对于我们自行构建的镜像,使用过程也并无二异,命令如下所示
docker run -it -d \
--name ubuntu-2 \
aaron1995/custom-ubuntu:1.0
测试效果如下符合预期
推送至Docker Hub
我们还可以将我们的镜像推送到Docker Hub,以方便共享给他人。需要注意的是,仓库名称(aaron1995/custom-ubuntu)中的用户名(aaron1995)必须和Docker Hub账号的用户名保持一致,否则会推送失败
# 登陆 Docker Hub 账号,并输入账号、密码
docker login
docker push aaron1995/custom-ubuntu:1.0
效果如下所示
在Docker Hub中查看,符合预期
Dockerfile
Demo
事实上,我们更推荐使用Dockerfile来构建镜像。其通过一系列指令来描述镜像的构建过程,下面即是一个简单的通过Dockerfile构建镜像的示例
# 通过FROM指令 指定以 ubuntu:18.04 作为基础镜像
FROM ubuntu:18.04
# 通过MAINTAINER指令 设置作者信息
MAINTAINER Aaron Zhu "zgh@163.com"
# 通过RUN指令更新软件源,其支持shell格式语法
RUN apt update
# 通过RUN指令安装tree命令,其支持exec格式语法
RUN ["apt", "install", "-y", "tree"]
# 通过RUN指令安装nginx
RUN apt install -y nginx
# 通过RUN指令 修改nginx首页页面
RUN echo "Hello World, I'm Aaron" > /var/www/html/index.nginx-debian.html
# 通过CMD指令(exec格式语法) 设置Nginx前台运行
CMD ["nginx", "-g", "daemon off;"]
# 通过EXPOSE指定 声明镜像使用的端口
EXPOSE 80
效果如下所示
然后通过docker build命令对该Dockerfile文件构建镜像,该命令需在Dockerfile文件所在目录下执行
# 对当前目录下的Dockerfile文件构建镜像,-t选项设置镜像名称、tag
docker build -t="aaron1995/dockerfile-demo:1.0" .
效果如下符合预期
现在我们来创建一个该镜像的容器,验证下
docker run -d \
--name dockerfile-demo-1 \
-p 4321:80 \
aaron1995/dockerfile-demo:1.0
测试结果如下符合预期
指令详解
FROM
该指令用于指定我们自定义镜像的基础镜像,故第一条指令必须是FROM指令
# 通过FROM指令 指定以 ubuntu:18.04 作为基础镜像
FROM ubuntu:18.04
MAINTAINER
该指令用于描述作者信息。目前更推荐使用LABEL指令来定义作者信息等元数据
# 通过MAINTAINER指令 设置作者信息
MAINTAINER Aaron Zhu "zgh@163.com"
RUN
该指令用于描述镜像构建时需要执行的命令,其支持shell、exec两种形式的语法。示例如下
# 通过RUN指令安装tree命令,其支持shell格式语法
RUN apt install -y tree
# 通过RUN指令安装tree命令,其支持exec格式语法
RUN ["apt", "install", "-y", "tree"]
由于每次RUN指令都会建立一个新的镜像层,导致最终镜像体积膨胀。所以对于shell格式的多次RUN指令,推荐使用&&连接并利用反斜杠(\)进行换行。示例如下所示
# 优化前: 多条RUN指令
RUN apt update
RUN apt install -y tree
RUN apt install -y nginx
# 优化后: 使用&&进行连接, 使用\换行
RUN apt update \
&& apt install -y tree \
&& apt install -y nginx
CMD
该指令和RUN指令很类似,都是用于运行命令的。只不过后者用于指定镜像构建时需要运行的命令,而前者则指定容器被启动时需要运行的命令。Docker推荐使用exec格式语法,例如上文中我们通过CMD指令设置设置Nginx前台运行
需要注意的是:
如果Dockerfile文件中存在多条CMD指令,则只有最后一条CMD指令才会生效 docker run中如果指定了命令,则其会覆盖Dockerfile中的CMD指令,导致后者失效。这里我们尝试创建一个新的容器,并在docker run中添加一个ls命令,如果可以覆盖Dockerfile中的CMD指令,则该容器创建后一会儿就会结束退出。因为Nginx是以后台的方式运行的
# docker run中指定了要执行的命令ls
docker run -d \
--name dockerfile-demo-2 \
-p 5321:80 \
aaron1995/dockerfile-demo:1.0 \
ls
测试结果如下,符合预期
也正是因为此,很多时候我们容器创建过程中无需显式指定需要执行程序/命令,就是因为该镜像通过CMD指令设置了默认行为。例如我们通过docker inspect查看下redis的镜像信息,可以看到该镜像通过CMD指令设置了容器默认执行redis-server命令
故下述两种创建redis容器的方式,本质是一样的
# 方式1: 创建redis容器, 显式执行 redis-server 命令
docker run \
-d -p 6379:6379 \
--name Redis-Service \
redis:6.2.3-alpine3.13 \
redis-server
# 方式2: 创建redis容器, 执行默认命令 redis-server
docker run \
-d -p 6379:6379 \
--name Redis-Service \
redis:6.2.3-alpine3.13
EXPOSE
该指令用于声明镜像所使用的端口,用于帮助镜像使用者了解该镜像所使用的端口信息。但并不会对外暴露相关端口,端口的映射需要在创建容器过程中通过-p、-P选项实现
# 声明端口及协议, 如果不指定协议默认为TCP
EXPOSE <port>/<protocol>
# 声明80端口, 使用TCP协议
EXPOSE 80
# 声明80端口, 使用UDP协议
EXPOSE 80/udp
VOLUME
定义匿名数据卷。即在镜像中创建一个挂载目录,默认使用docker管理的匿名数据卷,也可通过docker run命令的-v选项挂载到宿主机上的指定目录或数据卷。与docker run命令的-v选项不同,Dockerfile中不能指定宿主机目录
# 指定镜像的挂载目录, 如果目录不存在会自动创建
VOLUME <路径>
# 指定镜像的多个挂载目录, 如果目录不存在会自动创建
VOLUME ["<路径1>", "<路径2>"]
# 指定镜像的挂载目录, 如果目录不存在会自动创建
VOLUME ["/home/aaron", "/home/aaron/data"]
dockerfile中定义了两个挂载目录,启动该容器后。效果如下所示,docker run命令中由于未使用-v选项,故其默认挂载匿名数据卷
WORKDIR
定义工作目录。一方面,其会自动创建相应目录;另一方面,其设置后续指令(RUN、CMD、COPY等)的工作目录,类似于Linux的cd命令效果
WORKDIR <路径>
WORKDIR指令可以在一个Dockerfile中使用多次。如果使用了相对路径,它将相对于前一条WORKDIR指令的路径。例如:
WORKDIR /a
WORKDIR b
WORKDIR c
RUN pwd
最终pwd命令将会输出/a/b/c,同时进入该容器也会发现存在/a/b/c路径
COPY
复制宿主机文件到容器内。首先要求源文件位于Dockerfile所在的目录下,如下所示
然后通过COPY指令进行复制
# 复制game.txt文件到/home/down1/目录中
COPY game.txt /home/down1/
# 复制源目录picture下的所有文件到/home/down1/目录下
COPY picture/ /home/down1/
# 将down2视作文件,复制mathBook.txt文件内容覆盖写入其中
COPY mathBook.txt /home/down2
容器内效果如下所示,符合预期。与此同时对于目标目录而言,如果不存在则会自动进行创建
LABEL
通过该指令添加元数据。如果值中包含空格,可使用引号或反斜杠(\)
# 通过LABEL指令添加元数据
LABEL <key>=<value>
# 通过LABEL指令添加版本信息
LABEL version=1.2.3.beta
# 通过LABEL指令添加作者信息
LABEL org.opencontainers.image.authors="Aaron Zhu"
# 通过LABEL指令添加描述信息
LABEL desc=This\ is\ a\ demo
我们可通过docker inspect命令来查看容器的元数据,效果如下所示
ENV
通过该指令定义环境变量。类似地,如果值中包含空格,可使用引号或反斜杠(\)
# 通过ENV指令定义环境变量
LABEL <key>=<value>
# 示例
ENV MY_NAME="Aaron Zhu"
ENV MY_JOB=software\ engineer
ENV MY_CAT=Tom
事实上,创建容器时还可以通过 「docker run --env
docker run -itd --env MY_CAT="Bob Tony" \
--name dockerfiledemo-02 \
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著