Publishing Docker images to GitHub Container Registry with GitHub Actions

No Comments

Tired of bumping into Docker Hub’s rate limiting? Why not give the GitHub Container Registry a try? Right now it’s in public beta but it already looks great, especially in combination with GitHub Actions.

GitHub Actions – blog series

Part 1: GitHub Actions CI pipeline: GitHub Packages, Codecov, release to Maven Central & GitHub
Part 2: Publishing Docker images to GitHub Container Registry with GitHub Actions
Part 3: Stop re-writing pipelines! Why GitHub Actions drive the future of CI/CD

What’s the problem with Docker Hub?

Recently I’ve been running into Docker Hub’s new rate limiting more and more often. So regardless which CI system I use, I find myself looking into a log file at something like this:

Unable to find image 'hello...
You have reached your pull rate limit. You may increase the limit by authenticating and upgrading:\nSee 'docker run --help'.\n"

or this

ERROR: toomanyrequests: Too Many Requests.

So I thought of using an alternative container registry that doesn’t expose such a limit. Coincidentally I was updating my example project, which shows how to use GraalVM with Spring. I learned that Oracle moved their GraalVM Docker image from oracle/graalvm-ce (already producing a 404) from to a place at The resulting image has the coordinates and is served by the GitHub Container Registry. Wow, I didn’t even know that GitHub had a registry! But in late 2020 they introduced it as part of their GitHub Packages offering (if you want to see the “normal” Packages in action, you can take a sneak peek here where I show how to publish Maven artifacts).

Logo sources: GitHub & GitHub Actions & GitHub Packages logo, Docker logo

Soon I realized that more people started to migrate their Docker images to the new GitHub Container Registry:

GitHub Container Registry is currently in public beta and subject to change. During the beta, storage and bandwidth are free. To use GitHub Container Registry, you must enable the feature preview.

The GitHub Container Registry will ultimately supersede the already existing Packages Docker Registry.

All I wanted was: docker run hello-world

I hadn’t touched my beloved molecule showcase project for a while (wow, my blog about Continuous cloud infrastructure with Ansible, Molecule & TravisCI on AWS is already older than two years). So I finally needed to upgrade to the latest molecule module system with separate drivers not being shipped inside the core anymore. The upgrade really went super smoothly. Right until I hit the Docker Hub rate limiting problem in the final testinfra test case. 🙁 It was really all about a failing docker run hello-world!! What the hell?!

So I thought about publishing my own hello-world image to the GitHub Container Registry. Maybe even others would bump into the same problem. And this would give me the chance to get to know this new registry. 🙂 In the end all I wanted was something I could simply use like this:

docker run

A simple Go-based executable

The original hello-world image from Docker Hub uses a small executable to print a text. I decided to leverage Go in order to create a reasonably small executable myself. Every piece of code I use throughout this post is also available on GitHub.

So let’s start with a ultra simple hello-world.go:

package main
import "fmt"
func main() {
	fmt.Println("Hello from Docker on GitHub Container Registry!\nThis message shows that your installation appears to be working correctly.")

Building and running a Go program is easy. You only need to have the Go compiler installed on your machine. On my Mac I used homebrew and did a brew install go. Now we can build the program with

go build hello-world.go

This will produce a hello-world executable that we can run with ./hello world.

A multi-stage build for our GO program

As we only need to have the Go compiler present to build the binary, we should implement a Docker multi-stage build. The official Go image is quite huge:

$ docker images
golang     latest     861b1afd1d13   7 days ago     862MB

Therefore we should leverage a multi-stage build inside of our Dockerfile:

# We need a golang build environment first
FROM golang:1.16.0-alpine3.13
WORKDIR /go/src/app
ADD hello-world.go /go/src/app
RUN go build hello-world.go
# We use a Docker multi-stage build here in order that we only take the compiled go executable
FROM alpine:3.13
COPY --from=0 "/go/src/app/hello-world" hello-world
ENTRYPOINT ./hello-world

The second “run” image is based on the same alpine image as the builder image containing the Go compiler. So let’s now simply build and run our image:

$ docker build . --tag hello-world
$ docker run hello-world
Hello from Docker on GitHub Container Registry!
This message shows that your installation appears to be working correctly.

The resulting image is around 7.55MB which should be small enough for our use cases.

List of steps how to publish to GitHub Container Registry with GitHub Actions

In order to publish a container image on GitHub Container Registry using GitHub Actions, we have to do the following steps:

1. Activate improved container support
2. Create a personal access token (PAT) and a repository secret
3. Create GitHub Actions workflow and login to GitHub Container Registry using the PAT
4. Publish (push) Container image to GitHub Container Registry & link it to our repository
5. Optional: Make your image publicly accessible

1. Activating improved container support

This step is only needed while the GitHub Container Registry is in beta phase. In order to use the new Container Registry feature, we need to activate it in our account first. Therefore head to your GitHub account’s settings menu und click on Feature preview:

Feature preview GitHub profile settings

In my account there was only one feature I could choose from: the improved container support feature we need to enable to have GitHub Container Registry ready:

Activate improved container support

2. Creating a personal access token (PAT) and a repository secret

Right now (in beta) we can’t use the GITHUB_TOKEN in GitHub Actions to authenticate to the GitHub Container Registry. So we need to create a personal access token (PAT). But keep in mind what the docs state:

PATs can grant broad access to your account. We recommend selecting only the necessary read, write, or delete package scope when creating a PAT to authenticate to the container registry.

Create a PAT in Settings/Developer settings Personal access tokens and click on Generate new token. Here you need to select read:packages, write:packages and delete:packages scopes like this:

Create a personal access token in GitHub

Having created the PAT, we can move on to create a new repository secret inside our GitHub repository that contains our Go program and Dockerfile. To create a repository secret, head to your repository’s settings tab and click on Secrets. There you should be able to create a new repository secret:

Create a new repository secret containing the PAT

3. Creating GitHub Actions workflow and logging into GitHub Container Registry using the PAT

With both PAT and repository secret set up, we can now create a new (or choose an existing) GitHub Actions workflow. After the usual checkout action, we should set our secret as an environment variable. The example project’s workflow file .github/workflow/publish.yml looks like this:

name: publish

on: [push]

    runs-on: ubuntu-latest

    - uses: actions/checkout@v2

    - name: Build the hello-world Docker image
      run: |
        echo $CR_PAT | docker login -u YourAccountOrGHOrgaNameHere --password-stdin
        CR_PAT: ${{ secrets.CR_PAT }}

Be sure to use your account or GitHub organization name instead of YourAccountOrGHOrgaNameHere. This should successfully do the login to the GitHub Container Registry.

4. Publishing (Pushing) Container image to GitHub Container Registry & linking it to our repository

So we’re already approaching the final steps. Now we should have everything in place to push our container image to the GitHub Container Registry. Therefore we need to tag our image correctly while building it. The tag’s pattern must adhere to Inside the example project’s .github/workflow/publish.yml it looks like this:

        docker build . --tag
        docker run
        docker push

This is already enough to push our image!

As the image is published as a GitHub account global package, it isn’t linked with our repository and thus won’t be displayed there, nor will it use the as description. In order to automatically link our image to our GitHub repository, we need to add a specific LABEL into our Dockerfile. It should contain the URL to our repository like this:

LABEL org.opencontainers.image.source=""

Having added this label to our Dockerfile, the image package automatically gets linked to our repository:

The image gets linked to the repository using the LABEL

Also, the image becomes visible on our repository’s main page:

The image gets visible on the repositories main page

5. Optional: Making your image publicly accessible

By default , our container image is private on the GitHub Container Registry. To make it publicly accessible, we need to move to our user account or GitHub organization page. For my account, this is

images are shown as packages on the account level

Click on the container image published (which looks the same as a normal GitHub package) and then go to Package Settings. In the Danger Zone, click on change visibility and choose Public:

package settings for public visibility

Now we should finally be able to pull and run our image! Simply run docker run

$ docker run
Unable to find image '' locally
latest: Pulling from jonashackt/hello-world
ba3557a56b15: Already exists
8d624a13b642: Pull complete
Digest: sha256:c88996d21c33ed08a76decc2b53e109bbc601d0fa1e444f24682250c5d406aa1
Status: Downloaded newer image for
Hello from Docker on GitHub Container Registry!
This message shows that your installation appears to be working correctly.

Give the GitHub Container Registry a try!

I didn’t really want to create a hello-world image myself in the first place. But hey! Working with the GitHub Container Registry is fun and you should consider it for your next project. In this article we have seen how to create a small example program and build it using a Docker multi-stage build. The activation of the new feature and the creation of a personal access token (PAT) are the prerequisites to using the Container Registry right now in beta phase. I guess both won’t be necessary anymore soon.

Using GitHub Actions, we only need to log in using our PAT which we can store in a repository secret. The LABEL org.opencontainers.image.source links our image to our repository after we pushed it using a GitHub Actions workflow of choice. Finally we define our image as publicly accessible if we want it to be. That’s it already! Now it’s time to move your images from Docker Hub to GitHub Container Registry I guess. 🙂 And if you ever need a hello-world image, you know where to find it …

After falling in love with Spring, Jonas also developed an interest in all container- and infrastructure-related topics. Now he focuses on bringing methods like Test-driven Development and Continuous Integration into the world of infrastructure code. He founded the codecentric branch in Erfurt/Thuringia and is involved in the local community, organizing the Java User Group Thüringen, DevOps Thüringen, and IoT Thüringen meetups. He loves to write blog posts & give lectures at Thuringian universities. Spare time is reserved for his family and mountain biking.


Your email address will not be published.