Docker Hub was the place to get container images. To a degree, it still is, but support for other public registries has increased and if your software's installation instructions include a Docker Compose file, then having that file pre-populated with images from a third-party registry is trivial.

Docker announced last year that they'd be limiting the pull rate of container images on free accounts. Later, they announced that they would discontinue autobuilds for free accounts. For my FOSS projects, this was a problem. I used autobuilds to create Docker images when I made a release. Since my images can get marked as "unused" and I have to build them with a CI tool anyway, I decided to move to GitHub Container Registry and use GitHub Actions--which was already running my unit tests--to build and push the images.

Goals

The goal of this CI job is to build a container image and push it to GHCR when a release tag is pushed. In my case, I'm building two different images: the application and the nginx frontend.

This should be fairly straight-forward, but there are a number of gotchas which I found and will discuss as they come up.

Preparation

In order to make this work, we'll need to add some secrets to the project: a GitHub username (GHCR_USERNAME) and an access token associated with that username (GHCR_ACCESS_TOKEN). You can use your username or the username of a dedicated account.

The access token will require the following permissions:

"write:packages" will automatically select the others.

Now, under the repository's setting, go to Secrets, and add them in.

We are now ready to move on to creating the actual job.

Creating the Job

In the .github/workflows folder at the root of your repository, create a new file called docker.yml. It can start simply with the following:

name: Build and Push Docker Images
on:
  push:
    tags:
      - v*

This provides the name of the job and tells it to run when tags starting with v are pushed to the repo. Obviously, you can change this to match whatever tag pattern you use for releases.

Now, we'll set up some environment variables. This will make it easy to change things later on should we need to use a different registry or rename our image.

env:
  REGISTRY: ghcr.io
  IMAGE_NAME: seancallaway/subredditlog

NOTE: Here's where I found the first "gotcha": GitHub Container Registry doesn't support uppercase letters in image names. While my project is seancallaway/SubredditLog, I have to lowercase the image name. I believe the difference caused the images to be made private under my account, so I had to manually change their visibility to public and associate them with the SubredditLog repository. Once that was done, they became visible from the project's home page.

Since we've defined when the job will run, it's time to define what will actually happen. The job needs to take the following steps:

  1. Check out the repository
  2. Setup Docker BuildX
  3. Login to the Container Registry
  4. Build and Push the Image
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Check Out Repo
        uses: actions/checkout@v2
      
      - name: Setup BuildX
        uses: docker/setup-buildx-actions@v1

Here, we're telling GitHub Actions to do this job on an ubuntu-latest image. You can probably use something else here, but it seems to be the most common choice in the community, so I selected that to minimize any troubleshooting.

We also make use of reusable actions to check out the repository and setup BuildX.

      - name: Login to Container Registry
        uses: docker/login-action@v1
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ secrets.GHCR_USERNAME }}
          password: ${{ secrets.GHCR_PASSWORD }}

Our next step is logging into the container registry. Again, we make use of an action here, but also pass it arguments: the registry, username, and password. The registry is the environment variable we defined earlier, but the username and password are the repository secrets we created earlier on. These will be masked in the job output, but will work just fine.

      - name: Build and Push Main Image
        id: main_build
        uses: docker/build-push-action@v2
        with:
          context: .
          file: Dockerfile.prod
          tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.event.release.tag_name }}
          push: true

For building and pushing the image (here called "main" because I have multiple), we make use of work Docker has already done for use which utilize BuildX. We only need to pass in a few arguments.

  • context: . - this probably isn't needed, since it's likely the default, but I like being verbose sometimes.
  • file: Dockerfile.prod - as I have a Dockerfile for testing and multistage file for release, I need to specify this. If you're just using Dockerfile, you can skip this part, but again verbosity isn't a bad thing.
  • tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.event.release.tag_name }} - here I specify how I want to tag the image. This is just like how you'd do it on the CLI, specifying the registry, image name, and tag. In this case, it'd be something like ghcr.io/seancallaway/subredditlog:v0.1.3.
  • push: true - push the image to the registry after building. If you have other things to do, you can skip this, but the all-in-one nature of this action is appealing to me.

That's it! Not bad, huh? Add and commit this file to your repository and you're off to the GitHub Container Registry races.

The Whole File

The latest version of the file we discussed can be found here, but I've included the current version below:

name: Build and Push Docker Images
on:
  push:
    tags:
      - v*

env:
  REGISTRY: ghcr.io
  IMAGE_NAME: seancallaway/subredditlog

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Check Out Repo
        uses: actions/checkout@v2

      - name: Setup BuildX
        uses: docker/setup-buildx-action@v1

      - name: Login to Container Registry
        uses: docker/login-action@v1
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ secrets.GHCR_USERNAME }}
          password: ${{ secrets.GHCR_ACCESS_TOKEN }}

      - name: Build and Push Main Image
        id: main_build
        uses: docker/build-push-action@v2
        with:
          context: .
          file: Dockerfile.prod
          tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.event.release.tag_name }}
          push: true

      - name: Build and Push Nginx Image
        id: nginx_build
        uses: docker/build-push-action@v2
        with:
          context: nginx/
          tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-nginx:${{ github.event.release.tag_name }}
          push: true