How to Build Multi-Architecture Container Images for .NET Applications

Raul NaupariRaul Naupari
3 min read

Containers are increasingly popular due to their ability to package software as images that can run anywhere. This concept is exciting but poses challenges when dealing with multiple architectures. Fortunately, features like multi-architecture support in Docker and cross-platform compilation in .NET applications help us overcome these challenges with a few additional steps.

Pre-requisites

Create Dockerfile

We will use the webapi template to create our application. Run the following commands to set up the solution:

dotnet new webapi -n MyWebApi
dotnet new sln -n Sandbox
dotnet sln add --in-root MyWebApi

Create a Dockerfile in the project directory with the following content:

FROM --platform=$TARGETPLATFORM mcr.microsoft.com/dotnet/aspnet:8.0 AS runtime
USER app
WORKDIR /app
EXPOSE 8080
EXPOSE 8081

FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0 AS build
ARG BUILD_CONFIGURATION=Release
ARG BUILDPLATFORM
ARG TARGETARCH
ARG TARGETOS
WORKDIR /src
COPY ["MyWebApi/MyWebApi.csproj", "MyWebApi/"]
RUN dotnet restore "./MyWebApi/MyWebApi.csproj" -a $TARGETARCH 
COPY . .
RUN echo "Building on $BUILDPLATFORM, targeting $TARGETOS/$TARGETARCH"

WORKDIR "/src/MyWebApi"

FROM build AS publish
ARG BUILD_CONFIGURATION=Release
RUN dotnet publish -a $TARGETARCH --no-restore "./MyWebApi.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false

FROM runtime AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "MyWebApi.dll"]

Docker provides arguments to specify the build and target platforms:

  • The build arguments are related to the host where the builder runs (BUILDPLATFORM, BUILDOS, BUILDARCH, and BUILDVARIANT).

  • The target arguments include the values set with the --platform flag in the build command (TARGETPLATFORM, TARGETOS, TARGETARCH, and TARGETVARIANT).

These arguments are available in the global scope of the Dockerfile but not within the build stages. To use these arguments inside a stage, declare them as arguments, and Docker will automatically set their values. Here's how we can use them:

  • The FROM command uses the --platform flag to specify the image's platform, such as linux/amd64, linux/arm64, or windows/amd64. In the Dockerfile above, the image used for building the application includes the --platform=$BUILDPLATFORM flag to align with the host's platform and avoid any issues. The image used for running the application includes the --platform=$TARGETPLATFORM flag to match the platform where the application will run.

  • Additionally, the dotnet publish and dotnet restore commands use the -a $TARGETARCH option to define the target architecture, such as arm64 or amd64.

Build Images

To build an image for each target platform, execute the following command at the solution level:

docker build --platform linux/amd64 --tag raulnq/mywebapi:amd64 --file .\MyWebApi\Dockerfile .
docker build --platform linux/arm64 --tag raulnq/mywebapi:arm64 --file .\MyWebApi\Dockerfile .

The --platform flag specifies the target platform for the build. As previously mentioned, it only passes the value to the Dockerfile, which performs the work. By default, the value is set to the host's platform where the build runs, formatted as os/arch or os/arch/variant.

Create Manifest

A manifest is a JSON file that describes a set of images for different platforms, collectively known as a manifest list. It allows multiple images to be associated with a single reference. When a container is deployed using this manifest, the correct image is selected based on the host's platform. To create the manifest, use the following command:

 docker manifest create raulnq/webapi:1.0 raulnq/mywebapi:amd64 raulnq/mywebapi:arm64

We can inspect the manifest's content using the command docker manifest inspect raulnq/webapi:1.0:

{
    "schemaVersion": 2,
    "mediaType": "application/vnd.oci.image.index.v1+json",
    "manifests": [
        {
            "mediaType": "application/vnd.oci.image.manifest.v1+json",
            "size": 1522,
            "digest": "sha256:43ab8996af783470032926fb6c63b25313174d50b360a78f9e6f70344a706df3",
            "platform": {
                "architecture": "amd64",
                "os": "linux"
            }
        },
        {
            "mediaType": "application/vnd.oci.image.manifest.v1+json",
            "size": 1523,
            "digest": "sha256:a095526a10b3c981fbe17bb7f2a2cb1e236989463bcfe3eb0b5df654c351e0b4",
            "platform": {
                "architecture": "arm64",
                "os": "linux"
            }
        }
    ]
}

This allows us to verify the size and digest for each image manifest included in the manifest.

Push manifest

As the final step, we can push the manifest to our preferred container registry using the following command:

docker manifest push raulnq/webapi:1.0

It is worth mentioning that all the commands showed today are fully compatible with Podman. Thank you, and happy coding.

0
Subscribe to my newsletter

Read articles from Raul Naupari directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Raul Naupari
Raul Naupari

Somebody who likes to code