C#をLinuxやコンテナで動かせるようになって久しくC#をコンテナで実行している人も多いでしょう。 C#はlinuxのamd64(x86_64)とarm64の両方に対応しているので、ライブラリや実装コードに気を払えば「同一コードベースでマルチプラットフォーム動作」が可能です。 また、Visual StudioでDockerfileを自動生成できることも手伝いC#のコンテナイメージは簡単に作れます。
今回は、そんなC#のコンテナイメージをlinux/amd64・linux/arm64のマルチプラットフォームに対応させるやり方を紹介します。
構成
マルチプラットフォームコンテナを作るにあたって、運用上欠かせないポイントがDockerfile管理です。
amd64とarm64それぞれの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を作成します。

もし直接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/runtimeとdotnet/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ステージを変更します。
FROMの直後に--platform=$BUILDPLATFORMを追加する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/amd64とlinux/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上で実行できると、マルチプラットフォーム向けのイメージ作成がかなり楽になるので検討してみてください。