RichyHBM

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

Kotlin Server in Less Than 5MB

One of the things I like most about languages like GoLang is the fact you can create a small executable that holds all the files needed for a server, or any other application. Kotlin, and any other JVM based languages, however always seem to be rather bloated, Kotlin and Spring have often been over 100mb, and even Scala and Play Framework are of a similar size.

Ok, so this title may be a bit clickbait, what I actually managed to get was a distribution zip of my server code along with html templates and static files in just about 5mb. Of course once uncompressed this will grow and you still have the requirement of having the JVM installed to run it.

So to begin with you have to choose a server framework to run on, Netty or Jetty are lightweight servers that have pretty good performance whilst being relatively small, however both of these are quite low level for most of my use cases. Instead I have chosen HTTP4K which is a web server built on top of either Netty or Jetty (and also includes a simple server for development purposes), another option would be Ktor which is developed by Jetbrains and is also a slim layer over Netty/Jetty.

Setup

For this I am using Gradle, I have an HTML templating library, dependency injection, logging, Stripe and even a unit test framework in my project (though this last one isn’t included when making a distribution build)

The Gradle file looks like so:

buildscript {
    ext {
        kotlin_version = '1.2.40'
        http4k_version = '3.24.0'
        stripe_version = '5.31.0'
        kodein_version = '5.0.0'
        rocker_version = '0.24.0'
        detekt_version = '1.0.0.RC6-4'
        java_target_version = 1.8
        slf4j_version = '1.7.25'
        kotlintest_version = '3.1.0-RC2'
    }

    repositories {
        jcenter()
        maven { url "https://plugins.gradle.org/m2/" }
    }

    dependencies {
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
        classpath "gradle.plugin.com.fizzed:rocker-gradle-plugin:$rocker_version"
        classpath "gradle.plugin.io.gitlab.arturbosch.detekt:detekt-gradle-plugin:$detekt_version"
    }
}

//
//  Plugins
//
apply plugin: "kotlin"
apply plugin: "application"
apply plugin: "com.fizzed.rocker"
apply plugin: "io.gitlab.arturbosch.detekt"

//
//  Source sets
//
sourceSets { 
    main { 
        rocker { 
            srcDir('src/main/kotlin') 
        } 
        java {
            srcDirs += 'build/generated-src/rocker/main'
        }
    } 
}

//
//  Compiler
//
mainClassName = 'example.ApplicationKt'

sourceCompatibility = java_target_version
targetCompatibility = java_target_version
compileKotlin { kotlinOptions.jvmTarget = "$java_target_version" }
compileTestKotlin { kotlinOptions.jvmTarget = "$java_target_version" }

//
//  Plugins config
//
kotlin { 
    experimental { 
        coroutines "enable" 
    } 
}

detekt { 
    profile("main") { 
        config = "$projectDir/detekt.yml" 
    } 
}

test {
    useJUnitPlatform()
}

rocker {
    skip false
    failOnError true
    skipTouch true
    touchFile ""
    javaVersion "$java_target_version"
    optimize false
}

//
//  Gradle tasks
//
compileKotlin.dependsOn tasks.generateRockerTemplateSource
run.dependsOn tasks.generateRockerTemplateSource
assembleDist.dependsOn tasks.generateRockerTemplateSource

//
//  Dependencies
//
repositories {
    jcenter()
    maven { url "http://dl.bintray.com/jetbrains/spek" }
    maven { url "https://dl.bintray.com/http4k/maven" }
    maven { url 'https://jitpack.io' }
}

dependencies {
    implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
    implementation "org.http4k:http4k-core:$http4k_version"
    implementation "org.http4k:http4k-server-netty:$http4k_version"
    implementation "com.stripe:stripe-java:$stripe_version"
    implementation "org.kodein.di:kodein-di-erased-jvm:$kodein_version" 
    implementation "com.fizzed:rocker-runtime:$rocker_version"
    implementation "org.slf4j:slf4j-api:$slf4j_version"
    implementation "org.slf4j:slf4j-simple:$slf4j_version"
    //Test
    testImplementation "io.kotlintest:kotlintest-runner-junit5:$kotlintest_version"
}

So a bit about the above file, and my rational over why to use specific frameworks:

Language

As should be evident by the file above I have chosen Kotlin for this API as it allows the versatility of a JVM based application, along with the libraries and frameworks already existing for Java, but without any of the verbosity of Java nor the complication of Scala; which while a really enjoyable language, does have its issues for on-boarding new developers.

Server

For this example I have chosen HTTP4K, realistically to keep within the 5mb constraint it was between HTTP4K, Ktor or rolling my own layer on top of Netty/Jetty. HTTP4K seems to have a little higher level focus than Ktor, ships with support for server-less, is immutable by design (which appeals to the functional programmer within me) setups and seems like a nice community of developers so I thought I would give it a try.

Dependency Injection

I feel like the benefits of dependency injection shouldn’t really need to be said again, there are plenty of articles and guides as to why DI is beneficial. That said I went with Kodein as it seems like a mature and well used framework.

Templating

Templating is a hard one, there are many different libraries for this and HTTP4K ships with a few built in, that said I chose to go with a completely different library in the form of Rocker rather than others partially due to its similarities with Play Frameworks template syntax and partly because it works by generating code at compile time rather than run time, meaning your templates get checked by the compiler.

Logging

For logging I am using SLF4J which is an interface of sorts that loads up other logging implementations, in this case the implementation is just a simple logger that outputs to the console, but this could be swapped for something more sophisticated in production.

Unit tests

Any good application should have unit tests, they verify that your code and logic are working correctly and hopefully help avoid any issues when live. There are many flavours of unit testing, with Spek being one of the bigger Kotlin based frameworks. With that said I just wanted a simple framework with everything included, and an easy to see output onto the console, that is why I decided to go with KotlinTest alongside gradles’ built in JUnit support.

Overall

Ultimately I was aiming to produce something akin to Play Framework, I feel this setup gives me a similar syntax for both the templates and unit tests to play, and mostly all that is needed is a configuration setup to read settings from a file at runtime. As the netty backend is used for the server, this setup should be more than capable to take a decent amount of load with minimal effort.

Code

The code is fairly straightforward, consisting of the sample HTTP4K starter app with a small modification to load up and return the template HTML. Bellow you can also find the template itself, this requires a separate gradle task when using kotlin however I have made the compileKotlin task depend on this generation task.

package example

import org.http4k.core.Method.GET
import org.http4k.core.Request
import org.http4k.core.Response
import org.http4k.core.Status.Companion.OK
import org.http4k.routing.ResourceLoader.Companion.Classpath
import org.http4k.routing.bind
import org.http4k.routing.routes
import org.http4k.routing.static
import org.http4k.server.Netty
import org.http4k.server.asServer

fun main(args: Array<String>) {
    val port = if (args.isNotEmpty()) args[0].toInt() else 5000

    val app = routes(
        "/static" bind static(Classpath("/")),
        "/" bind GET to { _: Request -> Response(OK).body(views.HelloWorld
            .template("My Website Title", "This is some content")
            .render()
            .toString())
    })

    app.asServer(Netty(port)).start().block()
}

The views are alongside code files, and can be accessed using the same package they are in (In this example, this view would be in the views package.)

@args (String title, String content)

<html>
  <head>
    <title>@title</title>
  </head>
  <body>
    @content
  </body>
</html>

Static files such as CSS or JavaScript can be placed in the Resources folder and accessed directly from “/” when running the server.

Outcome

Whilst a simple example, all of this put together gets you a distribution build at just 5mb, with a minimal JRE container this could easily mean a sub 100mb docker image which I feel is more than acceptable (even if it isn’t the 5mb docker image for a basic GoLang web server)

$ gradle assembleDist; ls -lh build/distributions/

BUILD SUCCESSFUL in 1s
9 actionable tasks: 9 up-to-date
total 11M
-rwxrwxrwx 1 richy richy 5.6M Apr 29 13:19 server.tar
-rwxrwxrwx 1 richy richy 5.0M Apr 29 13:19 server.zip

Comments