Oskar Dudycz

Pragmatycznie o programowaniu

How to build and push Docker image with GitHub actions?

2021-08-11 oskar dudyczDevOps

cover

In the previous post, I explained that with a few simple tricks, you can make your Docker image less cluttered and build faster. I explained practical patterns on how to do that.

This time I’ll take a step forward and explain how to publish the image to the Docker registry. It’s a place in which you can store your Docker images. You can use them to share images with your team or deploy them to your hosting environment (e.g. Kubernetes, or another container hosting). We’ll use GitHub Actions as an example. It has a few benefits:

  • it’s popular and easy to set up,
  • it’s free for Open Source projects,
  • by design, it integrates easily with GitHub tools like GitHub Container Registry.

It’s a quickly evolving, decent tool. Of course, it’s not perfect. Documentation is not great, but that’s also the reason why I’m writing this post.

We’ll use two popular Docker registries:

  • Docker Hub: the default one, provided by Docker. It’s commonly used for the public available images. If you run docker pull, it’ll try to load the image from it by default. However, from November 2020, it has significant limits for free accounts.
  • GitHub Container Registry (GHCR): GitHub introduced its container registry as a Packages service spin-off (you can use it to host artefacts like NPM, NuGet packages, etc.). It allows both public and private hosting (which is crucial for commercial projects).

Before we push images, we need to do a basic setup for the container registry:

Docker Hub publishing setup

  1. Create an account and sign in to Docker Hub.
  2. Go to Account Settings => Security: link and click New Access Token.
  3. Provide the name of your access token, save it and copy the value (you won’t be able to see it again, you’ll need to regenerate it).
  4. Go to your GitHub secrets settings (Settings => Secrets, url https://github.com/{your_username}/{your_repository_name}/settings/secrets/actions).
  5. Create two secrets (they won’t be visible for other users and will be used in the non-forked builds)
  • DOCKERHUB_USERNAME - with the name of your Docker Hub account (do not mistake it with GitHub account)
  • DOCKERHUB_TOKEN - with the pasted value of a token generated in point 3.

Github Container Registry publishing setup

  1. Enable GitHub Container Registry. Profile => Feature Preview => Improved Container Support => Enable.
  2. Create GitHub Personal Access Token in your profile developer settings page. Copy the value (you won’t be able to see it again, you’ll need to regenerate it). Select at least following scopes:
  • repo
  • read:packages
  • write:packages
  1. Go to your GitHub secrets settings (Settings => Secrets, url https://github.com/{your_username}/{your_repository_name}/settings/secrets/actions).
  2. Navigate to your package landing page https://github.com/{your_username}/{your_repository_name}/pkgs/container/{your_package_name}. Grant GitHub action write access (more in GitHub Registry docs). By that, as long as user running action has proper permissions (we can also setup them to automatically derive them from repository config) we can use default GITHUB_TOKEN secret. Thanks, @Brickwall, for pointing that out in the comment!

Once we have Docker registries setup, we can create a workflow file. It should be located in the ..github\workflows directory in our repository. Let’s name it build-and-publish.yml.

We’ll run this pipeline when Pull Request is created and on the main branch. We’ll be pushing the Docker image only on the main branch because we don’t want to spam the registry with intermediate images. If you want to, e.g. run manual tests for the pull request branch - you may also consider publishing also prerelease packages.

The process will look as follows:

  1. Use working directory where Dockerfile is located (e.g. src)
  2. Checkout code.
  3. Log in to DockerHub and GHCR using credentials set up in the previous step.
  4. Build Docker image.
  5. Publish Docker image if the pipeline is running on the main branch.

The pipeline has to be run on the Linux machine, as Windows and macOS lack Docker configuration.

The resulting file will look as follow:

name: Build and Publish

on:
  # run it on push to the default repository branch
  push:
    branches: [main]
  # run it during pull request
  pull_request:

jobs:
  # define job to build and publish docker image
  build-and-push-docker-image:
    name: Build Docker image and push to repositories
    # run only when code is compiling and tests are passing
    runs-on: ubuntu-latest

    # steps to perform in job
    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      # setup Docker buld action
      - name: Set up Docker Buildx
        id: buildx
        uses: docker/setup-buildx-action@v2

      - name: Login to DockerHub
        uses: docker/login-action@v2
        with:
          username: ${{ secrets.DOCKERHUB_USERNAME }}
          password: ${{ secrets.DOCKERHUB_TOKEN }}

      - name: Login to Github Packages
        uses: docker/login-action@v2
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
      
      - name: Build image and push to Docker Hub and GitHub Container Registry
        uses: docker/build-push-action@v2
        with:
          # relative path to the place where source code with Dockerfile is located
          context: ./src/samples/simple
          # Note: tags has to be all lower-case
          tags: |
            oskardudycz/eventsourcing.nodejs.simple:latest 
            ghcr.io/oskardudycz/eventsourcing.nodejs/simple:latest
          # build on feature branches, push only on main branch
          push: ${{ github.ref == 'refs/heads/main' }}

      - name: Image digest
        run: echo ${{ steps.docker_build.outputs.digest }}

As you see the pipeline is technology agnostic. You can reuse it in whatever technology you choose. This gives a lot of possibilities to simplify pipelines and processing.

Read also other articles around DevOps process:

Cheers!

Oskar

👋 If you found this article helpful and want to get notification about the next one, subscribe to Architecture Weekly.

✉️ Join over 6500 subscribers, get the best resources to boost your skills, and stay updated with Software Architecture trends!

Loading...
Event-Driven by Oskar Dudycz
Oskar Dudycz For over 15 years, I have been creating IT systems close to the business. I started my career when StackOverflow didn't exist yet. I am a programmer, technical leader, architect. I like to create well-thought-out systems, tools and frameworks that are used in production and make people's lives easier. I believe Event Sourcing, CQRS, and in general, Event-Driven Architectures are a good foundation by which this can be achieved.