Github Private Packages Docker Containers

Building on the previous posts of publishing a maven jar then keeping that jar dependencies up to date with dependabot. The next step is to create a spring boot service using the jar, then creating a docker container finally publish that to private docker packages for others to use, and eventually be deployed.

The Docker File

First we need to create a docker file this file is rather straight forward, it just starts with a JAVA base, copies the jar and set’s the end point to run on startup.

1
2
3
4
FROM openjdk:17-jdk-slim
ARG JAR_FILE=target/*.jar
COPY ${JAR_FILE} app.jar
ENTRYPOINT ["java","-jar","/app.jar"]

GitHub Workflow

The whole process of building the project and publishing the docker container is going to be run by a GitHub Action, I’m just going to show sections of the workflow of which are of importance. I’m not going to cover target branches and conditional jobs with dependencies on other jobs here that’s a subject for another time.

Environment

The first step to make this all work together is tell the workflow to use the GitHub Register, and the image of the image to be created, here for ease of reference it’s simply set to the repository name. This could be hard coded, however I’d like to use this repository as a template repository so have used an easily accessible workflow variable.

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

Build and Upload Maven Jar

This section of the workflow purpose is to build the maven project, then share that jar file with the rest of the workflow. The process of building the jar file and docker image could be done in a single job. However, I like to keep this in a different job so that I can use the same workflow for build and test for all PRs and development branches. Then only on the main branch build the docker image.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
- name: Build and Test with Maven
  run: |
    mvn -B -Pgithub install --file pom.xml    
  env:
    GITHUB_USER_REF: ${{ secrets.MVN_USER }}
    GITHUB_TOKEN_REF: ${{ secrets.MVN_KEY }}

- name: Upload build artifacts
  uses: actions/upload-artifact@v4
  with:
    name: app-jars
    path: target/*.jar

This results in a slightly longer and complicated workflow, however it comes with the benefit that it’s consistent with building the software and only requires one place to update if the build process changes. The second part of this job is to upload the jar to the workflow runner, this allows the building of the jar and docker container to remain isolated and share the file. Therefore we will need to add an additional step to the docker build job in order for this process to work.

Docker Image Build and Publish

The next step and the main reason for this workflow to exist is to take the jar from the previous job, build a docker container and then finally publish this to the private GitHub docker register.

Gather Artifacts

In the building of the jar section of the workflow, I uploaded the jar file to be shared between jobs. This therefore requires this job to go and get that file to be used within the construction of the docker container.

1
2
3
4
5
- name: Download build artifacts
  uses: actions/download-artifact@v4
  with:
    name: app-jars
    path: target

This step is rather simple, however I’ve added a path variable. This is to ensure that from the root of the repository there is a target folder created and the jar file that’s been downloaded is placed inside that folder. This is because the docker file expects to find the jar file within the target folder, which is convention for maven projects. I’ve kept this convention so that the docker file can be used by developers locally on their machine as well without any configuration or changes required.

Build and Publish

The first step after downloading the jar file for the action service to be turned into a docker container is to log into the the GitHub private container registry, this will use the environment variable declared at the top of the workflow, then the standard github actor, and the default GITHUB_TOKEN which is accessible to the workflow. For this to work the workflow has to be given the correct permissions defined within it with the following contents: read, packages: write, attestations: write and id-token: write.

The next step is to extract all the data required to build a docker container with names, tags and labels. To keep things simple I’m just using the metadata action to do this. This means that the register will use the environment variable, the image name will be the repository name, and the tag will just use the branch name, which for my use case is perfect.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
- name: Log in to the Container registry
  uses: docker/login-action@65b78e6e13532edd9afa3aa52ac7964289d1a9c1
  with:
    registry: ${{ env.REGISTRY }}
    username: ${{ github.actor }}
    password: ${{ secrets.GITHUB_TOKEN }}

- name: Extract metadata (tags, labels) for Docker
  id: meta
  uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7
  with:
    images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}

- name: Build and push Docker image
  id: push
  uses: docker/build-push-action@f2a1d5e99d037542a71f64918e516c093c6f3fc4
  with:
    context: .
    push: true
    tags: ${{ steps.meta.outputs.tags }}
    labels: ${{ steps.meta.outputs.labels }}

The final step of this job with the id push is where the docker image is built, using the metadata from the previous step. This step is run with the context of root of the repository as that’s where the DockerFile is located, and told to attempt to push the container, using the tags. This will create a new image within the register with the tag of the branch, if there was an image already tagged with the branch name, then the tag is removed from the previous image.

Clean Up Register

The final step is to clean up the GitHub register. This step isn’t completely needed however it’s good practice and keeps storage costs low. This is because when a new container of the same tag is published, then the previous container is untagged. The untagged container is still using storage, which is something that GitHub will charge you for if you go over the allowances. If you’re deploying regularly then this is easily done.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
remove-package-versions:
    needs: docker
    name: Remove Untagged Docker Images
    runs-on: ubuntu-latest
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Get repository name
        id: repo-name
        uses: MariachiBear/get-repo-name-action@v1.1.0
        with:
          string-case: 'lowercase'

      - name: purge packages
        uses: dylanratcliffe/delete-untagged-containers@main
        with:
          package_name: ${{ steps.repo-name.outputs.repository-name }}
          token: ${{ secrets.MVN_KEY }}

The above workflow when run, will check the register for all images that are no longer tagged. Then all these containers will be deleted, this is all drive by the repository name. Therefore I’m assuming here that you’re still using the method explained previously to create your docker container as that image name uses the repository name, which I feel is the best approach as it keeps everything easily referenced and consistent.


↤ Github Private Packages Dependabot
Night Sky Pi Released ↦