Runar Ovesen Hjerpbakk

Software Philosopher

Build and test ASP.Net Core app in Docker via GitHub Actions

After my last post on building and testing an ASP.Net Core application using GitHub Actions, I received a question on Reddit whether it would be easier to do it all in Docker.

GitHub actions still have a couple of limitations when it comes to Docker:

  • Docker is not available on macOS virtual hosts
  • Docker is set to Windows-containers on Windows virtual hosts

Thus, a complete recreation of the previous build which built and tested on all three OSes is infeasible using Docker. But it works in Ubuntu and Windows and this post is how you do it.

Example repository

The strategy is building and testing in Docker while building a Docker image with the app and storing the needed Docker-command using scripts making it easy to do the same locally. I’ve created a template repository on GitHub showcasing an ASP.Net Core application, being built and tested with and without Docker.


The important file structure is as follows:

aspnet-core-github-actions-demo
.
├── .github
│   └── workflows
│       ├── aspnetcore-docker.yml
│       └── aspnetcore.yml
├── Dockerfile
├── Dockerfile.windows
├── build.ps1
├── build.sh
└── src
    ├── GitHubActionsDemo.sln
    ├── Tests
    │   └── Tests.csproj
    └── WebApi
        └── WebApi.csproj

GitHub Actions files can be found in .github/workflows. aspnetcore-docker.yml contains the build using Docker, aspnetcore.yml is the regular build.

Dockerfile is the Dockerfile to be used where Linux-containers are available, while Dockerfile.windows is for Windows-containers. build.ps is PowerShell-script used to build the Docker image on Windows and build.sh is a bash-script for the same on Ubuntu or macOS.

The code itself lies under src and this is just the default ASP.Net Core API-template with tests added.

Building and testing using Docker on Linux and macOS

Dockerfile reads as follows:

FROM mcr.microsoft.com/dotnet/core/sdk:3.0-buster AS builder
WORKDIR /source

COPY ./src/GitHubActionsDemo.sln .
COPY ./src/WebApi/WebApi.csproj ./WebApi/
COPY ./src/Tests/Tests.csproj ./Tests/

RUN dotnet restore

COPY ./src/WebApi ./WebApi
COPY ./src/Tests ./Tests

RUN dotnet test ./GitHubActionsDemo.sln --configuration Release --no-restore

RUN dotnet publish "./WebApi/WebApi.csproj" --output "../dist" --configuration Release --no-restore

FROM mcr.microsoft.com/dotnet/core/aspnet:3.0-buster-slim
WORKDIR /app
COPY --from=builder /dist .
EXPOSE 80 443
ENTRYPOINT ["dotnet", "WebApi.dll"]

Just regular multi-stage Dockerfile, running restore, test and publish in different steps to utilize the cache. build.sh contains the needed to command to build a Docker image:

#!/usr/bin/env bash
set -e
docker build -t hjerpbakk/github-actions-demo .

set -e exits the script immediately if a command exits with a non-zero status. This is technically not needed here since the script consists of only one command, but I leave it in since it’s a useful construct to know. docker build -t hjerpbakk/github-actions-demo . builds a Docker image with tag hjerpbakk/github-actions-demo using the default Dockerfile from the current folder.

Building and testing using Docker on Windows

The Windows-container variant is the same, just with different base images in Dockerfile.windows:

FROM mcr.microsoft.com/dotnet/core/sdk:3.0 AS builder
...
FROM mcr.microsoft.com/dotnet/core/aspnet:3.0
...

And similarly, a PowerShell script, build.ps1, instead of bash:

$tagName = "hjerpbakk/github-actions-demo"
$dockerfile = "Dockerfile.windows"
docker build -t $tagName -f $dockerfile .

Building a Docker image with tag hjerpbakk/github-actions-demo using the named Dockerfile.windows from the current folder.

Surprisingly, writing this tiny script was the most difficult part for me. In my childish enthusiasm, I thought it would be so easy as:

docker build -t hjerpbakk/github-actions-demo -f Dockerfile.windows .

But the PowerShell gods just gave me back the error:

unknown shorthand flag: 't' in -t

Turns out PowerShell does not enjoy - in paths and temporarily storing the paths in variables cured the confusion.

Build and test ASP.Net Core with Docker using GitHub Actions

Finally, we’re ready for GitHub actions. aspnetcore.yml you know from the last post, aspnetcore-docker.yml is the interesting one:

name: ASP.NET Core CI using Docker

on: [push]

jobs:
  build:
    runs-on: $
    strategy:
      matrix:
        os: [ubuntu-16.04, windows-latest]
    steps:
    - uses: actions/checkout@v1
    - name: Build and test using Docker on Ubuntu
      run: |
        chmod +x build.sh
        ./build.sh
      shell: bash
      if: matrix.os == 'ubuntu-16.04'
    - name: Build and test using Docker on Windows
      run: ./build.ps1
      shell: powershell
      if: matrix.os == 'windows-latest'

This workflow checks out the code and runs the correct build script for the given virtual host. If the script succeeds and exits with code 0, the build is green. Ubuntu uses the bash-script and Windows the PowerShell variant. Ubuntu must run an extra command before the script, making the build.sh executable using chmod +x build.sh.

The attentive reader will notice one thing, however. I use a specific version of Ubuntu, 16.04, instead of ubuntu-latest. That’s because the latest Ubuntu at the time of writing gives and error:

Step 6/15 : RUN dotnet restore
 ---> Running in f7dfcfad81cc
  Restore completed in 123.51 ms for /source/WebApi/WebApi.csproj.
  Restore completed in 3.28 sec for /source/Tests/Tests.csproj.
MSBUILD : error MSB1025: An internal failure occurred while running MSBuild.
Microsoft.Build.BackEnd.NodeFailedToLaunchException: The FileName property should not be a directory unless UseShellExecute is set.
 ---> System.ComponentModel.Win32Exception (0x80004005): The FileName property should not be a directory unless UseShellExecute is set.
   at System.Diagnostics.Process.StartCore(ProcessStartInfo startInfo)
...

The bug is in progress of being fixed by the Ubuntu team, but as of now, this is how it is.

Final thoughts

Building and testing an ASP.Net Core 3.0 application in Docker using GitHub Actions is indeed possible, but I would still use the regular method too for a couple of reasons:

  • Non-Docker builds are significantly faster. For this albeit empty project, a regular macOS build clocks in at 25 seconds, Ubuntu is a tiny bit slower and Windows dead last with an average of almost 2 minutes. For the Docker-build, Windows is unusably slow with its 4,5 minutes, while Ubuntu is a little more usable finishing in just over 1 minute.
  • Docker-builds still does not have the option of building on macOS.

Build with Docker if you use Docker to test and verify your image but use the faster bare-ish metal approach for your CI-builds.