tech.guitarrapc.cóm

Technical updates

dotnet publishでコンテナイメージを作成する

.NETは組み込みコンテナサポートを持っておりdotnet publishでコンテナイメージを作成できます。このようなDockerfileなしのコンテナ作成はJibBuildpacksを思い起こしますが、dotnet publishという標準コマンドで提供しているのが大きな違いです。

Dockerfileを書かなくていいというだけで魅力を感じる面があるので、何ができて何ができないのかいろいろ触ってみましょう。

.NET 7以降、dotnet publishでコンテナイメージ作成をサポート

.NET 7からdotnet publishでコンテナイメージ作成できるようになりました。.NETランタイムも、.NET 3.0でコンテナにおけるメモリ制限下のOOMの課題が修正され1、.NET 5以降はコンテナでの動作が安定しています。さらに.NET 8以降徐々に機能追加されて、簡単なコンテナイメージならDockerfileを書かずにdotnet publishだけでコンテナイメージを作成できるようになったことはうれしいことです。

今回は、コンテナイメージの作成、イメージ作成の調整、イメージの調整など「Dockerfileでこうやるよね」ということをdotnet publishでどうやるかを見ていきます。

dotnet publishでコンテナイメージを作成する

dotnet publishでコンテナイメージを作成する前に必要なものは次の通りです。

  • .NET 8+ SDK: 現在サポートされている.NET SDKに合わせる
  • コンテナランタイム: ローカルならDocker DesktopPodman、Docker Engineなど

プロジェクトタイプごとの事前準備

dotnet publishを使ったコンテナイメージ作成は、WebアプリケーションやWorkerサービスなどWeb SDKプロジェクトはデフォルトで有効になっており追加設定不要です。一方、ConsoleAppはプロジェクトファイル(.csproj)でEnableSdkContainerSupportを有効にする必要があります。

プロジェクトタイプ dotnet new SDKによるコンテナサポート 備考
Webアプリケーション web デフォルトで有効
Workerサービス worker デフォルトで有効
ConsoleApp console × EnableSdkContainerSupportの追加が必要

SDKによるコンテナサポートを有効にする.csproj設定は次の通りです。

  <PropertyGroup>
    <EnableSdkContainerSupport>true</EnableSdkContainerSupport>
  </PropertyGroup>

プロジェクトごとのコンテナイメージ作成

Webアプリケーション、Workerサービス、Web APIアプリケーション、ConsoleAppのプロジェクトごとにコンテナイメージを作成してみましょう。

Webアプリケーション

$ dotnet new web -o WebApp20
$ cd WebApp20
$ dotnet publish --os linux --arch x64 -c Release /t:PublishContainer
Restore complete (0.3s)
  WebApp20 succeeded (8.5s) → bin\Release\net9.0\linux-x64\publish\

Build succeeded in 9.1s

$ docker image ls
REPOSITORY          TAG       IMAGE ID       CREATED          SIZE
webapp20            latest    b909b3cec675   22 minutes ago   329MB

csprojです。

<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <TargetFramework>net9.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
  </PropertyGroup>

</Project>

Workerサービス

デフォルトで有効なため、追加設定不要でdothnet publishでコンテナイメージを作成できます。

$ dotnet new worker -o WorkerApp21
$ cd WorkerApp21
$ dotnet publish --os linux --arch x64 -c Release /t:PublishContainer
Restore complete (0.3s)
  WorkerApp21 succeeded (8.7s) → bin\Release\net9.0\linux-x64\publish\

Build succeeded in 9.4s

$ docker image ls
REPOSITORY          TAG       IMAGE ID       CREATED          SIZE
workerapp21         latest    90a9c6a9afda   30 seconds ago   295MB

csprojです。

<Project Sdk="Microsoft.NET.Sdk.Worker">

  <PropertyGroup>
    <TargetFramework>net9.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
    <UserSecretsId>dotnet-WorkerApp21-462df4c8-ecc5-4b76-8e17-5e3f8f27dd57</UserSecretsId>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.2" />
  </ItemGroup>
</Project>

Web APIサービス

デフォルトで有効なため、追加設定不要でdothnet publishでコンテナイメージを作成できます。

$ dotnet new api -o WebApiApp22
$ cd WebApiApp22
$ dotnet publish --os linux --arch x64 -c Release /t:PublishContainer
Restore complete (0.3s)
  WebApiApp22 succeeded (2.8s) → bin\Release\net9.0\linux-x64\publish\

Build succeeded in 3.5s

$ docker image ls
REPOSITORY          TAG       IMAGE ID       CREATED          SIZE
webapiapp22         latest    d8e46ecffda6   10 seconds ago   330MB

csporjです。

<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <TargetFramework>net9.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.2" />
  </ItemGroup>

</Project>

コンソールアプリケーション

追加設定をするとdothnet publishでコンテナイメージを作成できます。csprojでEnableSdkContainerSupportを有効にしない場合、ビルド時に-p:EnableSdkContainerSupport=trueを指定します。

$ dotnet new console -o ConsoleApp23
$ cd ConsoleApp23
# csprojでEnableSdkContainerSupportを有効にする場合
$ dotnet publish --os linux --arch x64 -c Release /t:PublishContainer
# csprojでEnableSdkContainerSupportを有効にしない場合
$ dotnet publish --os linux --arch x64 -c Release /t:PublishContainer -p:EnableSdkContainerSupport=true
Restore complete (0.3s)
  ConsoleApp23 succeeded (7.8s) → bin\Release\net9.0\linux-x64\publish\

Build succeeded in 8.3s

$ docker image ls
REPOSITORY          TAG       IMAGE ID       CREATED              SIZE
consoleapp23        latest    e7b922fab1f0   About a minute ago   293MB

csprojです。

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net9.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

  <PropertyGroup>
    <EnableSdkContainerSupport>true</EnableSdkContainerSupport>
  </PropertyGroup>

</Project>

イメージ作成を調整してみる

csprojに設定を追加したり、コマンドでイメージ作成時の動作を調整できます。ドキュメントを見つつ、調整してみましょう。

イメージ名の指定

デフォルトではAssemblyNameをべースに設定されます。これを上書きするには、ContainerRepositoryを指定します。

  <PropertyGroup>
    <EnableSdkContainerSupport>true</EnableSdkContainerSupport>
    <ContainerRepository>container-app-23</ContainerRepository>
  </PropertyGroup>

コンテナイメージを生成してみると、指定した名前でイメージができたことを確認できます。

$ dotnet publish --os linux --arch x64 -c Release /t:PublishContainer
Restore complete (0.2s)
  ConsoleApp23 succeeded (8.7s) → bin\Release\net9.0\linux-x64\publish\

Build succeeded in 9.1s

$ docker image ls
REPOSITORY          TAG       IMAGE ID       CREATED             SIZE
container-app-23    latest    21ff9cf2268b   7 seconds ago       293MB

イメージをプッシュする

イメージ名と同様にレジストリ名など設定ごとにプロパティがあります。例えばECRなら{ACCOUNT_ID}.dkr.ecr.{REGION}.amazonaws.com/foobar:9.0-alpineとなるのですが、次のマッピングになります。

Image name part MSBuild property Example values
REGISTRY[:PORT] ContainerRegistry {ACCOUNT_ID}.dkr.ecr.{REGION}.amazonaws.com
PORT ContainerPort :443
REPOSITORY ContainerRepository foobar
TAG ContainerImageTag 9.0
FAMILY2 ContainerFamily -alpine

レジストリ名を指定するとdotnet publish時に自動的に認証を確認してpushされます。

ECRにプッシュする

例えばECRに展開することを想定して次のような設定を追加します。

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net9.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

  <PropertyGroup>
    <EnableSdkContainerSupport>true</EnableSdkContainerSupport>
    <ContainerRegistry>12345678901234.dkr.ecr.ap-northeast-1.amazonaws.com</ContainerRegistry>
    <ContainerRepository>container-app-23</ContainerRepository>
  </PropertyGroup>

</Project>

あとはdocker loginして、-p:ContainerImageTag:タグ指定でタグを実行ごとに指定してみましょう。dotnet publishを実行するとイメージが作成、pushされます。

$ aws ecr get-login-password --region ap-northeast-1 | docker login --username AWS --password-stdin 12345678901234.dkr.ecr.ap-northeast-1.amazonaws.com
$ dotnet publish --os linux --arch x64 -c Release /t:PublishContainer -p:ContainerImageTag=1.0

Docker Hubにプッシュする

DockerHubの場合、-p:ContainerRegistry=docker.ioと指定します。事前にDocker Hubへログインしておきましょう。

マルチプラットフォーム対応イメージ

ベースイメージがマルチプラットフォームに対応しているなら、RuntimeIdentifiersを指定すると自動的に対象プラットフォーム+マルチプラットフォームイメージが生成されます。例えば、linux/amd64とlinux/arm64をサポートする場合は次のように指定します。3

  <PropertyGroup>
    <RuntimeIdentifiers>linux-x64;linux-arm64</RuntimeIdentifiers>
  </PropertyGroup>

  <PropertyGroup>
    <EnableSdkContainerSupport>true</EnableSdkContainerSupport>
    <ContainerRepository>container-app-23</ContainerRepository>
  </PropertyGroup>```

実行します。

$ dotnet publish -c Release -p:ContainerImageTag="latest" -p:ContainerRegistry=docker.io /t:PublishContainer

すると次の3イメージがpushされます。latestはマルチプラットフォーム対応イメージですが、linux-x64とlinux-arm64はそれぞれのプラットフォーム向けのイメージです。イメージ名は異なりますが、次のような結果です。

  • latest
  • latest-linux-x64
  • latest-linux-arm64

alt text

Dockerfileと違って、dotnet publishでマルチプラットフォーム対応イメージを作る時、各プラットフォームサフィックスイメージが生成を抑制できないことに注意してください。

デフォルトイメージ

dotnet publishのコンテナイメージは、設定に応じて自動的に切り替わります4。ベースイメージ変更は別途説明するので、まずはデフォルトを把握しておきましょう。

自己完結型アプリケーション (ベースイメージ: mcr.microsoft.com/dotnet/runtime-deps)

実行例

$ dotnet publish --os linux --arch x64 -p:SelfContained=true -p:PublishSingleFile=true -c Release /t:PublishContainer -p:ContainerImageTag=1.0
$ docker image ls
REPOSITORY          TAG       IMAGE ID       CREATED             SIZE
container-app-23    1.0       55f88494a42c   3 seconds ago       283MB

alt text

ASP.NET Coreプロジェクト (ベースイメージ: mcr.microsoft.com/dotnet/aspnet)

実行例

$ dotnet publish --os linux --arch x64 -c Release /t:PublishContainer -p:ContainerImageTag=1.0
$ docker image ls
REPOSITORY          TAG       IMAGE ID       CREATED              SIZE
webapiapp22         1.0       04af635c8280   17 seconds ago       330MB

alt text

ランタイム指定がlinux-musl-x64linux-musl-arm64 (ベースイメージ: alpine)

実行例

$ dotnet publish -r linux-musl-x64 -c Release /t:PublishContainer -p:ContainerImageTag=1.0
$ docker image ls
REPOSITORY          TAG       IMAGE ID       CREATED          SIZE
container-app-23    1.0       547a3669f3ac   8 seconds ago    130MB

alt text

PublishAot=true (ベースイメージ: mcr.microsoft.com/dotnet/runtime-deps:9.0-noble-chiseledなど)

実行例

$ dotnet publish --os linux --arch x64 -p:PublishAot=true -c Release /t:PublishContainer -p:ContainerImageTag=1.0
$ docker image ls
REPOSITORY          TAG       IMAGE ID       CREATED         SIZE
container-app-23    1.0       3acafde7dd76   2 minutes ago   125MB
$ docker inspect container-app-23:1.0
[
    {
        "Id": "sha256:3acafde7dd76b6b9799139b78e225fcc2a6d9decc9782b564c5e2a235cfc3a60",
        "RepoTags": [
            "container-app-23:1.0"
        ],
        "RepoDigests": [],
        "Parent": "",
        "Comment": "",
        "Created": "2025-03-10T09:55:18.0968842Z",
        "DockerVersion": "",
        "Author": "",
        "Config": {
            "Hostname": "",
            "Domainname": "",
            "User": "1654",
            "AttachStdin": false,
            "AttachStdout": false,
            "AttachStderr": false,
            "ExposedPorts": {
                "8080/tcp": {}
            },
            "Tty": false,
            "OpenStdin": false,
            "StdinOnce": false,
            "Env": [
                "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
                "APP_UID=1654",
                "ASPNETCORE_HTTP_PORTS=8080",
                "DOTNET_RUNNING_IN_CONTAINER=true"
            ],
            "Cmd": null,
            "Image": "",
            "Volumes": null,
            "WorkingDir": "/app",
            "Entrypoint": [
                "/app/ConsoleApp23",
                "dotnet",
                "ConsoleApp23.dll"
            ],
            "OnBuild": null,
            "Labels": {
                "org.opencontainers.artifact.created": "2025-03-10T09:55:17.4730219Z",
                "org.opencontainers.image.authors": "ConsoleApp23",
                "org.opencontainers.image.base.name": "mcr.microsoft.com/dotnet/runtime-deps:8.0",
                "org.opencontainers.image.created": "2025-03-10T09:55:17.4730219Z",
                "org.opencontainers.image.version": "1.0.0"
            }
        },
        "Architecture": "amd64",
        "Os": "linux",
        "Size": 125199770,
        "GraphDriver": {
            "Data": {
                "LowerDir": "/var/lib/docker/overlay2/4a9eec2b50acd2f8acd2d7967da00beecceb60194a16316d07a9dc2f7cd7fbb4/diff:/var/lib/docker/overlay2/2c4c43e9f57a0cc4bcb440483d784d5de24502ea979bad82e8ca4eb0379eb855/diff:/var/lib/docker/overlay2/4b2b1d21296eac3283284faa97b977c23a08bb359611b32fac7714044da40817/diff",
                "MergedDir": "/var/lib/docker/overlay2/fda7ce03d0948fef20e0132be71f59a87159da9dc8745bf1faaa931ea9ca5cf2/merged",
                "UpperDir": "/var/lib/docker/overlay2/fda7ce03d0948fef20e0132be71f59a87159da9dc8745bf1faaa931ea9ca5cf2/diff",
                "WorkDir": "/var/lib/docker/overlay2/fda7ce03d0948fef20e0132be71f59a87159da9dc8745bf1faaa931ea9ca5cf2/work"
            },
            "Name": "overlay2"
        },
        "RootFS": {
            "Type": "layers",
            "Layers": [
                "sha256:5f1ee22ffb5e68686db3dcb6584eb1c73b5570615b0f14fabb070b96117e351d",
                "sha256:13538303ed9cedfa748390cad44880ed7002ff88467c351388a8df331a989662",
                "sha256:05df6742558bf5c12ca2109e48499d5d5103127803e51f85502e1b223ce0df00",
                "sha256:210d1b841410c5bd137db34f6d54df88e9c777b9fd3df8fe2675b128dbb3485b"
            ]
        },
        "Metadata": {
            "LastTagTime": "0001-01-01T00:00:00Z"
        }
    }
]

他 (ベースイメージ: mcr.microsoft.com/dotnet/runtime)

実行例

$ dotnet publish --os linux --arch x64 -c Release /t:PublishContainer -p:ContainerImageTag=1.0
$ docker image ls
REPOSITORY          TAG       IMAGE ID       CREATED          SIZE
container-app-23    1.0       862527cad6f1   6 seconds ago    293MB

alt text

コンテナイメージを調整する

Dockerfileでいうところの以下の調整ができます。詳しくはドキュメントを見るとして、ベースイメージ、ラベル、EXPOSE、ENV、USERあたりは経験上操作する可能性があるので見ておきましょう。

  • FROM
  • LABEL
  • WORKDIR
  • EXPOSE
  • ENV
  • USER
  • ENTRYPOINT

ベースイメージを変更する

いわゆるFROM相当です。ベースイメージはコマンドライン--osContainerFamilyContainerBaseImageのいずれかで設定できます。

C#においてもベースイメージを変更することでコンテナイメージサイズは大きく変わるため、サイズ削減に取り組むときは検討するといいでしょう。検証結果を見るとaspnet:8.0-jammyで非圧縮217MBがaspnet:8.0-jammy-chiseledで111MBになります。

--osでディストロを指定する

--os ディストロで指定したディストロイメージを使うこともできます。例えば、--os linux-muslalpineイメージを使うことができます。

$ dotnet publish --os linux-musl -c Release /t:PublishContainer
Restore complete (0.3s)
  ConsoleApp23 succeeded (2.1s) → bin\Release\net9.0\linux-musl-x64\publish\

Build succeeded in 2.7s
$ docker image ls
REPOSITORY          TAG       IMAGE ID       CREATED          SIZE
container-app-23    latest    e48a67df6cbc   16 seconds ago   130MB

alt text

ContainerFamilyでイメージを指定する

例えばchiseledイメージを使う場合、次のように指定します。8.0ならjammy、9.0ならnobleと違うことに気を付けてください。めんどくさい。ICUやtzdataを使えないので、大抵は後述している-extraかデフォルトイメージのほうが使いやすいでしょう。

$ dotnet publish -t:PublishContainer -c Release -r linux-amd64 -p:ContainerFamily=noble-chiseled
Restore complete (0.2s)
  ConsoleApp23 succeeded (5.7s) → bin\Release\net9.0\linux-x64\publish\

Build succeeded in 6.2s

$ docker image ls
REPOSITORY          TAG       IMAGE ID       CREATED          SIZE
container-app-23    latest    bb555d7fcc63   21 seconds ago   133MB

alt text

ラベルを設定する

LABEL相当です。ItemGroupでContainerLabelを指定します。5

コンテナのラベルは外からどのような用途のものか識別するのに使われることが多いです。dotnet publishでイメージ作成する場合、デフォルトラベルが設定さますが、これに加えて独自ラベルを設定できます。例えば、git-shagit-branchなどを設定しておくと便利だったりします。

  <ItemGroup>
    <ContainerLabel Include="git-sha" Value="abcd123" />
    <ContainerLabel Include="git-branch" Value="FooBar" />
  </ItemGroup>
$ dotnet publish --os linux --arch x64 -c Release /t:PublishContainer
$ docker image inspect container-app-23:latest
[
    {
        {
            // 省略
            "Labels": {
                "git-branch": "FooBar",
                "git-sha": "abcd123",
                "net.dot.runtime.majorminor": "9.0",
                "net.dot.sdk.version": "9.0.200",
                "org.opencontainers.artifact.created": "2025-03-10T13:06:42.0776844Z",
                "org.opencontainers.image.authors": "ConsoleApp23",
                "org.opencontainers.image.base.digest": "sha256:e0ac714c1c99cf2b30e655ebd0f4bd6736b1d960a21b2c061711e43474aa5632",
                "org.opencontainers.image.base.name": "mcr.microsoft.com/dotnet/runtime:9.0",
                "org.opencontainers.image.created": "2025-03-10T13:06:42.0776844Z",
                "org.opencontainers.image.version": "1.0.0"
            }
        },
        "Architecture": "amd64",
        "Os": "linux",
        // 省略
    }
]

コンテナの公開ポートを設定する

EXPOSE相当です。ItemGroupでContainerPortを指定します。また、ASPNETCORE_から始まる定番の環境変数でポートが類推されます。

  • ASPNETCORE_URLS
  • ASPNETCORE_HTTP_PORTS
  • ASPNETCORE_HTTPS_PORTS

直接指定する場合は次のようにIncludeとTypeで設定します。

  <ItemGroup>
    <ContainerPort Include="8080" Type="tcp"/>
  </ItemGroup>

コンテナの環境変数を設定する

ENV相当です。ItemGroupでContainerEnvironmentVariableを指定します。

制約として、コマンドラインからdotnet publish -p:ContainerEnvironmentVariable=のように設定できないことがIssueで報告されています。

  <ItemGroup>
    <ContainerEnvironmentVariable Include="FOOBAR" Value="Trace" />
  </ItemGroup>

FOOBARという環境変数が設定されていますね。

$ dotnet publish --os linux --arch x64 -c Release /t:PublishContainer
$ docker image inspect container-app-23:latest
[
    {
        {
            // 省略
            "Env": [
                "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
                "APP_UID=1654",
                "ASPNETCORE_HTTP_PORTS=8080",
                "DOTNET_RUNNING_IN_CONTAINER=true",
                "DOTNET_VERSION=9.0.2",
                "FOOBAR=Trace"
            ],
        }
        // 省略
    }
]

USERの設定

USER相当です。ItemGroupでContainerUserを指定します。

.NET 8から.NETのコンテナはnon-rootを果たしました。このため、rootではなくappユーザー(UID/GIDは1654[^5])で動作します。non-rootならばUSERを変更する理由はあまりないはずですが、独自USERでやってる場合にどうぞ。

$ docker run -it --entrypoint /bin/bash container-app-23
app@0e671b15e9f6:/app$ whoami
app

もしもrootに変更したい場合は次のように設定します。

  <ItemGroup>
    <ContainerUser Include="root" />
  </ItemGroup>

あるいはコマンドラインでも設定できます。

$ dotnet publish -t:PublishContainer -c Release -r linux-amd64 -p:ContainerUser=root
$ docker run -it --entrypoint /bin/bash container-app-23
root@ec6dac910244:/app# whoami
root

FAQ

いくつかドキュメントに明示記載されていない挙動について検証した結果を残しておきます。

RUNのサポートはないのか

ありません、Dockerfileを使うことになります。例えばgrpc_health_probeを使わざるを得ない場合は、Dockerfile一択になります。

Microsoft.NET.Build.Containersパッケージ

ドキュメントでは、Microsoft.NET.Build.Containersパッケージを追加するケースも記載されています。.NET9ではこれがなくても問題なくビルドできるようです。

$ dotnet new console -o ConsoleApp24
$ cd ConsoleApp24
$ dotnet add package Microsoft.Net.Build.Containers
$ dotnet publish --os linux --arch x64 -c Release /t:PublishContainer
Restore complete (0.3s)
  ConsoleApp24 succeeded (1.2s) → bin\Release\net9.0\linux-x64\publish\

Build succeeded in 1.8s
# コンテナイメージが作成されない
$ docker image ls
REPOSITORY          TAG       IMAGE ID       CREATED          SIZE

ただ.NET 8でPublishAot=trueをする場合、あったほうが安定してビルドできるようです。この時、自身のSDKバージョンに合わせたMicrosoft.NET.Build.Containersバージョンをインストールするようにしましょう。

$ dotnet --list-sdks
8.0.112 [/usr/lib/dotnet/sdk]
$ cat ConsoleApp24.csproj
# 省略
  <ItemGroup>
    <PackageReference Include="Microsoft.NET.Build.Containers" Version="8.0.112" />
  </ItemGroup>

dotnet publish時のプロファイルはmsbuildで直接指定する

dotnet publish時は/t:PublishContainerを使ってコンテナイメージを作ると、どのようなプロジェクトでも安定してコンテナイメージを作成できます。

Microsoft LearnやGitHub Issue、あるいはGitHubの適当な文章を見ると、dotnet publish -p:PublishProfile=DefaultContainer-t:PublishContainerを使ってコンテナイメージを作成する方法が記載されていますが、ConsoleAppなどデフォルトでサポートされていないプラットフォームでコンテナイメージを作成できません。Web SDKプロジェクトなら-t:PublishContainerでイメージ作成されるのですが、ConsoleAppプロジェクトなどで-t:PublishContainerを使ってもイメージ作成されません。

# 微妙 (ConsoleAppだと発行できない)
$ dotnet publish --os linux --arch x64 -c Release -p:PublishProfile=DefaultContainer
Restore complete (0.8s)
  ConsoleApp19 succeeded with 1 warning(s) (1.3s) → bin\Release\net9.0\linux-x64\publish\
    C:\Program Files\dotnet\sdk\9.0.200\Sdks\Microsoft.NET.Sdk\targets\Microsoft.NET.Publish.targets(208,5): warning NETSDK1198: A publish profile with the name 'DefaultContainer' was not found in the project. Set the PublishProfile property to a valid file name.
$ docker image ls

# 微妙 (こっちも同じくConsoleAppだと発行できない)
$ dotnet publish --os linux --arch x64 -c Release -t:PublishContainer
Restore complete (0.2s)
  ConsoleApp23 succeeded (0.1s) → bin\Release\net9.0\linux-x64\publish\

Build succeeded in 0.5s
$ docker image ls

一方、/t:PublishContainerを使うとイメージが作成できています。

# 安定
$ dotnet publish --os linux --arch x64 -c Release /t:PublishContainer
Restore complete (0.2s)
  ConsoleApp19 succeeded (8.3s) → bin\Release\net9.0\linux-x64\publish\

Build succeeded in 8.7s
$ docker image ls
REPOSITORY          TAG       IMAGE ID       CREATED          SIZE
consoleapp19        latest    c5c9f37a3a4a   14 seconds ago   293MB
$ docker run -it consoleapp19
Hello, World!

ICU/tzdataが含まれるイメージか注意する

ベースイメージ変更時、alpinedistrolessubuntu chiseledイメージはICUやtzdataを含まないため国際対応で困ることが予想されます。csprojの設定がInvariantGlobalization=trueならこのイメージでいいのですが、ICUやtzdataを含む適切なイメージを使うといいでしょう。[^7]

<!-- falseを指定しても-extraイメージは自動選択されない -->
<PropertyGroup>
  <InvariantGlobalization>false</InvariantGlobalization>
</PropertyGroup>

ICUやtzdataが問題ないかは、globalappのような実行を使うといいです。実行例を見てみましょう。

デフォルトイメージの実行例

デフォルトイメージにはICU/tzdataが含まれるので、問題なく出力できます。

$ dotnet publish -t:PublishContainer -c Release -r linux-amd64
$ docker image ls
REPOSITORY          TAG       IMAGE ID       CREATED          SIZE
container-app-23    latest    c01119c971f4   3 minutes ago    293MB
$ docker run -it container-app-23
Hello, World!

****Print baseline timezones**
Utc: (UTC) Coordinated Universal Time; 03/10/2025 13:36:22
Local: (UTC) Coordinated Universal Time; 03/10/2025 13:36:22

****Print specific timezone**
Home timezone: America/Los_Angeles
DateTime at home: 03/10/2025 06:36:22

****Culture-specific dates**
Current: 03/10/2025
English (United States) -- en-US:
3/10/2025 1:36:22 PM
3/10/2025
1:36 PM
English (Canada) -- en-CA:
3/10/2025 1:36:22 p.m.
3/10/2025
1:36 p.m.
French (Canada) -- fr-CA:
2025-03-10 13 h 36 min 22 s
2025-03-10
13 h 36
Croatian (Croatia) -- hr-HR:
10. 03. 2025. 13:36:22
10. 03. 2025.
13:36
jp (Japan) -- jp-JP:
3/10/2025 13:36:22
3/10/2025
13:36
Korean (South Korea) -- ko-KR:
2025. 3. 10. 오후 1:36:22
2025. 3. 10.
오후 1:36
Portuguese (Brazil) -- pt-BR:
10/03/2025 13:36:22
10/03/2025
13:36
Chinese (China) -- zh-CN:
2025/3/10 13:36:22
2025/3/10
13:36

****Culture-specific currency:**
Current: ¤1,337.00
en-US: $1,337.00
en-CA: $1,337.00
fr-CA: 1 337,00 $
hr-HR: 1.337,00 €
jp-JP: ¥ 1337
ko-KR: ₩1,337
pt-BR: R$ 1.337,00
zh-CN: ¥1,337.00

****Japanese calendar**
08/18/2019
01/08/18
平成元年818日
平成元年818日

****String comparison**
Comparison results: `0` mean equal, `-1` is less than and `1` is greater
Test: compare i to (Turkish) İ; first test should be equal and second not
0
-1
Test: compare Å Å; should be equal
0

chiseledイメージの実行例

chiseledイメージにはICU/tzdataが含まれないため、tzdataへアクセスできずエラーが発生します。これはICUでも同様にトラブルを起こします。

$ dotnet publish -t:PublishContainer -c Release -r linux-amd64 -p:ContainerFamily=noble-chiseled
$ docker image ls
REPOSITORY          TAG       IMAGE ID       CREATED          SIZE
container-app-23    latest    8a083166147b   4 seconds ago    133MB

$ docker run -it container-app-23
Hello, World!

****Print baseline timezones**
Utc: (UTC) Coordinated Universal Time; 03/10/2025 13:35:54
Local: (UTC) Coordinated Universal Time; 03/10/2025 13:35:54

****Print specific timezone**
Unhandled exception. System.TimeZoneNotFoundException: The time zone ID 'America/Los_Angeles' was not found on the local computer.
 ---> System.IO.DirectoryNotFoundException: Could not find a part of the path '/usr/share/zoneinfo/America/Los_Angeles'.
   at Interop.ThrowExceptionForIoErrno(ErrorInfo errorInfo, String path, Boolean isDirError)
   at Microsoft.Win32.SafeHandles.SafeFileHandle.Open(String path, OpenFlags flags, Int32 mode, Boolean failForSymlink, Boolean& wasSymlink, Func`4 createOpenException)
   at Microsoft.Win32.SafeHandles.SafeFileHandle.Open(String fullPath, FileMode mode, FileAccess access, FileShare share, FileOptions options, Int64 preallocationSize, UnixFileMode openPermissions, Int64& fileLength, UnixFileMode& filePermissions, Boolean failForSymlink, Boolean& wasSymlink, Func`4 createOpenException)
   at System.IO.Strategies.OSFileStreamStrategy..ctor(String path, FileMode mode, FileAccess access, FileShare share, FileOptions options, Int64 preallocationSize, Nullable`1 unixCreateMode)
   at System.TimeZoneInfo.ReadAllBytesFromSeekableNonZeroSizeFile(String path, Int32 maxFileSize)
   at System.TimeZoneInfo.TryGetTimeZoneFromLocalMachineCore(String id, TimeZoneInfo& value, Exception& e)
   --- End of inner exception stack trace ---
   at System.TimeZoneInfo.FindSystemTimeZoneById(String id)
   at Program.<Main>$(String[] args) in C:\Users\guitarrapc\source\repos\ConsoleApp23\Program.cs:line 24

chiseled-extraイメージの実行例

chiseledイメージでも-extraをつけたものはICU/tzdataが含まれているため、問題なく出力できます。

$ dotnet publish -t:PublishContainer -c Release -r linux-amd64 -p:ContainerFamily=noble-chiseled-extra
Restore complete (0.2s)
  ConsoleApp23 succeeded (10.5s) → bin\Release\net9.0\linux-x64\publish\

Build succeeded in 10.9s

$ docker image ls
REPOSITORY          TAG       IMAGE ID       CREATED          SIZE
container-app-23    latest    b2db2bfebb37   20 seconds ago   190MB

$ docker run -it container-app-23
Hello, World!

****Print baseline timezones**
Utc: (UTC) Coordinated Universal Time; 03/10/2025 13:34:50
Local: (UTC) Coordinated Universal Time; 03/10/2025 13:34:50

****Print specific timezone**
Home timezone: America/Los_Angeles
DateTime at home: 03/10/2025 06:34:50

****Culture-specific dates**
Current: 03/10/2025
English (United States) -- en-US:
3/10/2025 1:34:50 PM
3/10/2025
1:34 PM
English (Canada) -- en-CA:
2025-03-10 1:34:50 p.m.
2025-03-10
1:34 p.m.
French (Canada) -- fr-CA:
2025-03-10 13 h 34 min 50 s
2025-03-10
13 h 34
Croatian (Croatia) -- hr-HR:
10. 03. 2025. 13:34:50
10. 03. 2025.
13:34
jp (Japan) -- jp-JP:
3/10/2025 13:34:50
3/10/2025
13:34
Korean (South Korea) -- ko-KR:
2025. 3. 10. 오후 1:34:50
2025. 3. 10.
오후 1:34
Portuguese (Brazil) -- pt-BR:
10/03/2025 13:34:50
10/03/2025
13:34
Chinese (China) -- zh-CN:
2025/3/10 13:34:50
2025/3/10
13:34

****Culture-specific currency:**
Current: ¤1,337.00
en-US: $1,337.00
en-CA: $1,337.00
fr-CA: 1 337,00 $
hr-HR: 1.337,00 €
jp-JP: ¥ 1337
ko-KR: ₩1,337
pt-BR: R$ 1.337,00
zh-CN: ¥1,337.00

****Japanese calendar**
08/18/2019
01/08/18
平成元年818日
平成元年818日

****String comparison**
Comparison results: `0` mean equal, `-1` is less than and `1` is greater
Test: compare i to (Turkish) İ; first test should be equal and second not
0
-1
Test: compare Å Å; should be equal
0

alt text

まとめ

Dockerfileじゃなくてもdotnet publishでのコンテナ作成はずいぶんといい感じになってきていました。Dockerfileは必ずしも意識しないといけないものではないので、言語SDKからコンテナ作成できるのは便利です。ただ、RUNのサポートがないことや、マルチプラットフォームイメージを作成する際にプラットフォームイメージを省略できないのは使いにくさがあります。

.NET 8のnon-root化やchiseledイメージをはじめとして、.NETのコンテナエコシステムも着実によくなってきているので、今後も期待していきたいですね。ただ、コンテナに任せていた部分は、今後WASMサンドボックスの発展で置き換わっていくとより良いなぁとも感じます。

参考

Issue


  1. それまではメモリ制限での動作が不十分で、500MB未満のメモリ制限だとOOMされてしまう問題があった
  2. マルチプラットフォーム対応できないので、ファミリーをタグにつけて分別している
  3. ContainerRuntimeIdentifiersRuntimeIdentifiersと一致させるならあっても無害ですが、省略しても変わりません。
  4. DockerfileだとDockerfileのFROMでベースイメージが固定されるのでここは明確に違います
  5. PropertyGroupじゃないので注意です。