Building Multi Platform Docker Images using GitHub Actions

CI DOCKER GITHUB ACTIONS RASPBERRY PI

Trying to setup a service using Docker in a Raspberry Pi, I’ve found myself dealing with astronomical build times (I stoped counting after 20 minutes), many times resulting in errors due to incompatibility or network issues.

This post presents a complete and automated solution for this problem, using GitHub Actions to automatically build multi platform images and pushing them to Docker Hub.

Running Docker on Raspberry Pi

Docker is an excelent tool for creating and managing containers, greatly simplifying the process of setting up a project topology and all its depencies (DB, cache, static hosting, etc…).

In 2016, it was announced at the Raspberry Pi Blog that the new raspbian/jessie version would count with official support for Docker, allowing it to be installed with relatively ease:

$ curl -sSL https://get.docker.com | sh

Unfortunately, due to the reduced processing power of the Raspberry Pi, specially older hardware versions, building a container image is extremely slow.

In one instance, I waited more then 20 minutes for it to finish, just to see it being cancelled due to a temporary network issue with the NPM repositories.

Multi Platform Builds

The solution I found is using the buildx command, which is available via the CLI since version 19.03.

The command allows the creation of builders, capable of building images from Dockerfiles. The command, allied with QEMU, is capable of building multi platform images, including arm/v6 and arm/v7, both supported by the Raspbery Pi.

I tested this method by generating an image and storing it in a .tar file:

# Create Builder
$ docker buildx create --name mybuilder

# Use QEMU to support ARM archtectures
$ docker run --rm --privileged multiarch/qemu-user-static --reset -p yes

# Inspect the builder (the bootstrap flag also initiates its instance)
$ docker buildx inspect --bootstrap

# Build docker image and save it to dist/myproject.tar
$ docker buildx build --platform linux/arm/v6 -t myproject:latest -o type=docker,dest=- . > dist/myproject.tar

After finished, the image can be copied to the Raspbery Pi and loaded using:

$ docker load -i myproject.tar

# Tag the image to make it easier to reference
$ docker image tag <SHA digest> myproject:latest

Automatic Builds using GitHub Actions

The previous procedure is enough to solve the initial problem, but it’s still manual and doesn’t fit a continuously developed project.

A good way to solve this is by automating it and including it as a Continous Integration (CI) routine. GitHub Actions is a very good solution for that, being very easy to configure and integrating seamlessly with the repositories.

GitHub Actions allows us to configure Workflows. These are defined through .yml files and represent a list of actions to be executed sequentially. These files should be located in the .github directory, in the root of the repository:

$ mkdir -p .github/workflows
$ touch .github/workflows/docker.yml

This file is defined as:

name: Build Docker images

on:
  push:
    branches: [ main ]
  workflow_dispatch:

jobs:
  build:
    runs-on: ubuntu-18.04
    steps:
      - name: Checkout Repository
        uses: actions/checkout@v2

      - name: Setup QEMU
        id: qemu
        uses: docker/[email protected]
        with:
          platforms: linux/amd64,linux/arm/v6

      - name: Set up Docker Buildx
        id: buildx
        uses: docker/[email protected]

      - name: Login to Docker Hub
        uses: docker/[email protected]
        with:
          username: ${{ secrets.DOCKER_HUB_USERNAME }}
          password: ${{ secrets.DOCKER_HUB_TOKEN }}

      - name: Build and push
        id: docker_build
        uses: docker/build-push-action@v2
        with:
          platforms: linux/amd64,linux/arm/v6
          push: true
          tags: <repo>/<project>:latest

      - name: Image digest
        run: echo ${{ steps.docker_build.outputs.digest }}
  • The name directive simply gives the workflow a name.

  • on defines the conditions in which the action will be executed:
    • push - Defines that the workflow will be executed every time there’s a push to the main branch (If it’s an older repository, it probably uses the master branch).
    • workflow_dispatch - Allows manual execution of the workflow, in the Actions tab, in the project page.
  • jobs defines the jobs to be executed and it’s parameters:
    • build - is the name of the job being defined.
    • runs-on - defines the distribution used in the virtual machine that will execute the action:
      • I’ve chosen ubuntu-18.04, since it’s a mature and stable version, though I might consider updating it soon.
    • steps - Steps to be executed.
      • name - Step name.
      • id - ID of the step. Allows for it to be referenced in other parts of the config file.
      • uses - Repository where the action is defined.
      • with - Parameters passed to the action.

Actions Description

  • action/checkout - Checkouts the repository into the VM.

  • docker/setup-qemu-action - Setups QEMU:
    • platforms defines the platforms to be configured:
      • In this case, I’ve opted for linux/amd64 and linux/arm/v6.
      • For a complete list of supported platforms, check out the action repository.
  • docker/setup-buildx-action - Setups buildx.

  • docker/login-action - Login on Docker Hub:
    • This action will allow pushing the generated image directly to Docker Hub.
    • Since this config file is commited to the repository, it’s not a good idea to store credentials in plaintext, even if it’s a private repository.
    • To store credentials safely, it’s possible to use GitHub Secrets and reference them through variables prefixed with secrets..
  • build-push-action - Generates the Docker Image:
    • Once again, platforms are specified in platforms.
    • tags specifies which tag is applied to generated images:
      • I’ve chosen only the :latest tag, which is overriden every time a new image is generated.
      • I haven’t explored much yet, but it’s probably possible to use variables to assign the same tag of the commit being processed.
  • The run directive allows for the execution of a shell command on the VM:
    • In this case, the echo prints the hash of the image generated in the previous step. It’s an example on how to use the IDs to refer to previous steps (steps.<step_id>).
  • To discover other available actions, please take a look at the GitHub Marketplace.