tech.guitarrapc.cóm

Technical updates

.NETのコンテナイメージをマルチプラットフォーム対応する

C#をLinuxやコンテナで動かせるようになって久しくC#をコンテナで実行している人も多いでしょう。 C#はlinuxのamd64(x86_64)とarm64の両方に対応しているので、ライブラリや実装コードに気を払えば「同一コードベースでマルチプラットフォーム動作」が可能です。 また、Visual StudioでDockerfileを自動生成できることも手伝いC#のコンテナイメージは簡単に作れます。

今回は、そんなC#のコンテナイメージをlinux/amd64・linux/arm64のマルチプラットフォームに対応させるやり方を紹介します。

構成

マルチプラットフォームコンテナを作るにあたって、運用上欠かせないポイントがDockerfile管理です。 amd64arm64それぞれのDockerfileを用意する方法もありますが、管理が煩雑すぎて運用したくありません。 このため、単一Dockerfileでamd64/arm64の両方に対応させます。

.csprojのコンテナ作成はどうなの?

中にはcsprojからコンテナイメージを作る方法もご存じの方もいるでしょう。 しかし、csprojのコンテナイメージ定義は制約が多くまだ一般的に用いるには難しいものがあります。1

Dockerfileは2025年になってもコンテナを作る主流の方法であり、まだまだ使い続けることになるでしょう。

ベースとなるDockerfile

ベースとするC#のDockerfileは、Tutorial: Containerize a .NET app | Microsoft Learnに記載されたものです。

プロジェクト作成からDockerfile作成までの手順を示します。

mkdir DockerConsoleApp
cd DockerConsoleApp
dotnet new console -o ConsoleApp
dotnet new sln
dotnet sln add ConsoleApp/ConsoleApp.csproj

Visual StudioでDocker Support... を選択してDockerfileを作成します。

alt text

もし直接Dockerfileを作成する場合は、以下のようなDockerfileを作成します。 これで準備は完了です。

# This stage is used when running from VS in fast mode (Default for Debug configuration)
FROM mcr.microsoft.com/dotnet/runtime:9.0 AS base
USER $APP_UID
WORKDIR /app


# This stage is used to build the service project
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
ARG BUILD_CONFIGURATION=Release
WORKDIR /src
COPY ["ConsoleApp/ConsoleApp.csproj", "ConsoleApp/"]
RUN dotnet restore "./ConsoleApp/ConsoleApp.csproj"
COPY . .
WORKDIR "/src/ConsoleApp"
RUN dotnet build "./ConsoleApp.csproj" -c $BUILD_CONFIGURATION -o /app/build

# This stage is used to publish the service project to be copied to the final stage
FROM build AS publish
ARG BUILD_CONFIGURATION=Release
RUN dotnet publish "./ConsoleApp.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false

# This stage is used in production or when running from VS in regular mode (Default when not using the Debug configuration)
FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "ConsoleApp.dll"]

コンテナビルドするには以下のコマンドを実行します。2

$ docker build -t consoleapp:latest -f ConsoleApp/Dockerfile .

ビルドログ

[+] Building 6.5s (18/18) FINISHED                                                                 docker:desktop-linux
 => [internal] load build definition from Dockerfile                                                               0.0s
 => => transferring dockerfile: 1.11kB                                                                             0.0s
 => [internal] load metadata for mcr.microsoft.com/dotnet/sdk:9.0                                                  0.0s
 => [internal] load metadata for mcr.microsoft.com/dotnet/runtime:9.0                                              0.0s
 => [internal] load .dockerignore                                                                                  0.0s
 => => transferring context: 464B                                                                                  0.0s
 => [internal] load build context                                                                                  0.0s
 => => transferring context: 2.76kB                                                                                0.0s
 => [base 1/2] FROM mcr.microsoft.com/dotnet/runtime:9.0@sha256:f50efa2ae5fdb6c100d691b599dae5b1265520371bf79850e  0.2s
 => => resolve mcr.microsoft.com/dotnet/runtime:9.0@sha256:f50efa2ae5fdb6c100d691b599dae5b1265520371bf79850ec46cf  0.2s
 => [build 1/7] FROM mcr.microsoft.com/dotnet/sdk:9.0@sha256:1025bed126a7b85c56b960215ab42a99db97a319a72b5d902383  0.3s
 => => resolve mcr.microsoft.com/dotnet/sdk:9.0@sha256:1025bed126a7b85c56b960215ab42a99db97a319a72b5d902383ebf6c6  0.2s
 => CACHED [base 2/2] WORKDIR /app                                                                                 0.0s
 => [final 1/2] WORKDIR /app                                                                                       0.0s
 => [build 2/7] WORKDIR /src                                                                                       0.0s
 => [build 3/7] COPY [ConsoleApp/ConsoleApp.csproj, ConsoleApp/]                                                   0.0s
 => [build 4/7] RUN dotnet restore "./ConsoleApp/ConsoleApp.csproj"                                                2.9s
 => [build 5/7] COPY . .                                                                                           0.0s
 => [build 6/7] WORKDIR /src/ConsoleApp                                                                            0.0s
 => [build 7/7] RUN dotnet build "./ConsoleApp.csproj" -c Release -o /app/build                                    2.0s
 => [publish 1/1] RUN dotnet publish "./ConsoleApp.csproj" -c Release -o /app/publish /p:UseAppHost=false          0.9s
 => [final 2/2] COPY --from=publish /app/publish .                                                                 0.0s
 => exporting to image                                                                                             0.1s
 => => exporting layers                                                                                            0.0s
 => => exporting manifest sha256:aec2edda5e9c191818c72ac702702a119ef4f8a95d76ff26dc2f96ec1b2397bb                  0.0s
 => => exporting config sha256:a3659d5ccb6618991748e9fea164a930aad83ab1f78a8e23dea0389d549c8e65                    0.0s
 => => exporting attestation manifest sha256:8885e57c58e28d81ac5562e7b4c5735b05d6665f35d598bba33b8b5e8ab5dc41      0.0s
 => => exporting manifest list sha256:088e2f8b275527710c9c8fa4649ae8937fd490db17e7be725fadf12e8275c3d2             0.0s
 => => naming to docker.io/library/consoleapp:latest                                                               0.0s
 => => unpacking to docker.io/library/consoleapp:latest                                                            0.0s

What's next:
    View a summary of image vulnerabilities and recommendations → docker scout quickview

デフォルトのDockerfileは何をしているのか

ベースに使うDockerfileはマルチステージビルドを使っています。マルチステージビルドの詳細はChatGPTにでも聞いていただくとして、このDockerfileは以下のようなことをしています。 ようはビルドして作った成果物を.NETランタイムで実行しているだけです。

# .NETランタイムイメージで最後にアプリケーションを動かす環境を指定
FROM mcr.microsoft.com/dotnet/runtime:9.0 AS base

# .NET SDKイメージでアプリケーションをビルド、Publish
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build

# 先の.NETランタイムイメージに.NET SDKイメージでビルドしたアプリケーションをコピーして実行
FROM base AS final

利用イメージがマルチプラットフォーム対応しているか確認する

単一Dockerfileでマルチプラットフォームなコンテナイメージを作るには、FROMで指定したベースイメージがマルチプラットフォーム対応している必要があります。

ベースイメージがマルチプラットフォーム対応しているか確認するにはdocker manifest inspectコマンドを使います。今回利用するdotnet/runtimedotnet/sdkは両方ともマルチプラットフォームなイメージです。3

$ docker manifest inspect mcr.microsoft.com/dotnet/sdk:9.0
{
   "schemaVersion": 2,
   "mediaType": "application/vnd.docker.distribution.manifest.list.v2+json",
   "manifests": [
      {
         "mediaType": "application/vnd.docker.distribution.manifest.v2+json",
         "size": 2217,
         "digest": "sha256:c6f7685fc77854c289440fba406c03a722ca3a593fe897fd1e0b8a31eaa3891e",
         "platform": {
            "architecture": "amd64",
            "os": "linux"
         }
      },
      {
         "mediaType": "application/vnd.docker.distribution.manifest.v2+json",
         "size": 2217,
         "digest": "sha256:455ede8ab2a329e662af05c66506bb8bfbe9cf31b0dea6c300f636908384e689",
         "platform": {
            "architecture": "arm",
            "os": "linux",
            "variant": "v7"
         }
      },
      {
         "mediaType": "application/vnd.docker.distribution.manifest.v2+json",
         "size": 2217,
         "digest": "sha256:392dfe30a61be4f89b3b4425112e7dab34ed2056c4edf642c2c0b95a2b2eeb07",
         "platform": {
            "architecture": "arm64",
            "os": "linux"
         }
      }
   ]
}

$ docker manifest inspect mcr.microsoft.com/dotnet/runtime:9.0
{
   "schemaVersion": 2,
   "mediaType": "application/vnd.docker.distribution.manifest.list.v2+json",
   "manifests": [
      {
         "mediaType": "application/vnd.docker.distribution.manifest.v2+json",
         "size": 1368,
         "digest": "sha256:e0ac714c1c99cf2b30e655ebd0f4bd6736b1d960a21b2c061711e43474aa5632",
         "platform": {
            "architecture": "amd64",
            "os": "linux"
         }
      },
      {
         "mediaType": "application/vnd.docker.distribution.manifest.v2+json",
         "size": 1368,
         "digest": "sha256:b430a862d224ff14a34abf3b7c40099d57dfafbe51c54e9c20547a1d9f17c88c",
         "platform": {
            "architecture": "arm",
            "os": "linux",
            "variant": "v7"
         }
      },
      {
         "mediaType": "application/vnd.docker.distribution.manifest.v2+json",
         "size": 1368,
         "digest": "sha256:4f424e663bfc295b029b1eb9ddae19f2e4d4f823add24b39bcb22c0be612845f",
         "platform": {
            "architecture": "arm64",
            "os": "linux"
         }
      }
   ]
}

マルチプラットフォームビルドに対応する

アプリケーションコードがamd64/arm64の両方に対応していることは必要条件です。本記事のスコープ外なので、ここではDockerfileの変更に焦点を当てます。

先ほどのDockerfileを2行変更するだけで、マルチプラットフォーム対応のコンテナイメージを作成できます。具体的には、FROM mcr.microsoft.com/dotnet/sdkステージを変更します。

  1. FROMの直後に--platform=$BUILDPLATFORMを追加する
  2. ARG TARGETARCHを追加する

$BUILDPLATFORM$TARGETARCHの詳細はAutomatic platform ARGs in the global scope | Dockerに詳細が書かれています。

  • BUILDPLATFORM - platform of the node performing the build
  • TARGETARCH - architecture component of TARGETPLATFORM
  • TARGETPLATFORM - platform of the build result. Eg linux/amd64, linux/arm/v7, windows/amd64

before

FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build

after

FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:9.0 AS build
ARG TARGETARCH

Dockerfile全体像

# This stage is used when running from VS in fast mode (Default for Debug configuration)
FROM mcr.microsoft.com/dotnet/runtime:9.0 AS base
USER $APP_UID
WORKDIR /app


# This stage is used to build the service project
FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:9.0 AS build
ARG TARGETARCH
ARG BUILD_CONFIGURATION=Release
WORKDIR /src
COPY ["ConsoleApp/ConsoleApp.csproj", "ConsoleApp/"]
RUN dotnet restore "./ConsoleApp/ConsoleApp.csproj"
COPY . .
WORKDIR "/src/ConsoleApp"
RUN dotnet build "./ConsoleApp.csproj" -c $BUILD_CONFIGURATION -o /app/build

# This stage is used to publish the service project to be copied to the final stage
FROM build AS publish
ARG BUILD_CONFIGURATION=Release
RUN dotnet publish "./ConsoleApp.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false

# This stage is used in production or when running from VS in regular mode (Default when not using the Debug configuration)
FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "ConsoleApp.dll"]

マルチプラットフォームビルドを実行する

マルチプラットフォームビルドをするときはdoker buildxコマンドを使います。ポイントは--platform linux/amd64,linux/arm64で、先ほどのDockerfileに--platform=$BUILDPLATFORMのように渡されるイメージです。

$ docker buildx build --platform linux/amd64,linux/arm64 -t consoleapp:latest -f ConsoleApp/Dockerfile .

ビルドログを見ると、linux/amd64linux/arm64の両方でビルドされていることがわかります。

[+] Building 7.3s (28/28) FINISHED                                                                            docker:desktop-linux
 => [internal] load build definition from Dockerfile                                                                         0.0s
 => => transferring dockerfile: 1.11kB                                                                                       0.0s
 => [linux/amd64 internal] load metadata for mcr.microsoft.com/dotnet/sdk:9.0                                                0.1s
 => [linux/amd64 internal] load metadata for mcr.microsoft.com/dotnet/runtime:9.0                                            0.1s
 => [linux/arm64 internal] load metadata for mcr.microsoft.com/dotnet/runtime:9.0                                            0.5s
 => [internal] load .dockerignore                                                                                            0.0s
 => => transferring context: 464B                                                                                            0.0s
 => [linux/amd64 build 1/7] FROM mcr.microsoft.com/dotnet/sdk:9.0@sha256:1025bed126a7b85c56b960215ab42a99db97a319a72b5d90238 0.2s
 => => resolve mcr.microsoft.com/dotnet/sdk:9.0@sha256:1025bed126a7b85c56b960215ab42a99db97a319a72b5d902383ebf6c6e62bbe      0.2s
 => [internal] load build context                                                                                            0.0s
 => => transferring context: 262B                                                                                            0.0s
 => [linux/amd64 base 1/2] FROM mcr.microsoft.com/dotnet/runtime:9.0@sha256:f50efa2ae5fdb6c100d691b599dae5b1265520371bf79850 0.1s
 => => resolve mcr.microsoft.com/dotnet/runtime:9.0@sha256:f50efa2ae5fdb6c100d691b599dae5b1265520371bf79850ec46cf43cd73810d  0.1s
 => [linux/arm64 base 1/2] FROM mcr.microsoft.com/dotnet/runtime:9.0@sha256:f50efa2ae5fdb6c100d691b599dae5b1265520371bf79850 4.7s
 => => resolve mcr.microsoft.com/dotnet/runtime:9.0@sha256:f50efa2ae5fdb6c100d691b599dae5b1265520371bf79850ec46cf43cd73810d  0.1s
 => => sha256:05feba1ea79b38e3da8ea189434d8cc73f7ddd70cf4fe0ef1d2c7a6b63857a04 165B / 165B                                   0.1s
 => => sha256:c11a0280f3980af51c5807e4c68752d0b057e7c9f612fe9a51786cea74ce93e9 32.83MB / 32.83MB                             3.1s
 => => sha256:123009df50c6dcdf34077004bd9acfe33da0e667276c2cf4c9510b7719e8064a 3.33kB / 3.33kB                               0.2s
 => => sha256:570895edc796532a2801ea7b6b50a6f6e595f26d7d22bd6dcc64bc50d3339f71 18.50MB / 18.50MB                             1.4s
 => => sha256:d51c377d94dadb60d549c51ba66d3c4eeaa8bace4935d570ee65d8d1141d38fc 28.05MB / 28.05MB                             3.4s
 => => extracting sha256:d51c377d94dadb60d549c51ba66d3c4eeaa8bace4935d570ee65d8d1141d38fc                                    0.6s
 => => extracting sha256:570895edc796532a2801ea7b6b50a6f6e595f26d7d22bd6dcc64bc50d3339f71                                    0.2s
 => => extracting sha256:123009df50c6dcdf34077004bd9acfe33da0e667276c2cf4c9510b7719e8064a                                    0.0s
 => => extracting sha256:c11a0280f3980af51c5807e4c68752d0b057e7c9f612fe9a51786cea74ce93e9                                    0.2s
 => => extracting sha256:05feba1ea79b38e3da8ea189434d8cc73f7ddd70cf4fe0ef1d2c7a6b63857a04                                    0.0s
 => CACHED [linux/amd64->arm64 build 2/7] WORKDIR /src                                                                       0.0s
 => CACHED [linux/amd64->arm64 build 3/7] COPY [ConsoleApp/ConsoleApp.csproj, ConsoleApp/]                                   0.0s
 => CACHED [linux/amd64 base 2/2] WORKDIR /app                                                                               0.0s
 => CACHED [linux/amd64 final 1/2] WORKDIR /app                                                                              0.0s
 => CACHED [linux/amd64 build 4/7] RUN dotnet restore "./ConsoleApp/ConsoleApp.csproj"                                       0.0s
 => CACHED [linux/amd64 build 5/7] COPY . .                                                                                  0.0s
 => CACHED [linux/amd64 build 6/7] WORKDIR /src/ConsoleApp                                                                   0.0s
 => CACHED [linux/amd64 build 7/7] RUN dotnet build "./ConsoleApp.csproj" -c Release -o /app/build                           0.0s
 => CACHED [linux/amd64 publish 1/1] RUN dotnet publish "./ConsoleApp.csproj" -c Release -o /app/publish /p:UseAppHost=false 0.0s
 => CACHED [linux/amd64 final 2/2] COPY --from=publish /app/publish .                                                        0.0s
 => [linux/amd64->arm64 build 4/7] RUN dotnet restore "./ConsoleApp/ConsoleApp.csproj"                                       3.2s
 => [linux/amd64->arm64 build 5/7] COPY . .                                                                                  0.0s
 => [linux/amd64->arm64 build 6/7] WORKDIR /src/ConsoleApp                                                                   0.0s
 => [linux/amd64->arm64 build 7/7] RUN dotnet build "./ConsoleApp.csproj" -c Release -o /app/build                           2.1s
 => [linux/arm64 base 2/2] WORKDIR /app                                                                                      0.2s
 => [linux/arm64 final 1/2] WORKDIR /app                                                                                     0.0s
 => [linux/amd64->arm64 publish 1/1] RUN dotnet publish "./ConsoleApp.csproj" -c Release -o /app/publish /p:UseAppHost=false 0.9s
 => [linux/arm64 final 2/2] COPY --from=publish /app/publish .                                                               0.0s
 => exporting to image                                                                                                       0.1s
 => => exporting layers                                                                                                      0.1s
 => => exporting manifest sha256:aec2edda5e9c191818c72ac702702a119ef4f8a95d76ff26dc2f96ec1b2397bb                            0.0s
 => => exporting config sha256:a3659d5ccb6618991748e9fea164a930aad83ab1f78a8e23dea0389d549c8e65                              0.0s
 => => exporting attestation manifest sha256:e54ed337755d8d5e2b099c13378d94029c934c7a40ae0516005a7bc5fa8ca511                0.0s
 => => exporting manifest sha256:0aca481b3beaa34e80cef665604b5271403320322496d8df36fb300e77f70200                            0.0s
 => => exporting config sha256:0ca315ff93053fec45844088c33a91b8ae2f62cd74cd944908ad7478057a848a                              0.0s
 => => exporting attestation manifest sha256:045fc0cb5cb49c4efc2a67fcb66571735a15367e2b2251f82dd498480f485b94                0.0s
 => => exporting manifest list sha256:d0a41d56254572debef45931332412b6efd33412879ef6fc222559cf0a104d57                       0.0s
 => => naming to docker.io/library/consoleapp:latest                                                                         0.0s
 => => unpacking to docker.io/library/consoleapp:latest                                                                      0.0s

What's next:
    View a summary of image vulnerabilities and recommendations → docker scout quickview

GitHub Actionsでマルチプラットフォームビルドを実行する

GitHub Actionsでマルチプラットフォームビルドを実行する時はdocker/build-push-actionが便利です。docker buildxを直接実行してもいいのですが、コマンドを意識しなくていいならそれに越したことはありません。

複数アーキテクチャ向けにビルドするためQEMUとBuildxをセットアップする必要があることに注意してください。

name: Docker Build

on:
  push:
    branches:
      - "main"

jobs:
  docker:
    runs-on: ubuntu-latest
    timeout-minutes: 10
    steps:
      - uses: actions/checkout@v4
      - name: Login to Docker Hub
        uses: docker/login-action@v3
        with:
          username: ${{ vars.DOCKERHUB_USERNAME  }}
          password: ${{ secrets.DOCKERHUB_TOKEN  }}
      - name: Set up QEMU
        uses: docker/setup-qemu-action@v3
      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3
      - name: Build and push
        uses: docker/build-push-action@v6
        with:
          platforms: linux/amd64,linux/arm64
          push: true
          file: ConsoleApp/Dockerfile
          tags: consoleapp:latest

まとめ

C#でもARM64向けにビルド、動作させることは現実的なワークフロードとして選択肢に入れて当然になってきました。 特に.NET 9はコンテナワークロードで有利になるGCの改善が入ったこともあって、.NET 8よりメモリ的なトラブルも減ります。

各クラウドプラットフォームにおいてamd64で動作させていアプリケーションをarm64に切り替えていくことが増えていくでしょう。その時は、Dockerfileのマルチプラットフォーム対応を覚えておくと便利です。

C#のアプリケーションビルド自体もDockerfile上で実行できると、マルチプラットフォーム向けのイメージ作成がかなり楽になるので検討してみてください。

参考


  1. aptなどでパッケージ追加、コンテナビルド時の任意の処理追加などはサポートされていない
  2. C#プロジェクトのコンテナビルドは、基本的にslnファイルのあるパスが起点になります
  3. linux/amd64linux/arm/v7linux/arm64に対応しており、作りたい環境が含まれている