Github Actions provide the ability to define a build workflow directly in Github. The workflow steps are defined as yaml and can be triggered by various events, including a code push, branch, or tagging in the repository.
In this article I will detail the steps of creating a simple Spring Boot web application that when tagged with a semantic value (e.g. ‘v1.0.1’), will package it into an OCI-compatible (Docker) image that is published to both Docker Hub and Github Container Registry.
Prerequisites
OpenJDK 17+ for testing local compilation
# view candidate list of JDK sudo apt search openjdk-* | grep '^openjdk\-' | grep '\-jdk' sudo apt install openjdk-17-jdk
Github CLI for creating github repo from console
See my article here for installing the Github CLI
Create Spring Boot web starter project
A convenient way of getting the basic layout and scaffolding of a Spring Boot project is to use start.spring.io to download a project starter.
# setup project values id=spring-boot-github-action-example artifact_id=$id SpringAppClassName=SpringMain version="1.0.0" groupId="org.fabianlee" javaVersion=17 springBootVersion=2.7.5 # returns archive containing SpringBoot project, extract curl -s https://start.spring.io/starter.tgz \ -d type=gradle-project \ -d dependencies=web,prometheus,devtools,actuator \ -d javaVersion=$javaVersion \ -d bootVersion=$springBootVersion \ -d groupId=$groupId \ -d artifactId=$artifact_id \ -d name=$SpringAppClassName \ -d version=$version | tar -xzvf - # validate files have been created in directory cd $id ls -l
You now have a directory named “spring-boot-github-action-example” containing all the basic structure and files required for a Spring Boot web application.
You can copy-paste the commands above, or download start-spring-io-webapp.sh from my github.
Test Spring Boot web locally
This minimal web application will create a web server on port 8080 when run locally. Validate by compiling and running the Spring Boot web app.
# locally compiles Spring Boot jar ./gradlew bootJar # web server running locally on 8080 ./gradlew bootRun
From another console (or even a local browser), you should be able to get a valid HTTP response from port 8080.
# test from another console $ curl http://localhost:8080/actuator/health {"status":"UP"}
Add support for building OCI image using Gradle
The previous section illustrated the support for building the Spring Boot jar with the ‘bootJar’ task. But we need to enrich ‘build.gradle’ to support embedding this jar into an OCI-compatible image.
Dockerfile
The first step is creating a “src/main/resources/Dockerfile”. You can download my full Dockerfile here, or copy-paste the contents below.
# eclipse-temurin because OpenJDK is deprecated FROM eclipse-temurin:19.0.1_10-jdk-jammy # create non-root user and group for security compliance ARG THE_USER_ID=1001 ARG THE_GROUP_ID=1001 RUN \ /usr/sbin/groupadd -g $THE_GROUP_ID spring && \ /usr/sbin/useradd -l -u $THE_USER_ID -G spring -g $THE_GROUP_ID spring && \ mkdir logs && chgrp spring logs && chmod ug+rwx logs # run as non-root USER spring:spring # main port EXPOSE 8080 COPY springBoot.jar springBoot.jar CMD ["java","-jar","springBoot.jar"]
This will create an OCI image that runs springBoot.jar as the non-root “spring” user, using the eclipse-temurin base image (adoptopenjdk image has been deprecated).
build.gradle
Then we need to customize the build.gradle file to support building an OCI image that invokes the Spring Boot jar. We will add support for building the image in two different ways:
- Using Docker – well-know for image building and running
- Using Buildah – newer non-daemonized utility that can build OCI images
We only need to build an image using one of these tools, but I am going to add support for both in the build.gradle so we have the flexibility to switch at-will.
You can download my full build.gradle here, or copy-paste the contents below.
plugins { id 'org.springframework.boot' version '2.7.5' id 'io.spring.dependency-management' version '1.0.15.RELEASE' id 'java' } // ADDED as source of plugins repositories { mavenCentral() } group = 'org.fabianlee' version = '1.0.0' sourceCompatibility = '17' // ADDED want consistent name for jar bootJar { archiveFileName = "springBoot.jar" } // ADDED ability to specify docker owner and version on command line ext.dockerOwner = project.hasProperty('dockerOwner') ? project.getProperty('dockerOwner'):'fabianlee' ext.dockerVersion = project.hasProperty('dockerVersion') ? project.getProperty('dockerVersion'):'1.0.0' dependencies { implementation 'org.springframework.boot:spring-boot-starter-actuator' implementation 'org.springframework.boot:spring-boot-starter-web' developmentOnly 'org.springframework.boot:spring-boot-devtools' runtimeOnly 'io.micrometer:micrometer-registry-prometheus' testImplementation 'org.springframework.boot:spring-boot-starter-test' } tasks.named('test') { useJUnitPlatform() } // ADDED to make Dockerfile available in build directory task copyDockerfile(type: Copy) { from 'src/main/resources/Dockerfile' into 'build/libs' } ext.ownerProject = "${dockerOwner}/${project.name}" // ADDED build OCI image using Docker task docker(type: Exec) { group "OCI" dependsOn bootJar dependsOn copyDockerfile workingDir "${buildDir}/libs" commandLine "docker", "build", "-f", "Dockerfile", "-t", "${ownerProject}:${dockerVersion}", "-t", "${ownerProject}:latest", "." } // ADDED build OCI image using Buildah task buildah(type: Exec) { group "OCI" dependsOn bootJar dependsOn copyDockerfile workingDir "${buildDir}/libs" commandLine "buildah", "bud", "-f", "Dockerfile", "-t", "${ownerProject}:latest", "-t", "docker.io/${ownerProject}:latest","-t", "docker.io/${ownerProject}:${dockerVersion}","-t","ghcr.io/${ownerProject}:latest","-t","ghcr.io/${ownerProject}:${dockerVersion}", "." }
Create Github repository
Let’s go ahead and create the remote Github repository for this starter project and our Dockerfile and build.gradle modifications. We will add the Github Action workflows in the next sections.
# create remote repo (the 'id/repo' from output will need to be used below) $ CURDIR=${PWD##*/} $ gh repo create $CURDIR --public ✓ Created repository fabianlee/spring-boot-github-action-example on GitHub # initialize repo and add files git init git add * git add .gitignore # commit files and create main branch git commit -a -m "first commit" git branch -M main # push files to remote Github repo # use id/repo value from above for remote name git remote add origin https://github.com/fabianlee/spring-boot-github-action-example.git git push -u origin main
View remote Github repository in browser
The remote repository URL is shown by using the command below.
$ git remote -v origin https://github.com/fabianlee/spring-boot-github-action-example.git (fetch) origin https://github.com/fabianlee/spring-boot-github-action-example.git (push)
Pulling this URL up in your browser should looking something like the screenshot below.
Adding Github Action, overview
We will be adding a Github Action workflow by adding a yaml manifest file into the “.github/workflows” directory of our repository.
Workflows support various triggering events, we will choose to trigger when there is push to the repository of a tag that looks like a semantic version.
These workflows are run remotely on Github-hosted runners.
Add Github Action for OCI Image build and publish
Our workflow will kick off when any new semantic tag (e.g. v1.0.1) is pushed to the repository. I will go over key areas below, but download the full version of github-actions-buildOCI-image.yml from my github.
Trigger
on: push: tags: ['v*']
Java and Gradle setup
- name: setup Java uses: actions/setup-java@v3 with: java-version: '17' distribution: adopt cache: gradle
Build OCI image
Then we have gradle either build the OCI image with Docker or Buildah. You only need to build with one or the other, I simply wanted you to have an example of how each would be used.
env: BUILD_OCI_WITH: buildah # docker|buildah ... # (CONDITIONAL) build using docker - name: Execute Gradle build of OCI with Docker run: ./gradlew docker -PdockerVersion=${{ steps.getversion.outputs.VERSION }} if: ${{ env.BUILD_OCI_WITH == 'docker' }} # (CONDITIONAL) build using buildah - name: Execute Gradle build of OCI with Buildah run: ./gradlew buildah -PdockerVersion=${{ steps.getversion.outputs.VERSION }} if: ${{ env.BUILD_OCI_WITH == 'buildah' }}
Publish OCI image
This OCI image needs to be published to a registry. I will provide examples of pushing to both Docker Hub and Github Container Registry.
env: PUSH_TO_DOCKERHUB: true PUSH_TO_GITHUBCR: true ... # Push to Github CR - run: echo push to Github Container Registry ${{ env.PUSH_TO_GITHUBCR }} - name: Buildah push to Github Container Registry uses: redhat-actions/push-to-registry@v2 with: image: ${{ env.FULL_IMAGE_NAME }} tags: ${{ steps.getversion.outputs.VERSION }} latest registry: ${{ env.GH_REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} extra-args: | --disable-content-trust if: ${{ env.PUSH_TO_GITHUBCR == 'true' }} # Push to Docker Hub - run: echo push to Docker Hub ${{ env.PUSH_TO_DOCKERHUB }} - name: Buildah push to Docker Hub uses: redhat-actions/push-to-registry@v2 env: USER: ${{ secrets.DOCKER_USERNAME }} PASS: ${{ secrets.DOCKER_TOKEN }} with: image: ${{ env.FULL_IMAGE_NAME }} tags: ${{ steps.getversion.outputs.VERSION }} latest registry: docker.io username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_TOKEN }} extra-args: | --disable-content-trust if: ${{ env.PUSH_TO_DOCKERHUB == 'true' && env.USER!='' && env.PASS!='' }}
You may notice that the Docker Hub push uses a “secrets.DOCKER_USERNAME” and “secrets.DOCKER_TOKEN”. These are your Docker Hub username and personal access token that need to be added as secrets in this Github repository. The “secrets.GITHUB_TOKEN” used for pushing to the Github Container Registry is a special variable already populated by Github.
Create a Docker Hub personal access token using the documentation provided here. Then you can either go to “Settings” > “Secrets” > “Actions” in your Github web UI and press “New Repository Secret”. Or you can use the Github CLI to create repository secrets.
# prepend a space ' ' to the commands, avoids sensitive info going to history echo <yourDockerId> | gh secret set DOCKER_USERNAME echo <yourDockerPAT> | gh secret set DOCKER_TOKEN
Trigger Github Workflow
As mentioned earlier, this workflow is invoked by pushing a tag that looks like a semantic version. Here are the git commands for creating a tag and pushing it remotely.
# check for any changes that might need to be checked in first git status # create new tag that triggers workflow, push tag newtag=v1.0.0 git tag $newtag && git push origin $newtag
This will almost immediately create a workflow which you can view in real-time by going to your Github repository > Actions tab.
You can view the real-time progress by clicking into the task. The icon will indicate when the workflow is complete.
Validate Published images
Docker Hub
You can visit Docker Hub and sign-in with your Docker username to validate whether the image was published. You should be able to see your image and its tagged version similar to the screenshot below.
Github Container Registry
The Github Container Registry image can be reached from the Github web UI. Click on “Packages” > spring-boot-github-action-example, and the available image and tags will be displayed.
REFERENCES
stackoverflow, github action invoking python
github marketplace, redhat-actions/push-to-registry
github source, redhat-actions/push-to-registry
github source, slackapi/slack-github-action
jozala.com, reasons to use palantir plugin for docker instead of plain exec
NOTES
(Optional) Local OCI image creation for development/tests
The primary purpose of this article is to enable remote Github Actions to build this OCI image, so you do NOT need Docker or Buildah installed locally. However, in the real-world when there are many iterations of active development on the code, so for testing the container environment, you will clearly want to build and test the image locally.
local Docker build test
We are going to enable the remote Github Actions to build this OCI image using Docker. But IF you wanted to test this build with Docker locally, you would need to install Docker per my article here, then run:
# build using local Docker daemon ./gradlew docker # newly built image should now be available docker images
local Buildah build test
We are going to enable the remote Github Actions to build this OCI image using Buildah. But IF you wanted to test this build with Buildah locally, you would need to install Buildah per my article here, then run:
# build using local Buildah utility (non-daemonized) ./gradlew buildah # newly built image should now be available buildah images