Using GitHub Actions to build ARM-based Docker Images

Kevin Mansel
The Startup
Published in
6 min readApr 15, 2020
Using GitHub Actions to build ARM-based Docker Images

Hi everyone. I tend to play around with my Raspberry PI from time to time, and recently I set off on a task to build out an ARM-based Docker Image that I could run on my PI for an IoT project. I figured hey, it would be great to have a nice CD process to build the image once I merged my code into my master branch on GitHub. Well hello, GitHub Actions.

https://github.com/features/actions

Actions are GitHub’s answer to the CI/CD software workflow process. Actions can do a ton of things, and there is a great marketplace that already has quite a few actions to choose from. So let’s get started.

There are a few things we’re going to need:

  • A GitHub account
  • A container registry to push our images to
  • An IDE to write some code

I personally am going to use Azure Container Registry for this example, and I use VSCode as my IDE.

Let’s make a quick Node application that will run a webserver and say hello. This is a quick and easy express server. Just going to output a general message and log to the console the requesting IP address.

app.js

‘use strict’; const express = require(‘express’); const PORT = 8080;
const HOST = ‘0.0.0.0’;
const app = express(); app.get(‘/’, (req, res) => {
res.send(`Hello Aubrey and Adalynn!<br><br>I see you’re from: ${req.ip}`);
console.log(`Request incoming from: ${req.ip}`);
});
app.listen(PORT, HOST); console.log(`Server running on http://${HOST}:${PORT}`);

Now it’s time to make our Dockerfile. Notice that we want to build this image from the ARM version for node.

Dockerfile

FROM arm32v7/node WORKDIR /usr/src/app COPY package*.json ./ RUN npm install COPY . . EXPOSE 8080CMD ["node", "app.js"]

So we now have our Dockerfile and our node application that we want to package up into an image. The last piece we need to write up is our GitHub Action. When you’re structuring your repository, the GitHub Actions need to be put into a specific directory .github/workflows/

All workflows for GitHub need to be in the .github/workflows/ directory

You can name your workflow whatever you’d like, here you can see that I have labeled mine dockerimage.yml

dockerimage.yml

name: Docker Build/Publish Image on:  
push:
branches: [ master ]
jobs:
build:
runs-on: ubuntu-18.04
env:
DOCKER_REGISTRY: kemansel.azurecr.io
DOCKER_IMAGE: kmansel/express-me
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
DOCKER_TARGET_PLATFORM: linux/arm/v7
steps:
- name: Checkout the code
uses: actions/checkout@v1
- name: Set up Docker Buildx
uses: crazy-max/ghaction-docker-buildx@v1
with:
version: latest
- name: Prepare
if: success()
id: prepare
run: |
echo ::set-output name=docker_platform::${DOCKER_TARGET_PLATFORM}
echo ::set-output name=docker_image::${DOCKER_REGISTRY}/${DOCKER_IMAGE}
echo ::set-output name=version::${GITHUB_RUN_NUMBER}
- name: Docker Login
if: success()
run: |
echo "${DOCKER_PASSWORD}" | docker login ${DOCKER_REGISTRY} --username "${DOCKER_USERNAME}" --password-stdin
- name: Run Buildx (push image)
if: success()
run: |
docker buildx build \ --platform ${{ steps.prepare.outputs.docker_platform }} \ --tag ${{ steps.prepare.outputs.docker_image }}:${{ steps.prepare.outputs.version }} \ --file ./Dockerfile \ --output type=image,push=true .

There is a lot going on here. So let's break it down a bit.

Here we are saying, please run this workflow when we push code to our master branch. Since you’re an amazing developer and you’re following best practices, you only push to master once your CI process and testing have passed…so for this action, we want to fire off the building of our docker image when new code is pushed to our master branch.

on:  
push:
branches: [ master ]

In this section, we are setting up some environment variables to use through the rest of the Action. As you can see, I’ve got my registry and image name I want to use. I’m importing my username and password from my secrets vault within GitHub. You can find that in the “Settings” tab under the “Secrets” blade. I’m also setting the target platform I want my image to build which is linux/arm/v7

jobs:   
build:
runs-on: ubuntu-18.04
env:
DOCKER_REGISTRY: kemansel.azurecr.io
DOCKER_IMAGE: kmansel/express-me
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
DOCKER_TARGET_PLATFORM: linux/arm/v7
You can set secrets through GitHub in your vault. Follow “Settings” -> “Secrets”

The first step that we want to run is to check out the code from the repo, this will bring our codebase into our build environment for GitHub Actions.

- name: Checkout the code       
uses: actions/checkout@v1

We now need to pull in an Action that will give us the ability to run the dockerx command. This is the command that will allow us to build our ARM-based image. Thanks to GitHub user crazy-max for building this action and sharing it with the world: https://github.com/crazy-max/ghaction-docker-buildx

- name: Set up Docker Buildx      
uses: crazy-max/ghaction-docker-buildx@v1
with:
version: latest

Here we are preparing some local variables for our docker commands to use. You’ll also notice the if: success() command here. This will make sure the previous step has completed successfully before moving to this step. You can see here we are referencing some of the environment variables we set at the beginning of the Action. You’ll notice I’m setting my version variable to ${GITHUB_RUN_NUMBER} This is a variable that is available to us as a default environment variable by GitHub Actions. You can read more about default environment variables that are available here: https://help.github.com/en/actions/configuring-and-managing-workflows/using-environment-variables

- name: Prepare      
if: success()
id: prepare
run: |
echo ::set-output name=docker_platform::${DOCKER_TARGET_PLATFORM}
echo ::set-output name=docker_image::${DOCKER_REGISTRY}/${DOCKER_IMAGE}
echo ::set-output name=version::${GITHUB_RUN_NUMBER}

Our final two steps are to login to your registry using the docker login command and then build/push our desired image to this registry. You can see that we are using our variables from our prepare step in the docker buildx command.

- name: Docker Login      
if: success()
run: |
echo "${DOCKER_PASSWORD}" | docker login ${DOCKER_REGISTRY} --username "${DOCKER_USERNAME}" --password-stdin
- name: Run Buildx (push image)
if: success()
run: |
docker buildx build \
--platform ${{ steps.prepare.outputs.docker_platform }} \
--tag ${{ steps.prepare.outputs.docker_image }}:${{ steps.prepare.outputs.version }} \
--file ./Dockerfile \
--output type=image,push=true .

You should be able to see your action progress through the steps live on GitHub. As you can see in the image, I’ve scrolled to the part where the output logs are showing that we have in fact pushed the docker image to the repository.

Viewing a GitHub Actions workflow live!

Ok, we see that it’s been pushed to the registry here, but I need proof. Let's head over to the container registry and see if we’ve got an image up there.

Look at that…an ARM-based docker image. :)

Awesome! So now you know that it’s possible to build ARM-based docker images through the GitHub workflow engine. This should get you on your way to a solid CD process for deploying containers to Raspberry PI ARM-based platforms. I personally used this pattern to push my images and pull them down to a Raspberry PI via Azure IoT Hub service.

Hope this helps you in your development journey.

Thanks for reading!

--

--