xyZGHio

本是青灯不归客,却因浊酒恋风尘

0%

Docker之构建镜像

本文就Docker下构建镜像的两种方式作相关介绍

abstract.png

docker commit命令

先创建一个Ubuntu 18.04的容器

1
2
3
4
5
docker pull ubuntu:18.04

docker run -it -d \
--name ubuntu-1 \
ubuntu:18.04

在ubuntu-1容器中安装tree命令

1
2
3
4
5
6
7
8
# 进入 ubuntu-1 容器
docker exec -it ubuntu-1 /bin/bash
# 更新软件源
apt update
# 安装 tree 命令
apt -y install tree
# 退出 ubuntu-1 容器
exit

ubuntu-1容器中使用tree命令效果如下,说明tree命令安装成功

figure 1.jpg

使用docker commit命令以ubuntu-1容器来构建镜像,如下所示。其中,-m用于描述提交信息,-a用于描述作者。用户仓库的命名由用户名、仓库名两部分组成,例如aaron1995/custom-ubuntu

1
2
3
4
# 创建镜像aaron1995/custom-ubuntu, 其中tag为1.0
docker commit -m "add tree 2 in ubuntu" \
-a "Aaron Zhu" \
ubuntu-1 aaron1995/custom-ubuntu:1.0

效果如下所示

figure 2.jpg

至此一个包含tree命令的镜像就已经构建完毕了,后续我们就可以直接通过该镜像来创建容器进行使用。而不必每次都利用ubuntu:18.04镜像创建容器,然后再在容器中安装tree命令

对于我们自行构建的镜像,使用过程也并无二异,命令如下所示

1
2
3
docker run -it -d \
--name ubuntu-2 \
aaron1995/custom-ubuntu:1.0

测试效果如下符合预期

figure 3.jpg

推送至Docker Hub

推送至Docker Hub

我们还可以将我们的镜像推送到Docker Hub,以方便共享给他人。现在Docker Hub当中建立一个名为custom-ubuntu的仓库。需要注意的是,仓库名称(aaron1995/custom-ubuntu)中的用户名(aaron1995)必须和Docker Hub账号的用户名保持一致,否则会推送失败。具体地,推送流程如下所示

1
2
3
4
5
6
7
8
# 登录Docker Hub, 并输入账号密码
docker login -u <Docker Hub用户名>

# 将 本地镜像 与 Docker Hub仓库进行关联
docker tag <本地镜像名称>:<标签> <Docker Hub用户名>/<Docker Hub仓库名>:<标签>

# 推送至 Docker Hub
docker push <Docker Hub用户名>/<Docker Hub仓库名>:<标签>

具体地,如下所示

1
2
3
4
5
6
# 登陆 Docker Hub 账号,并输入账号、密码
docker login -u aaron1995

docker tag custom-ubuntu:1.0 aaron1995/custom-ubuntu:1.0

docker push aaron1995/custom-ubuntu:1.0

在Docker Hub中查看,符合预期

figure 5.jpg

Dockerfile

Demo

事实上,我们更推荐使用Dockerfile来构建镜像。其通过一系列指令来描述镜像的构建过程,下面即是一个简单的通过Dockerfile构建镜像的示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 通过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

效果如下所示

figure 6.jpg

然后通过docker build命令对该Dockerfile文件构建镜像,该命令需在Dockerfile文件所在目录下执行

1
2
# 对当前目录下的Dockerfile文件构建镜像,-t选项设置镜像名称、tag
docker build -t="aaron1995/dockerfile-demo:1.0" .

效果如下符合预期

figure 7.jpg

现在我们来创建一个该镜像的容器,验证下

1
2
3
4
docker run -d \
--name dockerfile-demo-1 \
-p 4321:80 \
aaron1995/dockerfile-demo:1.0

测试结果如下符合预期

figure 8.jpg

指令详解

FROM

该指令用于指定我们自定义镜像的基础镜像,故第一条指令必须是FROM指令

1
2
# 通过FROM指令 指定以 ubuntu:18.04 作为基础镜像
FROM ubuntu:18.04

MAINTAINER

该指令用于描述作者信息。目前更推荐使用LABEL指令来定义作者信息等元数据

1
2
# 通过MAINTAINER指令 设置作者信息
MAINTAINER Aaron Zhu "zgh@163.com"

RUN

该指令用于描述镜像构建时需要执行的命令,其支持shell、exec两种形式的语法。示例如下

1
2
3
4
5
# 通过RUN指令安装tree命令,其支持shell格式语法
RUN apt install -y tree

# 通过RUN指令安装tree命令,其支持exec格式语法
RUN ["apt", "install", "-y", "tree"]

由于每次RUN指令都会建立一个新的镜像层,导致最终镜像体积膨胀。所以对于shell格式的多次RUN指令,推荐使用&&连接并利用反斜杠(\)进行换行。示例如下所示

1
2
3
4
5
6
7
8
9
# 优化前: 多条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前台运行

需要注意的是:

  1. 如果Dockerfile文件中存在多条CMD指令,则只有最后一条CMD指令才会生效
  2. docker run中如果指定了命令,则其会覆盖Dockerfile中的CMD指令,导致后者失效。这里我们尝试创建一个新的容器,并在docker run中添加一个ls命令,如果可以覆盖Dockerfile中的CMD指令,则该容器创建后一会儿就会结束退出。因为Nginx是以后台的方式运行的
1
2
3
4
5
6
# docker run中指定了要执行的命令ls
docker run -d \
--name dockerfile-demo-2 \
-p 5321:80 \
aaron1995/dockerfile-demo:1.0 \
ls

测试结果如下,符合预期

figure 9.jpg

也正是因为此,很多时候我们容器创建过程中无需显式指定需要执行程序/命令,就是因为该镜像通过CMD指令设置了默认行为。例如我们通过docker inspect查看下redis的镜像信息,可以看到该镜像通过CMD指令设置了容器默认执行redis-server命令

figure 10.jpg

故下述两种创建redis容器的方式,本质是一样的

1
2
3
4
5
6
7
8
9
10
11
12
# 方式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选项实现

1
2
3
4
5
6
7
8
# 声明端口及协议, 如果不指定协议默认为TCP
EXPOSE <port>/<protocol>

# 声明80端口, 使用TCP协议
EXPOSE 80

# 声明80端口, 使用UDP协议
EXPOSE 80/udp

VOLUME

定义匿名数据卷。即在镜像中创建一个挂载目录,默认使用docker管理的匿名数据卷,也可通过docker run命令的-v选项挂载到宿主机上的指定目录或数据卷。与docker run命令的-v选项不同,Dockerfile中不能指定宿主机目录

1
2
3
4
5
6
7
8
# 指定镜像的挂载目录, 如果目录不存在会自动创建
VOLUME <路径>

# 指定镜像的多个挂载目录, 如果目录不存在会自动创建
VOLUME ["<路径1>", "<路径2>"]

# 指定镜像的挂载目录, 如果目录不存在会自动创建
VOLUME ["/home/aaron", "/home/aaron/data"]

dockerfile中定义了两个挂载目录,启动该容器后。效果如下所示,docker run命令中由于未使用-v选项,故其默认挂载匿名数据卷

figure 11.jpg

WORKDIR

定义工作目录。一方面,其会自动创建相应目录;另一方面,其设置后续指令(RUN、CMD、COPY等)的工作目录,类似于Linux的cd命令效果

1
WORKDIR <路径>

WORKDIR指令可以在一个Dockerfile中使用多次。如果使用了相对路径,它将相对于前一条WORKDIR指令的路径。例如:

1
2
3
4
WORKDIR /a
WORKDIR b
WORKDIR c
RUN pwd

最终pwd命令将会输出/a/b/c,同时进入该容器也会发现存在/a/b/c路径

COPY

复制宿主机文件到容器内。首先要求源文件位于Dockerfile所在的目录下,如下所示

figure 12.jpg

然后通过COPY指令进行复制

1
2
3
4
5
6
# 复制game.txt文件到/home/down1/目录中
COPY game.txt /home/down1/
# 复制源目录picture下的所有文件到/home/down1/目录下
COPY picture/ /home/down1/
# 将down2视作文件,复制mathBook.txt文件内容覆盖写入其中
COPY mathBook.txt /home/down2

容器内效果如下所示,符合预期。与此同时对于目标目录而言,如果不存在则会自动进行创建

figure 13.jpg

LABEL

通过该指令添加元数据。如果值中包含空格,可使用引号或反斜杠(\)

1
2
3
4
5
6
7
8
9
10
11
# 通过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命令来查看容器的元数据,效果如下所示

figure 14.jpg

ENV

通过该指令定义环境变量。类似地,如果值中包含空格,可使用引号或反斜杠(\)

1
2
3
4
5
6
7
# 通过ENV指令定义环境变量
LABEL <key>=<value>

# 示例
ENV MY_NAME="Aaron Zhu"
ENV MY_JOB=software\ engineer
ENV MY_CAT=Tom

事实上,创建容器时还可以通过 docker run —env = 实现修改环境变量的值。示例如下所示

1
2
3
docker run -itd --env MY_CAT="Bob Tony" \
--name dockerfiledemo-02 \
aaron1995/dockerfile-demo:1.0

测试结果如下,符合预期

figure 15.jpg

ARG

通过该指令用于声明镜像构建过程中的变量,然后在Dockerfile中通过${paramName}方式来进行引用。如下所示

1
2
3
4
5
6
7
# 声明fileName变量并设置默认值为testService
ARG fileName=testService

# 声明jarFileName变量
ARG jarFileName
# 在Dockerfile中引用该变量
COPY target/${jarFileName}.jar app.jar

在镜像构建过程中,通过 —build-arg 来传入变量最终的值即可。如果存在多个变量,需要多个 —build-arg 选项来进行设置。显然镜像一旦构建完成后,通过ARG命令声明的变量就不复存在了

1
2
docker build -t="aaron1995/dockerfile-demo:1.0" \
--build-arg jarFileName=springbootdemo .

Note

  • 在docker build命令的最后,我们还指定了一个目录。如下图所示,其中小圆点.表示的是当前路径。因为Docker是以C/S架构运行的,在构建过程中需要将指定目录中的所有文件一起打包发送给Server端,即Docker引擎。故不要在Dockerfile所在的目录中存放无用文件,避免导致构建过程过长

figure 16.jpg

  • Dockerfile文件无需添加文件类型后缀

  • Dockerfile文件支持注释,以#开头的行即会视作为注释

  • Docker镜像在构建过程中利用了缓存机制。一旦有某个指令在缓存中未命中(即没有该指令对应的镜像层),则后续的整个构建过程都不会再使用缓存。故在编写Dockerfile过程中,尽量将易于发生变化的指令置于Dockerfile文件的后方执行,以便最大程度地利用缓存

参考文献

  1. 第一本Docker书·修订版 James Turnbull著
  2. 深入浅出Docker [英]Nigel Poulton著
请我喝杯咖啡捏~

欢迎关注我的微信公众号:青灯抽丝