RichyHBM

Software engineer with a focus on game development and scalable backend development

Kotlin and Docker and Spring, Oh my!

Kotlin is a programming language developed by Jetbrains in 2011 as a JVM alternative to Java, it gained popularity amongst Android developers and in 2017 Google started officially supporting it as a first class language on Android. Until now I had only used it on Android but with the release of Spring 2.0, and the official support of Kotlin, I thought now would be a great time to try it out!

Kotlin, Spring and Gradle

The first thing needed is to setting up a Kotlin project with Spring, to do this I have used Gradle. Gradle is a build system used within the JVM ecosystem and most known for being the android build system, that said it is perfectly capable of building any JVM based projects.

Gradle relies on a specific file in order to know what tasks it needs to perform, this file needs to have the name #[b build.gradle]. I have used the following file to set gradle and Kotlin up along with Spring.

buildscript {
    ext {
        kotlin_version = '1.2.30'
        spring_version = '2.0.0.RELEASE'
    }

    repositories {
        jcenter()
    }

    dependencies {
        classpath "org.springframework.boot$spring_version"
        classpath "org.jetbrains.kotlin$kotlin_version"
    }
}

apply plugin: 'kotlin'
apply plugin: 'java'
apply plugin: 'org.springframework.boot'

sourceCompatibility = 1.8
targetCompatibility = 1.8

repositories {
    jcenter()
}

sourceSets {
    main.java.srcDirs += 'src/main/kotlin'
}

dependencies {
    implementation "org.jetbrains.kotlin$kotlin_version"
    implementation "org.jetbrains.kotlin$kotlin_version"
    implementation "org.springframework.boot$spring_version"
}

So, a bit about this file

Everything within the build script block pertains specifically to the tasks performed by Gradle.

In it for example is the ext block that allows you to create variables to be used in the building task, for example the Kotlin and Spring versions to use.

It also defines the repository to use when looking for dependencies, here I use JCentre but you could use other ones like maven.

And finally, the last thing I have included in the build script are the dependencies to be used in the build process, remember these aren’t the same dependencies that your application needs to use, these are specific to the Gradle process. In this case I am adding the Kotlin plugin so that Gradle knows how to treat Kotlin files, and the Spring plugin.

Next you need to actually apply the plugins that are included with your dependencies, as you can see I have used the Kotlin, Java and Spring plugins so that Gradle can build the application correctly.

Next thing I have defined are the source and target compatibility levels, these are set to 1.8 in order to ensure the compiled code is compatible with Java 8, this is more of a habit of mine as I tend to use Java 8 features when writing pure Java.

Then you need to specify where Gradle should search in order to find your application dependencies, usually this will be the same as the build script repositories, but you may have an addon that only needs to be added as an application dependency rather than a build dependency.

After this is the source set definition, this is used to tell the compiler where are source files are, as we are using Kotlin they are under the Kotlin folder. This step is only necessary if you are writing in something other than Java as Java gets picked up by default, but it is always good practice to add it.

And lastly is defining the actual dependencies for your project, in this case for Kotlin and the Spring framework.

Optional Gradle settings file

A Gradle settings.gradle file is optional but allows you to specify additional options, in my case I use one in order to specify the name of the built Jar file containing the application (otherwise it will default to the name of the containing directory).

rootProject.name = 'server'

Writing a simple Kotlin web service

The next step is to write a simple web service using Kotlin and Spring. To start off you will need to define an Application class, this is just an open class that is marked with SpringBootApplication

package example

import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.SpringApplication

@SpringBootApplication
open class Application

fun main(args: Array<String>) {
	SpringApplication.run(Application::class.java, *args)
}

This will create a simple class that Spring uses for our web server and then calls on Spring to run the application.

Next is creating our controller, in this case a very simple one.

package example

import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RestController

@RestController
class GreeterController {
    @GetMapping("/greeting")
    fun greeting() = ResponseEntity.ok("Hello")
}

This creates a simple endpoint “/greeting” that just returns “Hello”. As you can tell, the GetMapping is used to define the endpoint, this is a very simple endpoint that responds to GET requests but Spring allows much more.

Both files are in the package example, and as such should be named as:

src/main/kotlin/example/Application.kt
src/main/kotlin/example/GreeterController.kt

For further information on Spring and on getting started with web services, look here.

Build and run

At this point your application should be ready to run, you can try it out by first building with:

gradle build

And then running it with:

java -jar build/libs/server.jar

You should see lots of output from the Spring initialisation stage and then the port number to hit in order to see your web page, port 8080.

A sprinkle of Docker

So now that the web server is building and able to run, it’s time to containerize our application in a docker image.

First off is choosing a base image to use for our image, I am a big fan of minimalism and as such generally look at Alpine linux based docker images, luckily for us OpenJDK maintain a number of docker images based on alpine with a number of different configurations. For this example I will use the 8 JRE Alpine image, others are available here.

FROM openjdk:8-jre-alpine
WORKDIR /app
EXPOSE 8080
ADD ./build/libs/server.jar .
CMD ["java", "-jar", "server.jar"]

Here you can see the Dockerfile I used to run my image, a dockerfile is a specific file used by docker to specify the steps to be taken in order to build your image.

This image will take openjdk:8-jre-alpine as its base, change to the /app directory, open up port 8080 (the one our web server uses), add the jar that we previously built, and finally run our application.

You may ask why this is needed, well there are a number of blog posts and articles that explain the benefits of containerisation better than I could so as a TL;DR a major benefit is being able to know your application will always run on the same OS and with the same libraries regardless of if they run now or in 10 years time.

So, running docker build -t kotlin .; docker run -p 4000:8080 kotlin will tell docker to first build the image, giving it a tag named “kotlin”, it will then run the “kotlin” image binding the containers 8080 port to the hosts 4000 port. The port binding is necessary in order to reach your container via the network, or else docker wouldn’t know what port to use for each container.

By now you should be able to deploy your web app and have it run in a container, this would mean that you could uninstall Java from your development machine and the web app could still run via docker!

But wait, there’s more!

See something that docker allows you to do is a multi-stage build. A multi-stage build is a docker build process that builds the application using a heavy image, then deploys the application onto a much lighter image that only needs to contain the runtime for the application.

The way that docker manages this is by allowing you to specify a number of images in your docker file but only taking the operations performed on the last image as what should be on your final image, and because all of these images can copy files over, it allows you to build the application on one image then take a much lighter image and copy the application over.

Lets take for example a multi-stage dockerfile that will build our Kotlin program using gradle then deploy it to a smaller image.

FROM gradle:4.5-jdk8-alpine as builder
USER root
WORKDIR /builder
ADD . /builder
RUN gradle build --stacktrace

FROM openjdk:8-jre-alpine
WORKDIR /app
EXPOSE 8080
COPY --from=builder /builder/build/libs/server.jar .
CMD ["java", "-jar", "server.jar"]

First thing you will notice with this dockerfile compared to the last one is that it has 2 sets of FROM …, these each define a new image with the last one being what will end up on your final docker image.

Next difference is that the second image uses the COPY command passing in a reference to the first image “builder”, this tells docker to copy a specific file (or folder) from the builder image onto the current image.

Other than that the file is mostly the same as the first one, take the gradle:4.5-jdk8-alpine image, change to the root user (this image creates a local user, but for simplicity we can just change to root), change to the /builder folder, add the current directory (containing our kotlin project) to the container, and make a build using gradle.

Then the second half of the file takes a new image, openjdk:8-jre-alpine, changes directory, exposes our port on the docker image and copies the built jar from the builder image onto this image, finally running the jar.

Hopefully you can see the benefit to this as it allows you to take the benefits of having a containerised build system without having to deploy large images onto your servers!

Bonus content!

I thought as a bonus, and a reminder to myself, I would add an additional Dockerfile to show how you could accomplish the same multi-stage build when using Go. Like with Kotlin I am using Alpine, but unlike with Kotlin, the final image doesn’t need anything other than the Go executable and the base Alpine image!

FROM golang:1.10-alpine as builder
USER root
WORKDIR /builder
ADD . /builder
RUN go build -o go_app .

FROM alpine:3.7
WORKDIR /app
EXPOSE 8080
COPY --from=builder /builder/go_app .
CMD ["/app/go_app"]

For further information on Kotlin, Gradle, Spring or Docker, please see their official websites.