Building for Multiple Architectures with Docker
Find out how we use Docker to build and distribute our software for multiple architectures.
Introduction
As the ARM architecture continues to pick up pace in both the desktop and server spaces, we thought it’d be a good idea to make sure that SMBeagle, our open source SMB share auditing tool, is supported. We’re starting to see more adoption now thanks to devices powered by the Apple M1 chip and by cloud providers such as AWS having ARM solutions.
ARM CPUs tend to be much more efficient than their x86/64 counterparts, drawing less power and producing less heat. More and more organisations are adopting ARM due to lower operational costs, and consumers are benefiting from significantly better battery life.
The transition to ARM has not been without issue: native programs are by nature built to run on a single CPU architecture. As a result, a lot of existing software simply will not run on ARM-based systems without the use of emulation, which is usually slower and less efficient.
Our build process
We use GitHub actions to build SMBeagle because they’re powerful and make it really quick and easy to get started with CI, without the need to set up your own dedicated CI server.
When a pull request is opened, the GitHub action will produce a preview build containing the changes in the pull request. When a commit is tagged, the GitHub action will build SMBeagle for Linux and Windows, sign the Windows binary, and then create a new GitHub release containing these binaries.
Dotnet makes building ARM binaries easy - we just needed to pass the architecture that we wanted:
- name: Build linux ARM
run: dotnet publish -c Release --self-contained -r linux-arm -o packages\linux\arm -p:PublishSingleFile=true -p:PublishTrimmed=true -p:InvariantGlobalization=true -p:DebugType=None -p:DebugSymbols=false
Enter Docker
At punk security, Docker is our bread and butter. We rely on Docker to produce lightweight, reproducible environments. It also makes software distribution and set up a breeze. As long as the end-user has Docker installed, all that they need to do is run two easy-to-remember commands:
docker pull punksecurity/smbeagle:latest
docker run punksecurity/smbeagle
Docker images are architecture-specific, so we’ll need to build two separate images. We know that we can easily build an image for the architecture that we’re currently on (e.g. x64), but how about building images for other architectures?
Introducing Buildx
Buildx is Docker’s solution to this problem. It allows you to build your docker images for multiple architectures without needing to actually build on those architectures. It does this by using QEMU to provide emulation. Building is slower for other architectures, but at the end of it we’ll have images that will run at full speed in production!
The Dockerfile
Our build process is actually quite simple - you can view our full setup on GitHub, but here’s what our Dockerfile looks like:
# This image will build the software. The dotnet SDK contains all of the dependencies we need to build our image.
FROM mcr.microsoft.com/dotnet/sdk:5.0.406-bullseye-slim as builder
COPY ./ /code
WORKDIR /code
RUN dotnet restore
# The TARGETARCH argument is automatically set by buildx
# This allows us to pass the correct architecture to the build tool
ARG TARGETARCH
# Unfortunately, dotnet and docker use different names for the architectures, so we need to make a correction
RUN if [ "$TARGETARCH" = "amd64" ]; \
then export ARCH=x64; \
else export ARCH=$TARGETARCH; \
fi; \
dotnet publish -c Release --self-contained -r "linux-$ARCH" -o "packages/linux/" -p:PublishSingleFile=true -p:PublishTrimmed=true -p:InvariantGlobalization=true -p:DebugType=None -p:DebugSymbols=false
FROM debian
ARG TARGETARCH
# Here we copy the binary from the build step earlier. This means that we don't need to include the entire SDK
# This keeps our final images nice and small
COPY --from=builder ./code/packages/linux/SMBeagle /bin/smbeagle
RUN chmod +x /bin/smbeagle
CMD ["smbeagle"]
Building the images
Now that we have a Dockerfile that supports building and running on multiple architectures, let’s actually build the images! This is as simple as running the buildx command with the architectures that we desire:
docker buildx build . \
--push \
--tag docker.io/punksecurity/smbeagle:version-name \
--platform linux/amd64,linux/arm64
Build the image with the current directory as the context (equivalent to ‘docker build .’):
docker buildx build . \
Tell docker to push the image to the registry (e.g. dockerhub):
--push
Specify the repository and tag to use for the image:
--tag docker.io/punksecurity/smbeagle:latest
The magic - build for the linux amd64 and arm64 platforms:
--platform linux/amd64,linux/arm64
It can take a while to build, but when complete we end up with both amd64 and arm4 variants of the same image.
You can find the full code on the SMBeagle GitHub repository