Managing Node.js Docker images in GitHub Packages using GitHub Actions

If you’re doing open source development today, chances are high that you’re active within the GitHub community — participating in open source projects and their repositories. A recent addition to the GitHub ecosystem is GitHub Packages, which was announced back in 2019 and is now receiving even more updates with the general availability of the GitHub Packages container registry. This means we can publish and pull Docker-based images, and any other OCI-compliant formats, entirely within the GitHub ecosystem.

In this post, I’m going to take you through a GitHub Actions workflow, piece by piece. You’ll learn how to publish Node.js projects as Docker images and then push them to the GitHub Packages container registry.

Before we start, did you know that there’s a dedicated support forum for GitHubbers known as the GitHub Support Community? Worth a visit if you need help with anything.

Our Node.js project in this article will be dockly, an open source Node.js command line tool that provides an immersive terminal interface for managing docker containers and services.

dockly immersive terminal user interface for managing Docker containers and images

Creating a GitHub Actions workflow 

Browse to your open source repository on GitHub, click the Actions tab, followed by New Workflow and set up a workflow yourself, which should look like this:

Build, test, and deploy your code. Make code reviews, branch management, and issue triaging work the way you want. Select a workflow template to get started.

We start off with defining the workflow file name and set it as docker-publish.yml or any other naming convention you prefer.

GitHub may pre-populate the workflow code, in which case just remove it all and paste the following:

name: Docker

# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.

on:
  push:
    branches: [ main ]
    tags: [ 'v*.*.*' ]
  pull_request:
    branches: [ main ]

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

The above workflow sets up the following conventions:

  • The workflow only runs on commits to the main branch, or pull requests to the main branch.
    Note: If your repository is using the former convention of master branch, then you should rename it here and throughout the rest of the code snippet. It also runs on any push of tags to the repository with the semver format of <v*.*.*>, which is handy because we can then publish Docker images for each version.
  • It ensures that when a new Docker image is published, it will also create GitHub repository tags.
  • It sets up global environment variables for the rest of the workflow jobs which point to the GitHub Container Registry (ghcr.io), and sets the Docker image name to use the standard conventions like in Docker Hub of <user>/<repo>, such as lirantal/nodejs-app.

Next, we’ll define our jobs, in which we will establish a process of building the Docker image and then publishing it.

Docker image build as part of the GitHub Actions workflow

To be able to publish Docker images to the GitHub Packages container registry (or even just to GitHub), we need to first authenticate with a valid account. And so, the first steps in the build and publish job is to login.

Copy the following code as a continuation to the previous code pasting:

jobs:
  build_and_publish:

    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write

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

      - name: Log into registry ${{ env.REGISTRY }}
        if: github.event_name != 'pull_request'
        uses: docker/login-action@28218f9b04b4f3f62068d7b6ce6ca5b26e35336c
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.repository_owner }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Extract Docker metadata
        id: meta
        uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38
        with:
          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}

Let’s review the actions performed by jobs outlined here:

  1. The first step is to check out the repository’s source code.
  2. Then we follow-up with authenticating to the GitHub Packages container registry. As you can see, we’re reusing the REGISTRY environment variable that we defined before, pointing to ghcr.io, which is GitHub’s own registry for container images.

    The username to authenticate with is that of the user who initiated the workflow, which should match your user as the owner of the repository. The password is making use of the GITHUB_TOKEN which is automatically made available to GitHub Actions workflows and you don’t need to manually add it as a secret or environment variable in the repository. You’ll notice that the registry login doesn’t happen if this is a pull request, which is unnecessary anyway, and may expose sensitive information on pull request CI and forks.
  3. The last step in the above code snippet helps extract metadata from the image and make it available in the Docker image building process (our next and last step in this workflow!). The metadata consists of information about tags and labels that are made available to the Docker build action. Specifically, the passed input of images defines the Docker image to use as a base name for tags.

Build a Docker image and publish it to GitHub Packages container registry

The final step is to build the Docker image and publish it. This happens as a unified step which uses an action that supports that. You’ll notice, though, that we set the push input of that workflow to a conditional that only attempts publishing the image to the registry if the event isn’t triggered on a pull request:

      - name: Build and push Docker image
        uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc
        with:
          context: .
          push: ${{ github.event_name != 'pull_request' }}
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}

That’s it.

Once you save the workflow (with proper indentation!) and trigger it by merging this workflow to the main branch then it should build and publish an image to GitHub Packages container registry.

If everything went well, you should see the long-awaited green checkmark for a successful job, like mine:

Docker build and publish images to GitHub Packages container registry with GitHub Actions

How do I push a Docker image to GitHub Packages container registry?

Use the official Docker GitHub Action docker/build-push-action in your GitHub Actions workflow file, and ensure there’s an environment variable REGISTRY set to ghcr.io.

Pulling Docker images from GitHub Packages container registry

Now that our project has a docker image published to a public registry, let’s test it out by pulling it from our local development environment:

$ docker pull ghcr.io/lirantal/my-nodejs-app
Using default tag: latest
latest: Pulling from lirantal/my-nodejs-app
b4d181a07f80: Pulling fs layer
de8ecf497b75: Pulling fs layer
69b92f9e5e70: Pulling fs layer
1f2b8e2c8ad8: Waiting
d0f4259cb643: Waiting
9ae47f3f99ba: Waiting
87270829eb60: Waiting
905fc634546c: Waiting

How do I specify Docker version images to pull?

The docker pull command takes in an extra argument that has the scheme of <registry>/<user>/<repo>:<image tag>. For example, to pull the Docker image tag “latest” from the GitHub Packages container registry of the my-nodejs-app image, use the following command: docker pull ghcr.io/lirantal/my-nodejs-app

Yay, success!

But wait… isn’t there a better way to view the Docker images that I push to GitHub Packages? Look no further!

Find Docker images in GitHub Packages 

If you browse over to your GitHub profile page (such as mine https://github.com/lirantal?tab=packages), you’ll now see all of your publicly available Docker images.

If you followed through on this tutorial to build your own Docker image, can you find it? Here’s the page for the Docker image package that I have pushed to GitHub Packages as part of the repository GitHub Actions CI:

GitHub Packages showing pushed Docker image

No latest tag in the published Docker image?

If you tried to pull the Docker image by its name, without specifying the Docker image tag for the main or master branch that you’re using, you might have seen that the commonly used Docker image tag latest isn’t available. It happens if no semver (semantic versioning) tags were pushed, so perhaps, in your own repository you are pushing them and it will be available.

If we want to enforce a Docker image tag latest alias in the published Docker image, we can update the metadata action as follows (notice the additional multiline flavor key):

      - name: Extract Docker metadata
        id: meta
        uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38
        with:
          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
          flavor: |
            latest=true
            prefix=
            suffix=

Notice, however, that this will always force a latest image tag to be set as the alias to the Docker image that is published when the main branch is triggering a CI job.

How do I tag a Docker image as latest?

To tag an image locally from a previously built one, we can use the following Docker tag image command structure: docker tag <local image name> <new tag>. For example: docker tag my-nodejs-app lirantal/my-nodejs-app:latest

How do I run a Docker image with a tag?

To use a specific Docker image tag when running an image, simply provide the fully qualified image name and tag to the docker run command. For example: docker run --rm -p 27017:27017 mongo:latest

GitHub Actions Marketplace features relevant Docker and Container related integrations

Another way to find and create GitHub Actions workflows is from the GitHub Actions Marketplace, which features over 9000 workflows for you to work with across Code quality, Dependency management, Security, and more. You’ll even find the Snyk GitHub Actions to ensure that your projects are secure from third-party vulnerabilities in open source libraries and that your team is following secure coding practices.

For our focus on this article — building, testing and publishing Docker images to a registry —  there’s an easier way to get started than browsing the marketplace. If you have a Dockerfile already present in your repository, then GitHub will automatically detect it and propose relevant workflows to use for GitHub Actions. Simply go to the Actions tab:

Get started with GitHub Actions

The first suggested workflow Publish Docker Container will set you up with a similar Docker image publishing and Docker image building workflow to what we have reviewed already.

Summary and follow-up

Did I get you a bit more interested in secure development practices? Lovely! I have a few follow-up reading material resources to get you started and win some security points with your team:

  1. Supply chain security is important, so here are 10 GitHub Security Best Practices to make sure you’re not repeating them.
  2. Are you into Node.js and building npm packages? Me too! I wrote an article to get you started with GitHub Actions to securely publish npm packages
  3. If you spend a lot of time within the GitHub ecosystem, check out Snyk’s code scanning which provides a more integrated experience within your workflows: GitHub Security Code Scanning: Secure your open source dependencies

Secure your SDLC for free

Sign up for Snyk to secure your code, dependencies, containers, and IaC.