Docker Image Multi-Stage
在這篇文章Docker Certified Associate(DCA)認證考試學習-Docker Image Layer當中有提到一個Dockerfile的規則
當使用RUN
, COPY
, ADD
任意一個指令,Docker就會幫我們創建一個layers
也就說當我們使用越多的指令Docker就會幫我們建立更多的層,但是在最佳實踐也有提到要盡量減少層數的的產生,因為越多層數會影響運作的速度
那有沒有辦法減少層數呢?
當然是有的,那就是Multi-Stage Builds
首先我們下載Dotnet提供的範例來學習
git clone https://github.com/dotnet/dotnet-docker.git
cd dotnet-docker/samples/dotnetapp
我們將專案目錄列出來看看,可以看到有許多的Dockerfile在Docker的規則中,預設會去讀取Dockerfile的文件,不過也可以叫做別的名稱只要在build時要額外添加 -f
參數指定文件名稱即可
[node1] (local) root@192.168.0.18 ~/dotnet-docker/samples/dotnetapp
$ ls -l
total 100
-rw-r--r-- 1 root root 493 Feb 25 14:57 Dockerfile
-rw-r--r-- 1 root root 780 Feb 25 14:57 Dockerfile.alpine-arm32
-rw-r--r-- 1 root root 885 Feb 25 14:57 Dockerfile.alpine-arm32-slim
-rw-r--r-- 1 root root 784 Feb 25 14:57 Dockerfile.alpine-arm64
-rw-r--r-- 1 root root 889 Feb 25 14:57 Dockerfile.alpine-arm64-slim
-rw-r--r-- 1 root root 778 Feb 25 14:57 Dockerfile.alpine-x64
-rw-r--r-- 1 root root 883 Feb 25 14:57 Dockerfile.alpine-x64-slim
-rw-r--r-- 1 root root 522 Feb 25 14:57 Dockerfile.chiseled
-rw-r--r-- 1 root root 485 Feb 25 14:57 Dockerfile.debian-arm32
-rw-r--r-- 1 root root 489 Feb 25 14:57 Dockerfile.debian-arm64
-rw-r--r-- 1 root root 483 Feb 25 14:57 Dockerfile.debian-x64
-rw-r--r-- 1 root root 588 Feb 25 14:57 Dockerfile.debian-x64-slim
-rw-r--r-- 1 root root 620 Feb 25 14:57 Dockerfile.nanoserver-x64
-rw-r--r-- 1 root root 650 Feb 25 14:57 Dockerfile.nanoserver-x64-slim
-rw-r--r-- 1 root root 483 Feb 25 14:57 Dockerfile.ubuntu-arm32
-rw-r--r-- 1 root root 487 Feb 25 14:57 Dockerfile.ubuntu-arm64
-rw-r--r-- 1 root root 481 Feb 25 14:57 Dockerfile.ubuntu-x64
-rw-r--r-- 1 root root 586 Feb 25 14:57 Dockerfile.ubuntu-x64-slim
-rw-r--r-- 1 root root 511 Feb 25 14:57 Dockerfile.windowsservercore-x64
-rw-r--r-- 1 root root 677 Feb 25 14:57 Dockerfile.windowsservercore-x64-slim
-rw-r--r-- 1 root root 3617 Feb 25 14:57 Program.cs
-rw-r--r-- 1 root root 11310 Feb 25 14:57 README.md
-rw-r--r-- 1 root root 215 Feb 25 14:57 dotnetapp.csproj
我們看看專案內的 Dockerfile ,可以看到竟然有兩個FROM
- FROM mcr.microsoft.com/dotnet/sdk:7.0 AS build
- FROM mcr.microsoft.com/dotnet/runtime:7.0
# https://hub.docker.com/_/microsoft-dotnet
FROM mcr.microsoft.com/dotnet/sdk:7.0 AS build
WORKDIR /source
# copy csproj and restore as distinct layers
COPY *.csproj .
RUN dotnet restore --use-current-runtime
# copy and publish app and libraries
COPY . .
RUN dotnet publish -c Release -o /app --use-current-runtime --self-contained false --no-restore
# final stage/image
FROM mcr.microsoft.com/dotnet/runtime:7.0
WORKDIR /app
COPY --from=build /app .
ENTRYPOINT ["dotnet", "dotnetapp.dll"]
首先我們知道在 dotnet 中要編譯程式那需要安裝 dotnet sdk 編譯完的程式如果沒有額外做設定,要在其他電腦運作需要安裝 dotnet runtime 我們比較一下這兩個image,看到SIZE欄位
- mcr.microsoft.com/dotnet/sdk 767MB
- mcr.microsoft.com/dotnet/runtime 190MB
[node1] (local) root@192.168.0.18 ~/dotnet-docker/samples/dotnetapp
$ docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
hello latest 314c0cca4b2d 4 minutes ago 190MB
<none> <none> 2f274cff5d2e 4 minutes ago 971MB
mcr.microsoft.com/dotnet/sdk 7.0 83489ef20059 11 days ago 767MB
mcr.microsoft.com/dotnet/runtime 7.0 eb10f76afdc9 11 days ago 190MB
這兩個image的大小差了好幾倍,所以如果只能選擇一個底層,那只能選擇功能比較多的sdk image
不過這樣大小就要767MB起跳了,還要加上編譯完程式的大小
所以docker提供了一個辦法讓你在功能比較完整的sdk環境做編譯,編譯完後在將程式傳送到runtime為底層的image,這樣大小就變成190MB起跳了
越小的image大小在我們部署跟啟動之時都是非常有幫助,這就是Multi-Stage Builds
接下來繼續解讀Dockerfile的內容
# 首先第一行最後面多了一段 ` AS build` 這個名字沒有限定,是用來指定這個編譯環境的名稱,看到下方 `--from=build` 兩個名字要一樣
FROM mcr.microsoft.com/dotnet/sdk:7.0 AS build
# 指定新的工作目錄
WORKDIR /source
# 將所有專案檔複製到image內部,並且還原所以專案
COPY *.csproj .
RUN dotnet restore --use-current-runtime
# 將所有檔案複製到image內部,並且publish專案並且輸出到 app 資料夾
COPY . .
RUN dotnet publish -c Release -o /app --use-current-runtime --self-contained false --no-restore
# 指定第二個image
# 指定工作目錄到 app
# 將所有 build 建構環境的 app 資料夾,複製到新的image底下
# 運行dll
FROM mcr.microsoft.com/dotnet/runtime:7.0
WORKDIR /app
COPY --from=build /app .
ENTRYPOINT ["dotnet", "dotnetapp.dll"]
大致了解之後我們在看一次所有的images
第一個就是我們成品的image,底層是使用runtime image
第二個image是
這個image如果硬碟容量足夠的話可以不用主動清除掉
[node1] (local) root@192.168.0.18 ~/dotnet-docker/samples/dotnetapp
$ docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
hello latest 314c0cca4b2d 4 minutes ago 190MB
<none> <none> 2f274cff5d2e 4 minutes ago 971MB
mcr.microsoft.com/dotnet/sdk 7.0 83489ef20059 11 days ago 767MB
mcr.microsoft.com/dotnet/runtime 7.0 eb10f76afdc9 11 days ago 190MB
我們可以使用 docker history
來看清楚image所有的層數
[node1] (local) root@192.168.0.18 ~/dotnet-docker/samples/dotnetapp
$ docker history 2f274c
IMAGE CREATED CREATED BY SIZE COMMENT
2f274cff5d2e 31 minutes ago /bin/sh -c dotnet publish -c Release -o /app… 9.42MB
48df258627a0 31 minutes ago /bin/sh -c #(nop) COPY dir:d177e334bdec992e0… 3.93kB
58f7aa6a3d6f 31 minutes ago /bin/sh -c dotnet restore --use-current-runt… 194MB
a66cd1e7ac71 31 minutes ago /bin/sh -c #(nop) COPY file:323095bfbe621dcb… 215B
3fdf37a8423e 31 minutes ago /bin/sh -c #(nop) WORKDIR /source 0B
83489ef20059 11 days ago /bin/sh -c powershell_version=7.3.2 && c… 42.4MB
<missing> 11 days ago /bin/sh -c curl -fSL --output dotnet.tar.gz … 438MB
<missing> 11 days ago /bin/sh -c apt-get update && apt-get ins… 74.8MB
<missing> 11 days ago /bin/sh -c #(nop) ENV ASPNETCORE_URLS= DOTN… 0B
<missing> 11 days ago /bin/sh -c #(nop) COPY dir:0757f1be98a8096d5… 21.8MB
<missing> 11 days ago /bin/sh -c #(nop) ENV ASPNET_VERSION=7.0.3 0B
<missing> 11 days ago /bin/sh -c ln -s /usr/share/dotnet/dotnet /u… 24B
<missing> 11 days ago /bin/sh -c #(nop) COPY dir:85163c9fd54a3f2f3… 73.2MB
<missing> 11 days ago /bin/sh -c #(nop) ENV DOTNET_VERSION=7.0.3 0B
<missing> 11 days ago /bin/sh -c #(nop) ENV ASPNETCORE_URLS=http:… 0B
<missing> 11 days ago /bin/sh -c apt-get update && apt-get ins… 36.2MB
<missing> 2 weeks ago /bin/sh -c #(nop) CMD ["bash"] 0B
<missing> 2 weeks ago /bin/sh -c #(nop) ADD file:3ea7c69e4bfac2ebb… 80.5MB
[node1] (local) root@192.168.0.18 ~/dotnet-docker/samples/dotnetapp
$ docker history 314c0
IMAGE CREATED CREATED BY SIZE COMMENT
314c0cca4b2d 32 minutes ago /bin/sh -c #(nop) ENTRYPOINT ["dotnet" "dot… 0B
0285c20c2552 32 minutes ago /bin/sh -c #(nop) WORKDIR /app 0B
eb10f76afdc9 11 days ago /bin/sh -c ln -s /usr/share/dotnet/dotnet /u… 24B
<missing> 11 days ago /bin/sh -c #(nop) COPY dir:85163c9fd54a3f2f3… 73.2MB
<missing> 11 days ago /bin/sh -c #(nop) ENV DOTNET_VERSION=7.0.3 0B
<missing> 11 days ago /bin/sh -c #(nop) ENV ASPNETCORE_URLS=http:… 0B
<missing> 11 days ago /bin/sh -c apt-get update && apt-get ins… 36.2MB
<missing> 2 weeks ago /bin/sh -c #(nop) CMD ["bash"] 0B
<missing> 2 weeks ago /bin/sh -c #(nop) ADD file:3ea7c69e4bfac2ebb… 80.5MB
這兩個層數相差非常多,這也是Multi-Stage Builds的好處之一
我們可以在建構環境的 image 完成所有基礎的操作,最後只要將dotnet編譯完的結果複製出來就好了,因為sdk跟程式的原始碼編譯完之後就沒有意義了
所以在實際使用環境Multi-Stage Builds是經常使用的
Summary
今天學習了Multi-Stage Builds的使用方法,使用之後image的層數跟大小,可以縮小數倍可謂是好處多多,所以在最佳實踐中Docker也是建議多使用Multi-Stage Builds 可以讓image能夠更快速的下載下來,層數減少也可以減少container啟動的速度,可以說是很重要的技術。