Using the Github Container Registry
The GitHub Container Registry (GHCR) allows you to host and manage Docker container images in your personal or organisation account on GitHub. One of the benefits is that permissions can be defined for the Docker image, independent of any repository. Thus, your repository could be private and your Docker image public.
This feature is part of Github's Packages feature. To manage your images you need to go to your
personal or organization's page and click on the Packages
tab.
By default, packages are "Private". You can change this default by going to your account or org's settings page, click on the "Packages" option on the side bar. Under "Packages permissions" -> "Package creation", you can change the default visibility.
In general, you can have a Github repo containing the source for a container image, and through the use of Github Actions, you can automatically publish the container to the GHCR. From then on, you can pull the container image using docker engine.
To enable this you need to create a .github/workflows/docker-image.yml
file with the contents:
Initialization
name: Docker Image CI
on:
workflow_dispatch:
push:
branches: [ "prerel", "prerel-*" ]
tags: [ "*" ]
pull_request:
branches: [ "main" ]
These control events on when to run the workflow. See Events that trigger workflows.
For rolling releases I would use:
on:
workflow_dispatch:
push:
branches: [ "main", "prerel", "prerel-*", "bugfix*" ]
schedule:
# Run every 8th of the month
- cron: "0 2 8 * *"
Basic env variables
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
Declares some environmental variables. Here we are indicating that we will be using GHCR. Also, we are using the repository name (owner/repo) for the image name. Be careful that the IMAGE_NAME should be all lowercase.
Job declaration
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
attestations: write
id-token: write
This starts the actual jobs to run. The permissions
block is important as these
are needed to configure the automatic GITHUB_TOKEN
. See
Controlling permissions for GITHUB_TOKEN for details.
Steps
steps:
Workflow requires the following steps:
Run-time additional env variables
- name: Additional environment
run: |
(
echo "IMAGE_NAME=$(echo "${{ env.IMAGE_NAME }}" | tr A-Z a-z)"
echo "PKG_OWNER=$(echo "${{ env.IMAGE_NAME}}" | cut -d/ -f1)"
echo "PKG_NAME=$(basename "${{ env.IMAGE_NAME}}")"
echo "BUILD_DATE=$(date +'%Y%m%d')"
) >> $GITHUB_ENV
This makes sure that the env.IMAGE_NAME
is all lowercase. This is a
docker client limitation. It also splits the image name into
env.PKG_OWNER
and env.PKG_NAME
. The env.PKG_NAME
is needed later by
the actions/delete-package-versions action.
Git source checkout
- name: checkout repository
uses: actions/checkout@v4
This is fairly standard. Check out our source code.
Additional configuration
- name: Read additional environment from file
uses: cosq-network/[email protected]
with:
env-file: dotenv
Read additional environment variables from a file. In this
example, dotenv
.
This is an optional step. I am using this to define certain variables instead of hardcoding them into the Dockerfile.
Log in to GHCR
- name: Log in to the Container registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
This steps logs you into the GHCR so you are able to push images. It uses docker/login-action. It supports to many container repositories such as docker hub. Not just GHCR.
Container metadata
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=schedule
type=ref,event=branch
type=ref,event=tag
type=ref,event=pr
type=raw,value=
Gets meta data using docker/metadata-action
Tags are generated according to:
Event | Ref | Docker Tags |
---|---|---|
pull_request |
refs/pull/2/merge |
pr-2 |
push |
refs/heads/master |
master |
push |
refs/heads/releases/v1 |
releases-v1 |
push tag |
refs/tags/v1.2.3 |
v1.2.3 , latest |
push tag |
refs/tags/v2.0.8-beta.67 |
v2.0.8-beta.67 , latest |
workflow_dispatch |
refs/heads/master |
master |
In my example, I also generate a tag based on the date.
Alternatively, for rolling releae I would use:
tags: |
type=raw,value=latest,enable={{ is_default_branch }}
type=raw,value=
Essentially, we are tagging by date, and the default branch also tags as latest
.
Container build and push
- name: Build and push Docker image
id: push
uses: docker/build-push-action@v3
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
LIBC=${{ env.LIBC }}
PYTHON_VERSION=${{ env.PYTHON_VERSION }}
PYINSTALLER_VERSION=${{ env.PYINSTALLER_VERSION }}
UPX_VERSION=${{ env.UPX_VERSION }}
In this step, we create the image and push it to the GHCR. It makes use of docker/build-push-action.
- Tags make use of the output of docker/metadata-action, also make use of some pre-computed values from a previous step.
- We also pass configuration values to the
Dockerfile
usingbuild-args
.
Artifact attestation
- name: Generate artifact attestation
uses: actions/attest-build-provenance@v2
with:
subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME}}
subject-digest: ${{ steps.push.outputs.digest }}
push-to-registry: true
Generates signed build provenance attestations for workflow artifacts.
Artifact Attestations allow project maintainers to effortlessly create a tamper-proof, unforgeable paper trail linking their software to the process which created it.
For more details read Introducing Artifact attestations.
Cleaning Container registry
Storage isn't free and registries can often get bloated with unused images. Having a retention policy to prevent clutter makes sense in most cases.
In GCHR you can use the action actions/delete-package-versions to keep things tidy.
Example usage:
- uses: actions/delete-package-versions@v5
with:
package-name: "${{ env.PKG_NAME }}"
package-type: 'container'
min-versions-to-keep: 9
This simply keeps only the last 9 images.
Using the image
Once the container image is build and published to the GCHR you can run it with a command:
docker run ghcr.io/pkgowner/imagename:latest