Getting Docker Containers into GitHub's Private Package Registry

Alright, so I’ve been on a bit of a journey with GitHub Packages lately. First I tackled publishing maven jars, then I set up dependabot to keep dependencies fresh. Now it’s time for the next logical step - wrapping everything up in a Docker container and getting it into GitHub’s private package registry.

The Docker File

Let’s start with the Docker file - nothing fancy here. It’s about as basic as it gets for a Spring Boot service. Grab a slim Java base image, copy in a jar, and tell it to run that jar when the container starts:

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

Pretty standard stuff. Now let’s get to the interesting part…

GitHub Workflow

I’m not going to bore you with every single detail of the workflow (branches, conditionals, etc. - that’s a whole other post). Instead, I’ll focus on the parts that actually make this Docker publishing process tick.

Environment

First things first - we need to tell our workflow where to publish the container and what to call it. I’m keeping it simple by using the repository name as the image name, which makes it super easy to track what’s what:

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

I could have hardcoded the name, but I want to be able to use this workflow as a template for other projects, so using the repo name just makes sense.

Build and Upload Maven Jar

Now we need to build our jar and make it available for the Docker build step. Sure, I could combine this with the Docker build job, but I prefer keeping them separate. This way I can reuse the exact same build process for PRs and feature branches without having to build Docker images every single time:

 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

Yeah, it makes the workflow a bit longer, but trust me - when you need to change how your build works, you’ll thank yourself for only having to update it in one place!

Docker Image Build and Publish

Ok, here’s where the real magic happens. We need to grab that jar we built, package it into a Docker image, and push it to GitHub’s registry.

Gather Artifacts

First, let’s grab the jar we just built:

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

See that path: target part? That’s important - it makes sure our jar ends up in the same place that our Dockerfile expects to find it. This way the same Dockerfile works both in the CI pipeline and for local development. No adjustments needed!

Build and Publish

Now let’s get that container built and published:

 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 }}

Quick note: for this to work, your workflow needs some specific permissions: packages: write, attestations: write, and id-token: write. Without those, you’ll just end up with permission errors.

Oh, and I’m using the metadata action to handle all the container naming and tagging. It uses the branch name as the tag, which keeps things nice and tidy. When you push a new container with the same tag, GitHub just moves the tag to the new image (but keeps the old one around, untagged).

Which brings me to…

Clean Up Register

Here’s something a lot of people miss - GitHub charges you for storage when you go over your allowances, and those untagged images add up FAST if you’re doing frequent deployments:

 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 }}

This job runs after we’ve pushed a new image and cleans house by removing any untagged containers. Your future self (and your GitHub billing) will thank you!

And there you have it - a complete pipeline that builds your jar, packages it into a Docker container, publishes it to GitHub’s package registry, and keeps things tidy. Neat, right?


↤ Previous Post
Next Post ↦