Fun with Github Actions and Docker

Fun with Github Actions and Docker

I haven’t really worked with Github Actions before, so I decided to set up a test pipeline and it was fun. It was surprising how powerful yet easy it was to get started with a build and deploy pipeline. In my example, I decided to Dockerise a small Spring Boot application and configured a Github Action to build it to a JAR using Maven, build an image and deploy it to DigitalOcean, where it automatically restarts with the new Docker image.

If you want to play with this example, I’ve included the code in this repository. The project is configured for Maven, but you’re welcome to switch to Gradle and make any relevant changes!

If you plan on following along with your own code, this post assumes:

  • you’re already set up to build and run Java code
  • you have Docker installed locally
  • you have GitHub and DigitalOcean accounts, or are willing to set those up

We Need Something to Deploy

We can’t deploy thin air, so let’s go to the Spring Initializr and generate a project. I left most values to default, but for reference I went for Maven, Java 17, Spring Boot 3.1.2 with JAR packaging. You do you, though. In Dependencies, let’s add Spring Boot Web. Realistically, this app isn’t going to do anything, so let’s just print a scheduled message so we can test it in DigitalOcean. In the main class, add the @EnableSchuling annotation.

Then we can make a class for printing the message. Let’s make a LogMessageScheduler class and add the following method:

@Scheduled(fixedRate = 30000)
public void printMessage() {    
	System.*out*.println("Hello from Docker Demo App 1! This is version 0.0.2");
}

This will print the message every 30 seconds. That’s it from the code side! You can run the application and verify the message is printing correctly.

Let’s Dockerise Our App

If you’re familiar with Docker, great! If not, there’s a great introduction right here. In short, it packages your application and its dependencies into containers that can be deployed consistently and easily across different environments. This leads to less environmental issues and makes things like testing and validation easier to do. Docker is ultimately pointless for this tiny app other than to have some fun, but it’s very useful at managing deployments, lowering the amount of time spent on environmental work and minimising production issues at scale.

Turning our Spring Boot into a Docker-ready application is very easy! I’m going to add a file called Dockerfile to the root of my application. In this file, I’ll put the following content:

FROM eclipse-temurin:20-jdk-alpine
VOLUME /tmp
COPY target/docker-github-actions-demo-app1-0.0.2-SNAPSHOT.jar app.jar
ENTRYPOINT ["java","-jar","/app.jar"]

Let’s go through these values:

FROM: this is the base image that you’re building your image off. There are many options for different use cases, but for this one we’ll use an Eclipse Temurin image.

VOLUME: this specifies a path within the directory to store data.

COPY: this specifies files to be moved from the local machine to the container. In this case, we’re copying our Spring Boot JAR over to the container and naming in app.jar. This would need to be updated as we increment our app version in real life.

ENTRYPOINT: this specifies the command to be run at startup, in this case it runs our Spring Boot application.

Dockerfiles can get more complicated than this, but ours is quite short!

Setting up DigitalOcean

I like DigitalOcean as a cloud provider, this here blog is hosted on their platform. I’ll be using DigitalOcean as an example of where to deploy, so if you’re intending to follow on, get an account set up there. You can usually find free credits for signing up to a new account if spending money to deploy a pointless application doesn’t sound appealing to you.

DigitalOcean offers users a free container registry, which we’ll use to store our deployed Docker containers from Github. To set this up, go to the Container Registry page of your control panel. Give yours a unique name (for mine, I’ll use “robdillon-docker-springboot”) and create your registry. In order to access this registry from Github, you’ll need a Personal Access Token. If you’re not sure how to do that, have a look here.

Our Github Workflow

Github Workflows are stored as YAML files in a .github directory in your project’s root. I’ve made one in my project called docker-image.yml with the following contents:

name: Docker Image Deploy to DigitalOcean

on:
  push:
    branches: [ "main" ]
  pull_request:
    branches: [ "main" ]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v3
    - name: Set up JDK 17
      uses: actions/setup-java@v3
      with:
        java-version: '17'
        distribution: 'temurin'
    - name: Build and test
      run: ./mvnw clean package
    - name: Install doctl
      uses: digitalocean/action-doctl@v2
      with:
        token: ${{ secrets.DO_API_PRIVATE_KEY }}
    - name: Log in to DigitalOcean Container Registry with short-lived credentials
      run: doctl registry login --expiry-seconds 1200
    - name: Build the Docker image
      run: | 
        docker build -t robdillon/demoapp1 .
        docker tag robdillon/demoapp1 registry.digitalocean.com/robdillon-docker-springboot/robdillon/demoapp1
        docker push registry.digitalocean.com/robdillon-docker-springboot/robdillon/demoapp1

So what does all this mean?

name: whatever name you want to give your shiny new workflow

on: this sets the trigger for the workflow, in this case any push directly or PR to the main branch

jobs: defines the jobs within the workflow. While ours could be split out into multiple steps, we just have one build job containing the following steps:

  • sets workflow to be ran on ubuntu-latest
  • checks out the code from the repository
  • sets up the JDK to be used (in this case JDK 17 from Temurin)
  • build, test and packages the code using Maven
  • installs DigitalOcean’s command line tool needed to talk to our Container Registry
  • logs into the DigitalOcean container registry
  • builds and pushes the docker image “robdillon/demoapp1” to the Container Registry

This file is directly linked to my container registry, so you’ll need to update the values to reflect your own setup. There’s also a reference to DO_API_PRIVATE_KEY. Remember the Personal Access Token you set up earlier? We need to add this to our pipeline so it has access rights to our Container Registry. To do this, go to your repository Settings → Secrets and Variables → Actions. You can then add a repository secret with the name DO_API_PRIVATE_KEY and the value that you generated from DigitalOcean.

Liftoff!

If we push to our main branch, we should see it deploy to our Container Registry in DigitalOcean! Let’s recap what we did to get here:

  • generated a sample Spring Boot app
  • added a Dockerfile in order to build an image of this app
  • added a GitHub Workflow file that defines our automated job
  • created a container registry in DigitalOcean to deploy to
  • added our DigitalOcean Personal Access Token as a secret to our repository

With this push, we can see our Action in Github was successful:

And in DigitalOcean, we now have a container in our Container Registry:

Let’s create an active instance of this application so that all this work doesn’t go to waste and we get to see our log message on a computer other than ours:

Create → Apps → DigitalOcean Container Registry → choose your container registry → select ‘latest’ tag → leave ‘Autodeploy’ option selected. Go through the next step and choose a Basic Plan. Once it starts, we should see our message in the runtime logs:

Redeploy, quick!

So our deployment went well, and we can see our “Hello from Docker Demo App 1! This is version 0.0.1” message just fine. But let’s pretend we just got a business critical request to change this message to say 0.0.2 instead. It’s a good opportunity to test that our auto-deploys work as well as keeping a fictional person happy. Let’s update our message:

@Scheduled(fixedRate = 30000)
public void printMessage() {    
	System.*out*.println("Hello from Docker Demo App 1! This is version 0.0.2");
}

Along with this, let’s update our pom.xml to reflect the new app version:

<version>0.0.2-SNAPSHOT</version>

And finally, let’s update our Dockerfile so it knows which JAR to copy to the image:

COPY target/docker-github-actions-demo-app1-0.0.2-SNAPSHOT.jar app.jar

Now, let’s push these three changes to main like the crazy people we are! We can see the Github Action kicking off based on our push to main:

And we can also check DigitalOcean to see our application has restarted and is displaying the new message we’ve just added:

And it all works as expected! Now we can celebrate by destroying our App in DigitalOcean to avoid getting a nasty bill for a pointless Spring Boot application. 🙂