Docker Images in GitHub Packages (Private Repos)
Getting private Maven packages working is useful, but most services deploy as containers. The next step is to package the application as a Docker image and publish it to GitHub Container Registry (GHCR), then keep costs sane by cleaning up what is left behind. This is a minimal, repeatable setup that builds a Spring Boot jar, turns it into an image, pushes it to GHCR, and removes untagged images so storage does not creep up over time.
For background, see the earlier posts on publishing Maven packages and Dependabot for private packages. This post focuses on containers only.
Dockerfile
Nothing fancy is needed for a Spring Boot service. A slim Java base, copy the built jar, and run it on start.
|
|
This same Dockerfile should work locally and in CI as long as the jar lands in target/
before the image build step.
Workflow structure
The workflow builds the jar first, stores it as an artifact, then downloads it in a second job that builds and publishes the container. Keeping the build separate from the image step makes it easy to reuse the same build process for pull requests without producing images on every branch.
Shared environment
Use the repository name as the image name and publish to GHCR.
|
|
Build the jar and upload it
Build with Maven and upload the resulting jar. This mirrors local development and keeps the Dockerfile simple.
|
|
Download, build, and push the image
Download the jar to target/
so the Dockerfile can find it, then log in to GHCR, generate tags and labels, and push.
|
|
Permissions: the job needs packages: write
to push, plus attestations: write
and id-token: write
if you enable provenance later. Add these at the job level:
|
|
The docker/metadata-action
uses sensible defaults. By default it tags images with the branch and commit. When a branch tag is reused, GHCR moves the tag to the latest image but keeps the older images untagged.
Keep storage under control
Untagged images accumulate after each push. To avoid paying for storage overages, add a cleanup job that removes untagged images for this repository after a successful push.
|
|
Use a Personal Access Token for the deletion step if required by the action you choose. Reusing the same PAT that authenticates Maven is fine as long as it has the repo
and write:packages
scopes.
Troubleshooting checklist
- Push denied: confirm job permissions include
packages: write
. - No jar in image build: verify the download step writes to
target/
so the Dockerfile’sARG JAR_FILE=target/*.jar
can find it. - Wrong image name: check
IMAGE_NAME
is set to${{ github.repository }}
and thatREGISTRY
isghcr.io
. - Storage climbing: confirm the cleanup job runs after pushes and that the PAT has permission to delete untagged images.
- Local dev parity: run
mvn -B -Pgithub install
locally, thendocker build -t test .
to confirm the Dockerfile works outside CI.
That is the entire loop. Build the jar once, reuse it for the container image, publish to GHCR with the built in GITHUB_TOKEN
, and prune what you do not need. The result is predictable, cheap to run, and easy to copy across repositories.
Thanks for reading. If you have a leaner cleanup approach or a tagging convention that scales for multi environment deploys, share it so others can benefit.