Publish a Docker Image on GHCR with GitHub Actions

Publish a Docker Image on GHCR with GitHub Actions

ยท

6 min read

Table of contents

GitHub provides a registry called GitHub Container Registry (GHCR) to host your Docker images, which is a great alternative to DockerHub. The blog post offers a tutorial on how to build and publish Docker images to the registry using GitHub Actions.

Throughout the blog, GHCR will be used instead of the complete name.

Prerequisites

  • A GitHub account

  • Basic understanding of YAML (Not mandatory)

Steps

Create a GitHub repository and place a Dockerfile in the root directory. While you can store the file anywhere, it is advisable to keep it in the root directory.

For this demo, we will use a very simple Dockerfile that prints "Hello World" by running an echo command on an Alpine image. Here is the syntax for it:

FROM alpine:3.17.2
CMD ["echo", "Hello World!"]

Create a .github folder in the root of your repository.

  • Inside the .github folder, create a folder named workflows.

  • Inside the workflows folder, create a file named ghcr.yml.

  • Add the following configuration to ghcr.yml file.

Do not commit the changes yet. Let's first understand the workflow, and then we can proceed with the commit.

YAML Config:

name: Create and publish a Docker image to GitHub Container Registry

on:
  push:
    branches: ['main']

env:
  REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.repository }}

jobs:
  build-and-push-image:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write

    steps:
      - name: Checkout repository
        uses: actions/checkout@v3

      - name: Log in to the Container registry
        uses: docker/login-action@v2
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Extract metadata (tags, labels) for Docker
        id: meta
        uses: docker/metadata-action@v4
        with:
          #images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
          images: ${{ env.REGISTRY }}/pradumnasaraf/hello-world
      - name: Build and push Docker image
        uses: docker/build-push-action@v4
        with:
          context: .
          push: true
          # tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
          tags: ${{ env.REGISTRY }}/pradumnasaraf/hello-world:latest
          labels: ${{ steps.meta.outputs.labels }}

Let's understand the YAML config

If you're familiar with GitHub Workflows, you'll know that each step is usually self-explanatory.

In this particular workflow, we're triggering it whenever a new commit is pushed to the main branch, but this can be customized to suit specific needs.

The env: defines environment variables that can be used throughout the GitHub Action. In this case, the REGISTRY environment variable is set to ghcr.io, which is the GitHub Container Registry where the Docker image will be pushed. The IMAGE_NAME environment variable is set to ${{ github.repository }}, which is a GitHub context variable that provides the repository name in the format of owner/repo. This variable can be used to specify the name of the Docker image that will be built and pushed to the registry.

on:
  push:
    branches: ['main']

env:
  REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.repository }}

This YAML code block defines a job that will run on a ubuntu-latest runner based on our specific requirements. A job can include multiple steps, which are defined in the steps field.

The permissions field specifies the permissions that the job requires to run. In this case, the job requires read access to the repository contents and write access to packages.

The actions/checkout step is used to copy the contents of our repository to the runner environment where the job will be executed. This is necessary to enable subsequent steps to access the repository files and build and push the Docker image.

jobs:
  build-and-push-image:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write

    steps:
      - name: Checkout repository
        uses: actions/checkout@v3

Next, the action will log in to the GHCR registry using the GITHUB_TOKEN. We don't need to create this token because GitHub provides it by default.

      - name: Log in to the Container registry
        uses: docker/login-action@v2
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

The YAML code block below defines two actions for building and pushing a Docker image. The first action extracts metadata for the image, and the second action builds and pushes the image to a Docker registry. The metadata extracted in the first action is used as input for the second action. The registry, image name, and tags are defined using environment variables.

However, two lines in the code have been commented out because the username in the GitHub Container Registry (GHCR) should have a lowercase first letter, whereas in this case, it starts with a capital letter ("Pradumnasaraf"). To resolve this issue, the image name has been explicitly defined, and a comment has been added. If your username in GHCR does not have a capital letter at the beginning, you can uncomment the relevant lines and remove the comment above them.

      - name: Extract metadata (tags, labels) for Docker
        id: meta
        uses: docker/metadata-action@v4
        with:
          #images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
          images: ${{ env.REGISTRY }}/pradumnasaraf/hello-world
      - name: Build and push Docker image
        uses: docker/build-push-action@v4
        with:
          context: .
          push: true
          # tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
          tags: ${{ env.REGISTRY }}/pradumnasaraf/hello-world:latest
          labels: ${{ steps.meta.outputs.labels }}

Now, let's commit the changes. As soon as you commit the file, the workflow will be triggered because we have set the on: parameter to push:. This means that every time you push a commit to your repository, it will build and push a fresh Docker image based on the Dockerfile in the repository. Of course, you can set some constraints and conditions to limit this behaviour, but we won't discuss those in this blog post.

To check if the workflow is running, go to the "Actions" tab. In my case, the workflow ran so fast that it already completed all the steps to publish an image to GHCR and publish a package.

To check if the package has been released, navigate to the root of your GitHub repository. Under the Packages section, you should find a package named hello-world. This package indicates that the Docker image has been successfully built and published to the container registry.

Next, click on the hello-world package to retrieve the image URL and other relevant details.

To test our Docker image locally, we can run the following command. Instead of using pull, we will replace it with the run and use the copied URL.

Note: that the URL will vary depending on your username and repository name. In my case it will be:

docker run ghcr.io/pradumnasaraf/hello-world:latest

This command will download the Docker image from GHCR and run it in a container. You should see the "Hello World" message printed in the terminal.

Congratulations on successfully building and pushing a Docker image to GHCR using GitHub Actions! ๐ŸŽ‰

I hope you learned something from this blog. If you have, don't forget to drop a like, follow me on Hashnode, and subscribe to my Hashnode newsletter so that you don't miss any future posts. If you have any questions or feedback, feel free to leave a comment below. Thanks for reading and have a great day!

Did you find this article valuable?

Support Pradumna Saraf by becoming a sponsor. Any amount is appreciated!

ย