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: https://www.docker.com/increase-rate-limit.\nSee 'docker run --help'.\n" |
Unable to find image 'hello... You have reached your pull rate limit. You may increase the limit by authenticating and upgrading: https://www.docker.com/increase-rate-limit.\nSee 'docker run --help'.\n"
or this
ERROR: toomanyrequests: Too Many Requests. |
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 hub.docker.com to a place at github.com/graalvm/container/pkgs/container/graalvm-ce. The resulting image has the coordinates ghcr.io/graalvm/graalvm-ce
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 ghcr.io/jonashackt/hello-world |
docker run ghcr.io/jonashackt/hello-world
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.")
} |
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
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 |
$ 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 |
# 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. |
$ 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
:

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:

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:

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:

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]
jobs:
publish-hello-world-image:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Build the hello-world Docker image
run: |
echo $CR_PAT | docker login ghcr.io -u YourAccountOrGHOrgaNameHere --password-stdin
env:
CR_PAT: ${{ secrets.CR_PAT }} |
name: publish on: [push] jobs: publish-hello-world-image: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Build the hello-world Docker image run: | echo $CR_PAT | docker login ghcr.io -u YourAccountOrGHOrgaNameHere --password-stdin env: 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 ghcr.io/OWNER/IMAGE_NAME:latest
. Inside the example project’s .github/workflow/publish.yml it looks like this:
docker build . --tag ghcr.io/jonashackt/hello-world:latest
docker run ghcr.io/jonashackt/hello-world:latest
docker push ghcr.io/jonashackt/hello-world:latest |
docker build . --tag ghcr.io/jonashackt/hello-world:latest docker run ghcr.io/jonashackt/hello-world:latest docker push ghcr.io/jonashackt/hello-world:latest
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 README.md
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="https://github.com/jonashackt/docker-hello-world" |
LABEL org.opencontainers.image.source="https://github.com/jonashackt/docker-hello-world"
Having added this label to our Dockerfile
, the image package automatically gets linked to our repository:

Also, the image becomes visible on our repository’s 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 https://github.com/jonashackt?tab=packages:

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
:

Now we should finally be able to pull and run our image! Simply run docker run ghcr.io/jonashackt/hello-world
:
$ docker run ghcr.io/jonashackt/hello-world
Unable to find image 'ghcr.io/jonashackt/hello-world:latest' locally
latest: Pulling from jonashackt/hello-world
ba3557a56b15: Already exists
8d624a13b642: Pull complete
Digest: sha256:c88996d21c33ed08a76decc2b53e109bbc601d0fa1e444f24682250c5d406aa1
Status: Downloaded newer image for ghcr.io/jonashackt/hello-world:latest
Hello from Docker on GitHub Container Registry!
This message shows that your installation appears to be working correctly. |
$ docker run ghcr.io/jonashackt/hello-world Unable to find image 'ghcr.io/jonashackt/hello-world:latest' locally latest: Pulling from jonashackt/hello-world ba3557a56b15: Already exists 8d624a13b642: Pull complete Digest: sha256:c88996d21c33ed08a76decc2b53e109bbc601d0fa1e444f24682250c5d406aa1 Status: Downloaded newer image for ghcr.io/jonashackt/hello-world:latest 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 …