I've been developing a service orientated smart home system which consists of a number of containerised workloads running on an edge Kubernetes cluster (via Microk8s), the "cluster" comprises two Raspberry Pi 4b (ARMv8).
As well as running multiple workloads on the Pi 4b I also run workloads on another Raspberry Pi 2b (ARMv7) which is much older (but very power efficient). And finally I also need to run general tests of the workloads on my local Windows development machine prior to deployment to my "Production cluster", and at a later date I may even want to run these workloads on Azure Kubernetes Service.
Although I could achieve my goal of deploying the same application to multiple architectures using separate Dockerfiles (i.e. Dockerfile.amd64, Dockerfile.arm64, etc...) in my view that is messy and makes the CI/CD more complex. I think the single Dockerfile is the elegant approach keeping all build instructions in one place.
This repository provides a fully working example of building a .NET application container image that is capable of targeting multiple platform architectures - all from a single Dockerfile.
If you find this repository useful then give it a ⭐ ... 😉
-
Construct a .NET multi-architecture container image via a single Dockerfile using the
docker buildx
command. -
Create a single GitHub Actions workflow ci.yml to handle all tasks and host the reuseable workflows in an external gha-workflows repository.
- Auto-Semantic Versioning
- Build App
- Build Container + Push To GitHub Packages
- Package Helm Chart + Push To GitHub Packages
- GitHub Release
RID is short for Runtime Identifier, these are the RID's which identify the target platforms I am interested in deploying my .NET application to;
- linux-x64 (Most desktop distributions like CentOS, Debian, Fedora, Ubuntu, and derivatives)
- linux-arm64 (Linux distributions running on 64-bit ARM like Ubuntu Server 64-bit on Raspberry Pi Model 3+)
- linux-arm (Linux distributions running on ARM like Raspbian on Raspberry Pi Model 2+)
- Note: this architecture was not plain sailing, but I used a great solution here.
#Run pre-built image on Docker
docker run --pull always --rm -it ghcr.io/f2calv/multi-arch-container-dotnet
#Run pre-built image on Kubernetes (via Helm)
helm upgrade --install multi-arch-container-dotnet oci://ghcr.io/f2calv/charts/multi-arch-container-dotnet
#helm uninstall multi-arch-container-dotnet
#Run pre-built image on Kubernetes (via kubectl)
kubectl run -i --tty --attach multi-arch-container-dotnet --image=ghcr.io/f2calv/multi-arch-container-dotnet --image-pull-policy='Always'
kubectl logs -f multi-arch-container-dotnet
#kubectl delete po multi-arch-container-dotnet
The .NET workload is an ultra simple worker process (i.e. a console application) which loops outputting a number of environment variables passed in during the CI process and then baked into the container image.
First clone the repository (ideally by opening it as vscode devcontainer) and then via a terminal window from the root of the repository execute;
#demo script PowerShell version
./build.ps1
Or
#demo script Shell version (also below)
. build.sh
#!/bin/sh
#set variables to emulate running in the workflow/pipeline
GIT_REPOSITORY=$(basename `git rev-parse --show-toplevel`)
GIT_BRANCH=$(git branch --show-current)
GIT_COMMIT=$(git rev-parse HEAD)
GIT_TAG="latest-dev"
GITHUB_WORKFLOW="n/a"
GITHUB_RUN_ID=0
GITHUB_RUN_NUMBER=0
IMAGE_NAME="$GIT_REPO:$GIT_TAG"
#Note: you cannot export a buildx container image into a local docker instance with multiple architecture manifests so for local testing you have to select just a single architecture.
#$PLATFORM="linux/amd64,linux/arm64,linux/arm/v7"
PLATFORM="linux/amd64"
#Create a new builder instance
#https://github.com/docker/buildx/blob/master/docs/reference/buildx_create.md
docker buildx create --name multiarchcontainerdotnet --use
#Start a build
#https://github.com/docker/buildx/blob/master/docs/reference/buildx_build.md
docker buildx build \
-t $IMAGE_NAME \
--label "GITHUB_RUN_ID=$GITHUB_RUN_ID" \
--label "IMAGE_NAME=$IMAGE_NAME" \
--build-arg GIT_REPOSITORY=$GIT_REPOSITORY \
--build-arg GIT_BRANCH=$GIT_BRANCH \
--build-arg GIT_COMMIT=$GIT_COMMIT \
--build-arg GIT_TAG=$GIT_TAG \
--build-arg GITHUB_WORKFLOW=$GITHUB_WORKFLOW \
--build-arg GITHUB_RUN_ID=$GITHUB_RUN_ID \
--build-arg GITHUB_RUN_NUMBER=$GITHUB_RUN_NUMBER \
--platform $PLATFORM \
--pull \
-o type=docker \
.
#Preview matching images
#https://docs.docker.com/engine/reference/commandline/images/
docker images $GIT_REPOSITORY
read -p "Hit ENTER to run the '$IMAGE_NAME' image..."
#Run the multi-architecture container image
#https://docs.docker.com/engine/reference/commandline/run/
docker run --rm -it --name $GIT_REPOSITORY $IMAGE_NAME
#userprofile=$(wslpath "$(wslvar USERPROFILE)")
#export KUBECONFIG=$userprofile/.kube/config
#kubectl run -i --tty --attach multi-arch-container-dotnet --image=ghcr.io/f2calv/multi-arch-container-dotnet --image-pull-policy='Always'
-
I highly recommend reading the official Docker blog posts about multi-arch images;
-
Official Docker documentation about support/implementation for multi-arch images;
-
Official Microsoft documentation useful for multi-arch .NET application builds;