Dockerfile多階段構(gòu)建鏡像
之前學(xué)習(xí)部署 Docker 應(yīng)用時(shí),我們搭建過一個(gè) redis 服務(wù),然后編寫并運(yùn)行了一個(gè)統(tǒng)計(jì)訪問次數(shù)的 flask 應(yīng)用。
現(xiàn)在,我們使用 Dockerfile,將這個(gè) flask 應(yīng)用也制作成鏡像,此外,在這個(gè)鏡像中,可以包含一個(gè) helloworld 二進(jìn)制程序,這個(gè) helloworld
的源碼就是我們學(xué)習(xí) rootfs 時(shí)用到的 helloworld.c。
1. 實(shí)戰(zhàn):直接構(gòu)建鏡像
首先 我們需要新建一個(gè)目錄 dockerfiledir,用于存放 Dockerfile 文件。
mkdir dockerfiledir
# 在這個(gè)目錄下新建個(gè)空文件 Dockerfile,之后填充內(nèi)容
touch dockerfiledir/Dockerfile
新建一個(gè)目錄code,用來存放flask和c的源代碼。
mkdir code
將之前 app.py 和 helloworld.c 兩個(gè)源碼文件放入到 code 目錄下,當(dāng)前的目錄結(jié)構(gòu)應(yīng)該是這樣的:
進(jìn)入 dockerfiledir 目錄,編輯 Dockerfile 文件:
# 從 ubuntu系統(tǒng)鏡像開始構(gòu)建
FROM ubuntu
# 標(biāo)記鏡像維護(hù)者信息
MAINTAINER user <user@imooc.com>
# 切換到鏡像的/app目錄,不存在則新建此目錄
WORKDIR /app
# 將 宿主機(jī)的文件拷貝到容器中
COPY ../code/app.py .
COPY ../code/helloworld.c .
# 安裝依賴 編譯helloworld
RUN apt update >/dev/null 2>&1 && \
apt install -y gcc python3-flask python3-redis >/dev/null 2>&1 && \
cc /app/helloworld.c -o /usr/bin/helloworld
# 設(shè)定執(zhí)行用戶為user
RUN useradd user
USER user
# 設(shè)定flask所需的環(huán)境變量
ENV FLASK_APP app
# 默認(rèn)啟動(dòng)執(zhí)行的命令
CMD ["flask", "run", "-h", "0.0.0.0"]
# 將flask的默認(rèn)端口暴露出來
EXPOSE 5000
然后執(zhí)行:
docker build .
出現(xiàn)如下報(bào)錯(cuò):
COPY failed: Forbidden path outside the build context: ../code/app.py ()
解決這個(gè)問題,需要引入一個(gè)重要的概念——構(gòu)建上下文。
docker build .
命令在執(zhí)行時(shí),當(dāng)前目錄.
被指定成了構(gòu)建上下文,此目錄中的所有文件或目錄都將被發(fā)送到 Docker 引擎中去,Dockerfile中的切換目錄和復(fù)制文件等操作只會(huì)對(duì)上下文中的內(nèi)容生效。
Tips:在默認(rèn)情況下,如果不額外指定 Dockerfile 的話,會(huì)將構(gòu)建上下文對(duì)應(yīng)的目錄下 Dockerfile 的文件作為 Dockerfile。但這只是默認(rèn)行為,實(shí)際上 Dockerfile 的文件名并不要求必須為 Dockerfile,而且并不要求必須位于上下文目錄中,比如可以用
-f ../demo.txt
參數(shù)指定父級(jí)目錄的demo.txt文件作為 Dockerfile。一般來說,我們習(xí)慣使用默認(rèn)的文件名 Dockerfile,將其置于鏡像構(gòu)建上下文目錄
.
中。
我們需要將 code 目錄納入到上下文中,一個(gè)直接的方法是,調(diào)整dockerfile中的COPY指令的路徑。
# 將 .. 改為 .
COPY ./code/app.py .
COPY ./code/helloworld.c .
然后將 code 所在的目錄指定為構(gòu)建上下文。由于我們當(dāng)前的目錄是 dockerfiledir,所以我們執(zhí)行:
docker build -f ./Dockerfile ..
如果你留意查看構(gòu)建過程,會(huì)發(fā)現(xiàn)類似這樣的提示:
Sending build context to Docker daemon 421.309 MB
如果..
目錄除了code和dockerfiledir,還包含其他的文件或目錄,docker build
也會(huì)將這個(gè)數(shù)據(jù)傳輸給Docker,這會(huì)增加構(gòu)建時(shí)間。
避免這種情況,有兩種解決方法:
-
使用
.dockerignore
文件:在構(gòu)建上下文的目錄下新建一個(gè).dockerignore
文件來指定在傳遞給 docker 時(shí)需要忽略掉的文件或文件夾。.dockerignore 文件的排除模式語法和 Git 的 .gitignore 文件相似。 -
使用一個(gè)干凈的目錄作為構(gòu)建上下文(推薦):使用 Dockerfile 構(gòu)建鏡像時(shí)最好是將 Dockerfile 放置在一個(gè)新建的空目錄下。然后將構(gòu)建鏡像所需要的文件添加到該目錄中。
在我們當(dāng)前的示例中,將code目錄移入dockerfiledir。
mv ../code .
現(xiàn)在的目錄層級(jí)如下:
執(zhí)行 docker build -t myhello .
執(zhí)行構(gòu)建即可獲得我們的自定義鏡像 myhello。
使用鏡像 myhello 創(chuàng)建 myhello 容器:
# 這里使用--net=host,方便使用之前章節(jié)中部署的redis容器服務(wù),與之進(jìn)行數(shù)據(jù)交換
docker run -dit --net=host --name myhello myhello
確保部署之前的 redis 容器正常啟動(dòng),然后在 Docker 宿主機(jī)的瀏覽器中訪問http://127.0.0.1:5000
:
說明 myhello 中的 flask 應(yīng)用已經(jīng)正常運(yùn)行了。接下來,我們?cè)龠\(yùn)行測(cè)試一下編譯的 helloworld。
docker exec myhello /usr/bin/helloworld
得到輸出:
Hello, World!
Tips: myhello容器已經(jīng)完成任務(wù),記得執(zhí)行
docker rm -f myhello
刪除它.
2. 改進(jìn): 使用多階段構(gòu)建
在鏡像構(gòu)建過程中,我們的 helloworld.c 源碼以及相關(guān)編譯工具和依賴也被構(gòu)建到了鏡像中,這導(dǎo)致我們最終得到的鏡像偏大。
理想狀態(tài)應(yīng)該是使用了一個(gè)系統(tǒng)鏡像生成的容器,編譯源碼后再將編譯的程序?qū)氲阶罱K的鏡像中,這樣就會(huì)縮減體積,并且將不同目的的操作有效分離開,但是按照我們之前掌握的知識(shí),這樣實(shí)現(xiàn)需要兩個(gè)Dockerfile 文件。
使用多階段構(gòu)建,我們可以在一個(gè) Dockerfile
中使用多個(gè) FROM 語句。每個(gè) FROM 指令都可以使用不同的鏡像,并表示開始一個(gè)新的構(gòu)建階段。很方便的將一個(gè)階段的文件復(fù)制到另外一個(gè)階段,在最終的鏡像中保留下需要的內(nèi)容即可。
我們還是在 Dockerfile 文件的同一目錄,新建一個(gè)新的構(gòu)建腳本,命名為 Dockerfile-multi-stage 便于區(qū)分:
#從ubuntu鏡像開始構(gòu)建, 將第一階段命名為`build`,在其他階段需要引用的時(shí)候使用`--from=build`參數(shù)即可。
FROM ubuntu AS build
# 將宿主機(jī)的源碼拷貝到鏡像中
COPY ./code/helloworld.c .
# 安裝依賴 并編譯源碼
RUN apt update >/dev/null 2>&1 && \
apt install -y gcc >/dev/null 2>&1 && \
cc helloworld.c -o /usr/bin/helloworld
# 第二階段 從官方的python:alpine基礎(chǔ)鏡像開始構(gòu)建
FROM python:alpine
# 鏡像維護(hù)者信息
MAINTAINER user <user@imooc.com>
# 將第一階段構(gòu)建的helloworld 導(dǎo)入到此鏡像中
COPY --from=build /usr/bin/helloworld /usr/bin/helloworld
# 安裝flask 和 redis 的依賴
RUN pip install flask redis >/dev/null 2>&1
# 設(shè)定鏡像在切換到/app目錄路徑
WORKDIR /app
# 將源碼導(dǎo)入到鏡像
COPY ./code/app.py .
# 設(shè)定執(zhí)行用戶為user
RUN useradd user
USER user
# 設(shè)定flask所需的環(huán)境變量
ENV FLASK_APP app
# 默認(rèn)啟動(dòng)執(zhí)行的命令
CMD ["flask", "run", "-h", "0.0.0.0"]
# 將flask的默認(rèn)端口暴露出來
EXPOSE 5000
執(zhí)行 build 命令:
docker build -f Dockerfile-multi-stage -t myhello-multi-stage .
使用此鏡像運(yùn)行一個(gè)容器:
# 這里使用--net=host,方便使用之前章節(jié)中部署的redis容器服務(wù),與之進(jìn)行數(shù)據(jù)交換
docker run -dit --net=host --name myhello-multi-stage myhello-multi-stage
自行測(cè)試一下這個(gè)容器吧。
3. 小結(jié)
通過以上內(nèi)容,相信大家對(duì) Dockerfile 的使用又有了新的認(rèn)知,我們?cè)跇?gòu)建鏡像的時(shí)候,一定要有合理的規(guī)劃, 在自己不熟悉的基礎(chǔ)鏡像上定義鏡像的時(shí)候,不妨先用它運(yùn)行一個(gè)容器,在容器中過一遍流程, 弄清最終的鏡像中到底應(yīng)該包含哪些內(nèi)容,再來調(diào)整構(gòu)建腳本。
這里有一些 Dockerfile 的一般規(guī)范:
- 通過 Dockerfile 構(gòu)建的鏡像所啟動(dòng)的容器越快越好,這樣可以快速啟停增刪容器服務(wù)(下面幾條也是為第1條服務(wù)的);
- 避免安裝不必要的包,必要時(shí)使用多階段構(gòu)建;
- 一個(gè)容器盡量只專注做一件事情;
- 最小化鏡像層數(shù), 將重復(fù)功能的
RUN、COPY、ADD
等指令縮減合并, 但一定要保證 Dockerfile 可讀性。
當(dāng)然,這些建議僅供參考,不要拘泥于它,要根據(jù)自己的使用場(chǎng)景來做權(quán)衡。