【Docker、.NET 6】使用Docker 建立.NET 6容器

最近新專案使用了.NET 6、Docker、K8S等技術,用.NET 6建立新專案的同時,對.NET 6多了一些認識,也在容器化的過程中遇到很多有趣的坑,特別將操作流程做個紀錄。

 

環境安裝流程


.NET SDK
.NET官網下載.NET 6 SDK
 

Docker Desktop 
Docker官網下載Docker Desktop

 

環境檢視入門


確認安裝是否成功
開啟cmd 或powershell,輸入下列指令

dotnet --version
docker --version

 

容器建立流程


流程介紹
要建立一個可連線的.NET 6容器,大致可分為4個重要的步驟(圖1)。
 

  1. .NET 6相關流程: 包含建立專案、安裝套件、引用套件,及其他客製化設定等。
  2. 撰寫Dockerfile
  3. 製作映像檔
  4. 建立並啟動容器
圖1 使用docker建立.net 6容器的流程

 

 

.NET 6 相關流程


建立.NET 6新專案

dotnet new webapi -o Testapi
dotnet new sln
dotnet sln add Testapi.csproj

 

安裝Package
Nuget官網,查詢可用的Package版本號,並使用dotnet add package下載package,例如

dotnet add package Microsoft.EntityFrameworkCore
dotnet add package Microsoft.EntityFrameworkCore.SqlServer

因.NET會將package放在 C:/User/.nuget/packages 目錄中,如果已經安裝過套件,只需要在.csproj檔案中加入PackageReference即可(圖2)。

圖2 .csproj檔案內容

或使用下列語法進行套件版本引用設定

dotnet add TestAPI.csproj package Microsoft.EntityFrameworkCore -v 7.0.5

 

註冊服務並客製化相關的設定


設定連線字串

builder.Services.AddDbContext<CommonDataContext>(options =>
{
   options.UseSqlServer(builder.Configuration.GetConnectionString("YourConnectionString"));

});

 

註冊Swagger 並客製化Header 

builder.Services.AddSwaggerGen(c =>
{
   c.OperationFilter<SwaggerHeaderFilter>();
});

 

實作IOperationFilter

public class SwaggerHeaderFilter : IOperationFilter
{
    public void Apply(OpenApiOperation operation, OperationFilterContext context)
    {
        if (operation.Parameters == null)
        {
            operation.Parameters = new List<OpenApiParameter>();
        }
        IDictionary<string, OpenApiExample> langs = new Dictionary<string, OpenApiExample>();
        langs.Add("繁中", new OpenApiExample() { Value = new OpenApiString("zh-TW") });
        langs.Add("簡中", new OpenApiExample() { Value = new OpenApiString("zh-CN") });
        langs.Add("英文", new OpenApiExample() { Value = new OpenApiString("en-US") });
        operation.Parameters.Add(
            new OpenApiParameter
            {
                Name = "Accept-Language",
                In = ParameterLocation.Header,
                Required = true,
                Schema = new OpenApiSchema { Type = "string" },
                Examples = langs
            }
        )
    }
}        

 

註冊NLog

builder.Logging.ClearProviders();
builder.Host.UseNLog();

 

用.NET 6 建立專案時的差異及釐清的概念(

官網列出的差異: https://learn.microsoft.com/zh-tw/aspnet/core/migration/50-to-60-samples?view=aspnetcore-7.0
 

  1. 捨棄StartUp.cs:
    .NET 6僅有Program.cs
     
  2. 新增GlobalUsing的功能:
    開啟.csproj可看到ImpliciutUsings初始設定為enable,會導入微軟預設的GlobalUsings.cs,並在檔案內。若要自訂義GlobalUsing,則自行建立全新的GlobalUsings.cs檔案,並使用global using設定要全域使用的命名空間。流程可參考。
     
  3. 對Null的設定改變:
    .csproj可設定Null的特性,例如<Nullable>disable</Nullable />,EntityFramework Context物件,可null時也要加上?,例如 public string? Request {get; set;}
     
  4. dotnet publish -c release
    若/bin/Release/net6.0/publish已存在,則大小寫沿用已有路徑大小寫,但第一次建立時
    dotnet publish -c release 
    dotnet publish -c Release 
    建立的/Release/資料夾大小寫會不同,撰寫Dockerfile 時,會出現複製路徑找不到的坑。
     
  5. .指定SDK進行build
    若電腦中安裝多個SDK版本,預設會使用最新版的SDK建置,可查詢電腦中的SDK版本,並在專案中建立global.json,指定build要使用的SDK版本。
dotnet --list-sdks
{ "sdk": {
    "version": "5.0.001"
   }
}

 

撰寫Dockerfile


理解Dockerfile內容
FROM: 使用的基礎映像,如mcr.microsoft.com/dotnet/sdk:6.0
WORKDIR: 設定容器內的工作目錄
COPY: 複製檔案,可從本機複製至容器內,也可在容器中互相複製
RUN: 執行可用指令,如RUN dotnet build
ENTRYPOINT與CMD: 執行可帶參數的CMD指令,參考
 

# 建置編譯階段 Image, from dotnet/sdk, 並指定工作目錄為 /source
FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build
WORKDIR /source

# COPY 本機的所有檔案到 build image source 目錄下, 並執行套件還原指令, 此步驟需連網
COPY . . 
RUN dotnet restore

# 執行 dotnet build 指令 
RUN dotnet build

# 執行 dotnet 發佈指令, 並指定為 Release 版本
RUN dotnet publish -c release --no-restore

# 建置執行階段 Image, from dotnet/aspnet image, 並指定工作目錄為 /app
FROM mcr.microsoft.com/dotnet/aspnet:6.0
ENV TZ="Asia/Taipei"
WORKDIR /app

# COPY 編譯階段已產生的發佈檔至 /app 下
COPY --from=build /source/bin/release/net6.0/publish .

# 安裝 ping 及 telnet 指令, 方便有問題時在 Container 內下指令排除
RUN apt-get update
RUN apt-get install -y iputils-ping
RUN apt-get install -y telnet

ENTRYPOINT ["dotnet", "TestAPI.dll"]

Listen on Port被改變的坑
原Dockerfile範例的順序邏輯,是將Source Code複製進Container,以一個基礎映像檔進行dotnet build及dotnet publish,最後將publish產出的dll檔複製到另一個基礎映像檔,此時監聽的port是80(圖3)。

圖3 容器監聽Port

因某些因素,阿猩修改了Dockerfile的順序邏輯,阿猩在自己的本地端進行dotnet publish,直接將dll檔複製到容器內。但發現監聽的port居然變成5000。原因是,若無launchSetting.json或其他的設定時,.NET預設port為5000,Port的設定及優先順序可參考此篇

後來阿猩發現Dockerfile可直接以ENV進行環境設定,例如

ENV TZ="Asia/Taipei"

阿猩成功以ENV修改容器監聽的Port

ENV ASPNETCORE_URLS=http://+:80 \
    DOTNET_RUNNING_IN_CONTAINER=true

 

Swagger無法顯示的問題
原Program中的Swagger Middleware會判斷當前的環境是否為Development(圖4)。

圖4 Environment.IsDevelopment

 

在launchSetting.json中可設定Development,例如使用偵錯模式、IIS啟動時,預設環境為Developmenet(圖5)。

圖5 launchSetting.json內容

 

因此在偵錯模式時,輸入localhost:7129/Swagger/index.html,可以看到Swagger UI的,但以容器啟用會看不到。
除了設定launchSetting.json之外,也可以直接在Dockerfile中加入

ENV ASPNETCORE_ENVIRONMENT=Development

 

製作映像檔


建立Image
開啟cmd或powershell,至dockerfile所在路徑,執行

docker build -t tag名稱或映像名稱 .

 

建立並啟動容器


啟用Container
開啟cmd或powershell,執行

docker run --name 容器名稱 -p 8080:80 映像檔名稱

 

參考資料

  1. https://dotnet.microsoft.com/en-us/download/dotnet/6.0
  2. https://docs.docker.com/engine/reference/builder/
  3. https://www.nuget.org/3
  4. https://www.huanlintalk.com/2022/02/csharp10-global-using.html
  5. https://myapollo.com.tw/blog/docker-cmd-vs-entrypoint/
  6. https://blog.yowko.com/aspdotnet-core-urls-setting-sequence/