這裡拿 Visual Studio 工具幫我們 create 的 Default Dockerfile 指令來做逐行說明。
如果還未在專案添加 Dockerfile 的人,可以參考微軟的教學 Windows 上的 Visual Studio Tools for Docker | Microsoft Learn 使用 Visual Studio 自動幫你添加基礎的 Dockerfile。
示範環境
TargetFramework:.NET 6
專案類型:Web API
方案名稱:MyWebAPI
專案結構:
📁 MyWebAPI ├── 📁 MyWebAPI.Api │ ├── Dockerfile │ └── MyWebAPI.Api.csproj ├── 📁 MyWebAPI.Model │ └── MyWebAPI.Model.csproj ├── 📁 MyWebAPI.Services │ └── MyWebAPI.Services.csproj ├── 📁 MyWebAPI.Adapter │ └── MyWebAPI.Adapter.csproj ├── 📁 MyWebAPI.Common │ └── MyWebAPI.Common.csproj ├── 📁 MyWebAPI.Repository │ └── MyWebAPI.Repository.csproj └── 📁 MyWebAPI.sln |
Default Dockerfile
我要部屬的服務是 MyWebAPI.Api
,所以我對 MyWebAPI.Api
專案點右鍵,生成一個以下格式的 Dockerfile
#See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging.
FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS base
WORKDIR /app
EXPOSE 80
FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build
WORKDIR /src
COPY ["MyWebAPI.Api/MyWebAPI.Api.csproj", "MyWebAPI.Api/"]
COPY ["MyWebAPI.Model/MyWebAPI.Model.csproj", "MyWebAPI.Model/"]
COPY ["MyWebAPI.Services/MyWebAPI.Services.csproj", "MyWebAPI.Services/"]
COPY ["MyWebAPI.Adapter/MyWebAPI.Adapter.csproj", "MyWebAPI.Adapter/"]
COPY ["MyWebAPI.Common/MyWebAPI.Common.csproj", "MyWebAPI.Common/"]
COPY ["MyWebAPI.Repository/MyWebAPI.Repository.csproj", "MyWebAPI.Repository/"]
RUN dotnet restore "MyWebAPI.Api/MyWebAPI.Api.csproj"
COPY . .
WORKDIR "/src/MyWebAPI.Api"
RUN dotnet build "MyWebAPI.Api.csproj" -c Release -o /app/build
FROM build AS publish
RUN dotnet publish "MyWebAPI.Api.csproj" -c Release -o /app/publish /p:UseAppHost=false
FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "MyWebAPI.Api.dll"]
Visual Studio 創建出來的 Dockerfile 分成多個階段,稱為 Multi-stage builds(中文稱為 多階段組建),每一個 FROM 就代表一個階段,以下會以一個階段一章節來分,逐行說明指令們的用途
Multi-stage builds 延伸閱讀 Multi-stage builds | Docker Documentation
第 1 階段(base 階段)
FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS base
FROM
代表以 mcr.microsoft.com/dotnet/aspnet:6.0 這個 image 為基底來建置
aspnet 的 image 包含 ASP.NET Core runtime 以及一些相關 Library,但是不含 .NET SDK & CLI,所以 size 比較小,適合作為 running 用的 base image,這裡就只是為了提供最終打包時要用的 base image 先行做準備而已
AS
將此階段命名為 base
WORKDIR /app
WORKDIR
指定工作目錄為 /app,他會在 Container 內建立一個 app Folder,之後下面的動作都將從此目錄中執行
(之後進 Container bash,也會看到他一進去就是 app 路徑 ⬇ )
EXPOSE 80
EXPOSE
代表將此 Container 公開 80 port 運行
但以上指令並不等於你的 Application 也會監聽 80 Port,如果是 .NET Core 的 Application 要另外做些設置,可以參考以下延伸閱讀文章,選擇自己想要的 Port 設置方式
.net - How to specify the port an ASP.NET Core application is hosted on? - Stack Overflow
asp.net - Why does aspnet core start on port 80 from within Docker? - Stack Overflow
第 2 階段(build 階段)
FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build
在 build 階段則FROM
mcr.microsoft.com/dotnet/sdk:6.0 這個 image 為基底來建置
這是因為 sdk 的 image 裡面包含了 .NET SDK、命令列工具 (CLI) 等項目,size 比 aspnet 的龐大很多,所以此 image 適合用於 build、test 等用途,但不會拿他做為最後打包的 base image
AS
將此階段命名為 build
WORKDIR /src
WORKDIR
在 build 階段也為他創建一個 src 工作目錄
在這個階段要開始講解 Dockerfile 裡的目錄結構 ~ ~
在 build docker image 的時候,有 2 個主要目錄:
- 在 host 中執行 docker build 的目錄(也就是專案的路徑)
- Container 內的目錄(也就是
WORKDIR
指定的目錄)
COPY ["MyWebAPI.Api/MyWebAPI.Api.csproj", "MyWebAPI.Api/"]
COPY ["MyWebAPI.Model/MyWebAPI.Model.csproj", "MyWebAPI.Model/"]
COPY ["MyWebAPI.Services/MyWebAPI.Services.csproj", "MyWebAPI.Services/"]
COPY ["MyWebAPI.Adapter/MyWebAPI.Adapter.csproj", "MyWebAPI.Adapter/"]
COPY ["MyWebAPI.Common/MyWebAPI.Common.csproj", "MyWebAPI.Common/"]
COPY ["MyWebAPI.Repository/MyWebAPI.Repository.csproj", "MyWebAPI.Repository/"]
COPY
將 host 專案內的 csproj ,複製到 Container 內的新路徑
這裡你會發現,他在 restore 前先只將 csproj 複製過去,是為什麼呢??
原理與圖層(image layer)的 cache 有關~!
在 dockerfile 中,每一個指令都會生成一個 image layer,由上往下依序執行,等到整個 dockerfile build 完後,再將這些 layer 整合成一個完整的 docker image
在每次 docker build 的時候,他其實會去判定這些 layer 的內容是否有更動,沒有更動的 layer 就會拿來重複使用(就是使用 cache 的概念),不會每次都重新建置每一層
但如果因為上面某一層的內容有變動,而後面的圖層也會受到影響的,就後面的 layer 全部都必須重建,而能夠重複使用 layer 的行為,稱為 build cache(構建緩存)
build cache 的官方詳細說明 Optimizing builds with cache management | Docker Documentation
回到 dockerfile 指令內的 restore 這行
RUN dotnet restore "MyWebAPI.Api/MyWebAPI.Api.csproj"
之所以在 restore
前面需要先 COPY csproj,也是基於 build cache 的概念。
restore 會根據 csproj 來還原專案的 Dependencies,這是一個非常耗時的動作,並且 csproj 與 Dependencies 大多數並不常變更,因此將這幾個動作先往上移先行做完,他們在下次 docker build 的時候就能使用 cache layer,減少 build 的時間
COPY . .
是將當前目錄下的所有文件都複製進 Container 內
.
代表當前目錄
這個 layer 非常重要,因為他是將整大包 code 全部複製過去,所以只要你有任何一個地方的 code 有改到,這個 layer 肯定是無法使用 cache 的,所以他在 Dockerfile 裡的指令順序最好排的下面一點,以免影響其他 layer 無法 cache
WORKDIR "/src/MyWebAPI.Api"
RUN dotnet build "MyWebAPI.Api.csproj" -c Release -o /app/build
WORKDIR
將工作目錄移至 MyWebAPI.Api 專案 Folder 內
執行 dotnet build
,將 build 好的檔案存進 Container 的 /app/build
目錄
這裡就不多解釋了,對 dotnet build 指令不熟的再去 dotnet build command - .NET CLI | Microsoft Learn 自行了解
第 3 階段(publish 階段)
第三階段就只做 publish 的動作
FROM build AS publish
RUN dotnet publish "MyWebAPI.Api.csproj" -c Release -o /app/publish /p:UseAppHost=false
FROM
這裡不像是前面 2 個階段那樣使用 image 當 base,他是使用上一個 build 階段來繼續作業
執行dotnet publish
,將 publish 好的檔案存進 Container 的 /app/publish
目錄
/p:UseAppHost=false
這個如果設置為 false,他 publish 出來的檔案中就不會產生 exe 可執行檔
一樣貼心附上 dotnet publish 指令詳解傳送門 ~ dotnet publish command - .NET CLI | Microsoft Learn
第 4 階段(final 階段)
終於到最後一個階段了,這個階段會將你最後要包進 docker image 的檔案做處理
FROM base AS final
FROM
這裡使用的是第 1 階段先行準備好的 layer 繼續往下做
WORKDIR /app
一樣 WORKDIR
進到工作目錄 /app
COPY --from=publish /app/publish .
這裡的 COPY 就跟前面處理的目錄位置不太一樣了
這裡會 COPY
來自 publish 階段的 /app/publish
目錄內的檔案,到 Container 內的當前目錄(也就是 /app 裡)
ENTRYPOINT ["dotnet", "MyWebAPI.Api.dll"]
Dockerfile 的最後,會使用 ENTRYPOINT
指定要 run 什麼指令
上面這段等同於以下指令 ⬇
dotnet MyWebAPI.Api.dll
Container 啟動的時候此 Application 也會一起啟動
Default 的 Dockerfile 指令到此講解完畢,不過使用在自己的專案上,還是需要再做一些進階的指令微調 & 加工,這個部分就……..
延伸閱讀:.NET 微服務。 容器化 .NET 應用程式的架構 | Microsoft Learn
這本 PDF 電子書內也有對於 Dockerfile 的詳細解說~推薦 👍