Containerising ASP.NET 7 Web Apps as Docker Images for Linux ARM with GitHub Actions

Containerising ASP.NET 7 Web Apps as Docker Images for Linux ARM with GitHub Actions

Docker is a great way to publish and deploy .NET applications, without having to worry about dependencies being missing on the host machine. With the boost in popularity of ARM, it’s also becoming increasingly likely that the application may end up running on an ARM based processor.

However, with the release of .NET 7 Microsoft removed support for building .NET projects under QEMU. This was the most common approach to building .NET projects for platforms different to the machine doing the build. For example, using a x86-64 based platform to build a .NET project to be deployed to an ARM based platform.

This means that the .NET template which includes Docker support, will no longer work out of the box when trying to build a ARM Docker image for your .NET application on GitHub Actions.

The Current Build Process

The included .NET template with Docker support enables includes a Dockerfile which looks similar to the following.

FROM mcr.microsoft.com/dotnet/aspnet:7.0 AS base
WORKDIR /app
EXPOSE 80
EXPOSE 443

FROM mcr.microsoft.com/dotnet/sdk:7.0 AS build
WORKDIR /src
COPY ["DockerWebApp/DockerWebApp.fsproj", "DockerWebApp/"]
RUN dotnet restore "DockerWebApp/DockerWebApp.fsproj"
COPY . .
WORKDIR "/src/DockerWebApp"
RUN dotnet build "DockerWebApp.fsproj" -c Release -o /app/build

FROM build AS publish
RUN dotnet publish "DockerWebApp.fsproj" -c Release -o /app/publish /p:UseAppHost=false

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

This Dockerfile publishes a new Docker image for the web application in 4 main steps.

  1. Create a new “base” image based on the Microsoft provided ASP.NET 7 image.
  2. Use the Microsoft provided .NET 7 SDK image to build the .NET project.
  3. Publish the .NET project to a folder in a “publish” image.
  4. Copy the published application files to a “final” Docker image which uses the original “base” image.

When you use the docker/build-push-action GitHub Action to build and push a multi-platform Docker image for this application, it will fail with the following error message.

buildx failed with: ERROR: failed to solve: failed to read dockerfile: open /tmp/buildkit-mount3605268226/Dockerfile: no such file or directory

Trying to find the cause of the error eventually led me to this GitHub issue, and then to the Supported OS Policy indicating the .NET 7 is unsupported under QEMU.

Breakdown of a compiled ASP.NET Web App

There may be another way to build a multi-platform Docker image, but to understand this, we need to understand how a framework dependent .NET application is compiled and deployed for different platforms.

There are three key areas of a .NET app, these are:

  • .NET runtime
  • .NET app executable
  • Your compiled app binary

.NET Runtime

The .NET runtime is the framework which you install to enable a framework-dependent .NET app to run. This is specific for each platform, so for each operating system and architecture combination you will install a different copy of the .NET runtime.

.NET App Executable

This is an executable file which is created when your application is published. For example, if your app is published for Windows a myappname.exe executable is created, while if it is published for Linux a myappname executable is created. This file makes it easier to run your application without having to use dotnet myappname.dll.

Compiled App Binary

This is the file which contains the code for your application. This is a cross-platform binary which can run on any platform with an appropriate .NET runtime installed. You can run your application directly using dotnet myappname.dll.

The framework-dependent app binary is cross-platform, so it can run on any platform so long as a version of the .NET runtime is present. This means we can copy the compiled application binary into the multi-platform .NET runtime Docker image and then run our application regardless of the platform it is actually running on.

Building the ASP.NET Web App

To build the ASP.NET web application we can use the commands provided by the .NET SDK.

We would need to use the following command to restore the packages for our application.

dotnet restore

Then use the command below to build the application.

dotnet build --no-restore

The last step is to use the publish command to publish our web application.

dotnet publish --no-build

After this you should have a cross-platform application binary named myappname.dll in the /bin/Release/.net7.0/publish directory.

Creating the Docker image

The next step is to make some changes to the Dockerfile so that it will copy the existing application binary into the Microsoft provided .NET runtime docker image, instead of trying to build the application specifically for each platform.

FROM mcr.microsoft.com/dotnet/aspnet:7.0 AS base
WORKDIR /app
EXPOSE 80
EXPOSE 443

FROM base AS final
WORKDIR /app
COPY /MyAppName/bin/Release/net7.0/publish .

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

The above file will do the following:

  • Start with the mcr.microsoft.com/dotnet/aspnet:7.0 Docker image.
  • Change the working directory to /app.
  • Expose ports 80 and 443.
  • Copy the application files into the Docker image /app directory.
  • Set the entry point to be dotnet myappname.dll, which will start the application.

When this Dockerfile is used, a new Docker image will be produced containing the ASP.NET web app.

Running it all on GitHub Actions

The steps outlined above can all be executed on GitHub Actions to firstly building your ASP.NET web app, and then create a multi-platform Docker image to make it easy to distribute and run.

The following steps can be used to create a Github Actions job to build the web application, copy it into the ASP.NET runtime container, and then push that container image to a Docker registry with support for multiple platforms.

The first step is to build the ASP.NET web application, which can be completed with the following steps.

- uses: actions/checkout@v3

- name: Setup .NET
  uses: actions/setup-dotnet@v3
  with:
    dotnet-version: 7.0
    
- name: Setup QEMU
  uses: docker/setup-qemu-action@v2.1.0
  
- name: Setup Docker Buildx
  uses: docker/setup-buildx-action@v2.4.1
  
- name: Restore Project
  run: dotnet restore MyAppName / MyAppName.csproj
  
- name: Build Project
  run: dotnet build MyAppName / MyAppName.csproj -c Release --no-restore
  
- name: Publish Project
  run: dotnet publish MyAppName / MyAppName.csproj -c Release --no-build

The steps above will checkout your GitHub repository, setup the .NET SDK, setup QEMU (required for multiplatform Docker builds), Setup Docker Buildx, restore your ASP.NET web application packages, build the project, and then publish the project ready to be deployed.

The following step will login to the appropriate Docker registry which the created Docker image will be pushed to.

- name: Login to Docker Registry
  uses: docker/login-action@v2.1.0
  with:
    registry: ${{ secrets.DOCKER_REGISTRY_URL }}
    username: ${{ secrets.DOCKER_REGISTRY_USERNAME }}
    password: ${{ secrets.DOCKER_REGISTRY_PASSWORD }}

You will need to setup secrets used as needed in the GitHub Repository settings.

The final step is to build and publish the Docker image.

- name: Build and Push Docker Image
  uses: docker/build-push-action@v4.0.0
  with:
    context: .
    file: MyAppName/Dockerfile
    push: true
    platforms: linux/amd64,linux/arm/v7
    tags: myuser/myappname:latest

This will build the Docker image for the linux/amd64 and linux/arm/v7 platforms. With this approach, you can add any platform supported by the mcr.microsoft.com/dotnet/aspnet:7.0 base image.

You will need to amend the tags and other settings as appropriate for your project. This just always uses the myuser/myappname:latest tag as an example.

The docker/build-push-action@v4.0.0action will also push the newly created Docker image to the specified Docker registry. As a result, it should be available and ready to use as soon as the GitHub action job is completed.

Summary

The removal of QEMU support for .NET 7 initially broke my GitHub Actions Docker image build workflow. This is due to the template Dockerfile build process attempting to build the ASP.NET web app for each platform using Buildx which uses QEMU.

The solution amends the process so that the build occurs once, and then the cross-platform application binaries are copied into the ASP.NET runtime Docker image for each platform.