Docker之構(gòu)建鏡像
本文就Docker下構(gòu)建鏡像的兩種方式作相關(guān)介紹

docker commit命令
先創(chuàng)建一個(gè)Ubuntu 18.04的容器
docker pull ubuntu:18.04
docker run -it -d \
--name ubuntu-1 \
ubuntu:18.04
在ubuntu-1容器中安裝tree命令
# 進(jìn)入 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命令以u(píng)buntu-1容器來構(gòu)建鏡像,如下所示。其中,-m用于描述提交信息,-a用于描述作者。用戶倉庫的命名由用戶名、倉庫名兩部分組成,例如aaron1995/custom-ubuntu
# 創(chuàng)建鏡像aaron1995/custom-ubuntu, 其中tag為1.0
docker commit -m "add tree 2 in ubuntu" \
-a "Aaron Zhu" \
ubuntu-1 aaron1995/custom-ubuntu:1.0
效果如下所示

至此一個(gè)包含tree命令的鏡像就已經(jīng)構(gòu)建完畢了,后續(xù)我們就可以直接通過該鏡像來創(chuàng)建容器進(jìn)行使用。而不必每次都利用ubuntu:18.04鏡像創(chuàng)建容器,然后再在容器中安裝tree命令
對(duì)于我們自行構(gòu)建的鏡像,使用過程也并無二異,命令如下所示
docker run -it -d \
--name ubuntu-2 \
aaron1995/custom-ubuntu:1.0
測(cè)試效果如下符合預(yù)期

推送至Docker Hub
我們還可以將我們的鏡像推送到Docker Hub,以方便共享給他人。需要注意的是,倉庫名稱(aaron1995/custom-ubuntu)中的用戶名(aaron1995)必須和Docker Hub賬號(hào)的用戶名保持一致,否則會(huì)推送失敗
# 登陸 Docker Hub 賬號(hào),并輸入賬號(hào)、密碼
docker login
docker push aaron1995/custom-ubuntu:1.0
效果如下所示

在Docker Hub中查看,符合預(yù)期

Dockerfile
Demo
事實(shí)上,我們更推薦使用Dockerfile來構(gòu)建鏡像。其通過一系列指令來描述鏡像的構(gòu)建過程,下面即是一個(gè)簡(jiǎn)單的通過Dockerfile構(gòu)建鏡像的示例
# 通過FROM指令 指定以 ubuntu:18.04 作為基礎(chǔ)鏡像
FROM ubuntu:18.04
# 通過MAINTAINER指令 設(shè)置作者信息
MAINTAINER Aaron Zhu "[email protected]"
# 通過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格式語法) 設(shè)置Nginx前臺(tái)運(yùn)行
CMD ["nginx", "-g", "daemon off;"]
# 通過EXPOSE指定 聲明鏡像使用的端口
EXPOSE 80
效果如下所示

然后通過docker build命令對(duì)該Dockerfile文件構(gòu)建鏡像,該命令需在Dockerfile文件所在目錄下執(zhí)行
# 對(duì)當(dāng)前目錄下的Dockerfile文件構(gòu)建鏡像,-t選項(xiàng)設(shè)置鏡像名稱、tag
docker build -t="aaron1995/dockerfile-demo:1.0" .
效果如下符合預(yù)期

現(xiàn)在我們來創(chuàng)建一個(gè)該鏡像的容器,驗(yàn)證下
docker run -d \
--name dockerfile-demo-1 \
-p 4321:80 \
aaron1995/dockerfile-demo:1.0
測(cè)試結(jié)果如下符合預(yù)期

指令詳解
FROM
該指令用于指定我們自定義鏡像的基礎(chǔ)鏡像,故第一條指令必須是FROM指令
# 通過FROM指令 指定以 ubuntu:18.04 作為基礎(chǔ)鏡像
FROM ubuntu:18.04
MAINTAINER
該指令用于描述作者信息。目前更推薦使用LABEL指令來定義作者信息等元數(shù)據(jù)
# 通過MAINTAINER指令 設(shè)置作者信息
MAINTAINER Aaron Zhu "[email protected]"
RUN
該指令用于描述鏡像構(gòu)建時(shí)需要執(zhí)行的命令,其支持shell、exec兩種形式的語法。示例如下
# 通過RUN指令安裝tree命令,其支持shell格式語法
RUN apt install -y tree
# 通過RUN指令安裝tree命令,其支持exec格式語法
RUN ["apt", "install", "-y", "tree"]
由于每次RUN指令都會(huì)建立一個(gè)新的鏡像層,導(dǎo)致最終鏡像體積膨脹。所以對(duì)于shell格式的多次RUN指令,推薦使用&&連接并利用反斜杠(\)進(jìn)行換行。示例如下所示
# 優(yōu)化前: 多條RUN指令
RUN apt update
RUN apt install -y tree
RUN apt install -y nginx
# 優(yōu)化后: 使用&&進(jìn)行連接, 使用\換行
RUN apt update \
&& apt install -y tree \
&& apt install -y nginx
CMD
該指令和RUN指令很類似,都是用于運(yùn)行命令的。只不過后者用于指定鏡像構(gòu)建時(shí)需要運(yùn)行的命令,而前者則指定容器被啟動(dòng)時(shí)需要運(yùn)行的命令。Docker推薦使用exec格式語法,例如上文中我們通過CMD指令設(shè)置設(shè)置Nginx前臺(tái)運(yùn)行
需要注意的是:
如果Dockerfile文件中存在多條CMD指令,則只有最后一條CMD指令才會(huì)生效 docker run中如果指定了命令,則其會(huì)覆蓋Dockerfile中的CMD指令,導(dǎo)致后者失效。這里我們嘗試創(chuàng)建一個(gè)新的容器,并在docker run中添加一個(gè)ls命令,如果可以覆蓋Dockerfile中的CMD指令,則該容器創(chuàng)建后一會(huì)兒就會(huì)結(jié)束退出。因?yàn)镹ginx是以后臺(tái)的方式運(yùn)行的
# docker run中指定了要執(zhí)行的命令ls
docker run -d \
--name dockerfile-demo-2 \
-p 5321:80 \
aaron1995/dockerfile-demo:1.0 \
ls
測(cè)試結(jié)果如下,符合預(yù)期

也正是因?yàn)榇耍芏鄷r(shí)候我們?nèi)萜鲃?chuàng)建過程中無需顯式指定需要執(zhí)行程序/命令,就是因?yàn)樵撶R像通過CMD指令設(shè)置了默認(rèn)行為。例如我們通過docker inspect查看下redis的鏡像信息,可以看到該鏡像通過CMD指令設(shè)置了容器默認(rèn)執(zhí)行redis-server命令

故下述兩種創(chuàng)建redis容器的方式,本質(zhì)是一樣的
# 方式1: 創(chuàng)建redis容器, 顯式執(zhí)行 redis-server 命令
docker run \
-d -p 6379:6379 \
--name Redis-Service \
redis:6.2.3-alpine3.13 \
redis-server
# 方式2: 創(chuàng)建redis容器, 執(zhí)行默認(rèn)命令 redis-server
docker run \
-d -p 6379:6379 \
--name Redis-Service \
redis:6.2.3-alpine3.13
EXPOSE
該指令用于聲明鏡像所使用的端口,用于幫助鏡像使用者了解該鏡像所使用的端口信息。但并不會(huì)對(duì)外暴露相關(guān)端口,端口的映射需要在創(chuàng)建容器過程中通過-p、-P選項(xiàng)實(shí)現(xiàn)
# 聲明端口及協(xié)議, 如果不指定協(xié)議默認(rèn)為TCP
EXPOSE <port>/<protocol>
# 聲明80端口, 使用TCP協(xié)議
EXPOSE 80
# 聲明80端口, 使用UDP協(xié)議
EXPOSE 80/udp
VOLUME
定義匿名數(shù)據(jù)卷。即在鏡像中創(chuàng)建一個(gè)掛載目錄,默認(rèn)使用docker管理的匿名數(shù)據(jù)卷,也可通過docker run命令的-v選項(xiàng)掛載到宿主機(jī)上的指定目錄或數(shù)據(jù)卷。與docker run命令的-v選項(xiàng)不同,Dockerfile中不能指定宿主機(jī)目錄
# 指定鏡像的掛載目錄, 如果目錄不存在會(huì)自動(dòng)創(chuàng)建
VOLUME <路徑>
# 指定鏡像的多個(gè)掛載目錄, 如果目錄不存在會(huì)自動(dòng)創(chuàng)建
VOLUME ["<路徑1>", "<路徑2>"]
# 指定鏡像的掛載目錄, 如果目錄不存在會(huì)自動(dòng)創(chuàng)建
VOLUME ["/home/aaron", "/home/aaron/data"]
dockerfile中定義了兩個(gè)掛載目錄,啟動(dòng)該容器后。效果如下所示,docker run命令中由于未使用-v選項(xiàng),故其默認(rèn)掛載匿名數(shù)據(jù)卷

WORKDIR
定義工作目錄。一方面,其會(huì)自動(dòng)創(chuàng)建相應(yīng)目錄;另一方面,其設(shè)置后續(xù)指令(RUN、CMD、COPY等)的工作目錄,類似于Linux的cd命令效果
WORKDIR <路徑>
WORKDIR指令可以在一個(gè)Dockerfile中使用多次。如果使用了相對(duì)路徑,它將相對(duì)于前一條WORKDIR指令的路徑。例如:
WORKDIR /a
WORKDIR b
WORKDIR c
RUN pwd
最終pwd命令將會(huì)輸出/a/b/c,同時(shí)進(jìn)入該容器也會(huì)發(fā)現(xiàn)存在/a/b/c路徑
COPY
復(fù)制宿主機(jī)文件到容器內(nèi)。首先要求源文件位于Dockerfile所在的目錄下,如下所示

然后通過COPY指令進(jìn)行復(fù)制
# 復(fù)制game.txt文件到/home/down1/目錄中
COPY game.txt /home/down1/
# 復(fù)制源目錄picture下的所有文件到/home/down1/目錄下
COPY picture/ /home/down1/
# 將down2視作文件,復(fù)制mathBook.txt文件內(nèi)容覆蓋寫入其中
COPY mathBook.txt /home/down2
容器內(nèi)效果如下所示,符合預(yù)期。與此同時(shí)對(duì)于目標(biāo)目錄而言,如果不存在則會(huì)自動(dòng)進(jìn)行創(chuàng)建

LABEL
通過該指令添加元數(shù)據(jù)。如果值中包含空格,可使用引號(hào)或反斜杠(\)
# 通過LABEL指令添加元數(shù)據(jù)
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命令來查看容器的元數(shù)據(jù),效果如下所示

ENV
通過該指令定義環(huán)境變量。類似地,如果值中包含空格,可使用引號(hào)或反斜杠(\)
# 通過ENV指令定義環(huán)境變量
LABEL <key>=<value>
# 示例
ENV MY_NAME="Aaron Zhu"
ENV MY_JOB=software\ engineer
ENV MY_CAT=Tom
事實(shí)上,創(chuàng)建容器時(shí)還可以通過 「docker run --env
docker run -itd --env MY_CAT="Bob Tony" \
--name dockerfiledemo-02 \
aaron1995/dockerfile-demo:1.0
測(cè)試結(jié)果如下,符合預(yù)期

Note
在docker build命令的最后,我們還指定了一個(gè)目錄。如下圖所示,其中小圓點(diǎn).表示的是當(dāng)前路徑。因?yàn)镈ocker是以C/S架構(gòu)運(yùn)行的,在構(gòu)建過程中需要將指定目錄中的所有文件一起打包發(fā)送給Server端,即Docker引擎。故不要在Dockerfile所在的目錄中存放無用文件,避免導(dǎo)致構(gòu)建過程過長

Dockerfile文件無需添加文件類型后綴
Dockerfile文件支持注釋,以#開頭的行即會(huì)視作為注釋
Docker鏡像在構(gòu)建過程中利用了緩存機(jī)制。一旦有某個(gè)指令在緩存中未命中(即沒有該指令對(duì)應(yīng)的鏡像層),則后續(xù)的整個(gè)構(gòu)建過程都不會(huì)再使用緩存。故在編寫Dockerfile過程中,盡量將易于發(fā)生變化的指令置于Dockerfile文件的后方執(zhí)行,以便最大程度地利用緩存
參考文獻(xiàn)
第一本Docker書·修訂版 James Turnbull著 深入淺出Docker [英]Nigel Poulton著
