Christmas Hacking: Squeezing Kotlin Native into Docker
Tags: kotlin, docker, linux
Published: 2019-12-25 22:49:39

Some time ago, I had a conversation with my fellow GDG organizer [Randy Gupta](https://twitter.com/randygupta). He's a Google Developer Expert on cloud, and he blew my mind when he said that you can create Docker images containing a single, self-contained executable. Think about it: no libraries, no package manager - just a single program doing it's job. That's amazing!

So because I love Kotlin, I wondered how far we can take this using [Kotlin Native](https://kotlinlang.org/docs/reference/native-overview.html). With that, we can compile Kotlin code as an executable. Then we can add this to a Docker image. Let's begin, and have some fun! 😊

## Kotlin, Go Native

As a first step, we'll create a simple hello world program and compile that to native. There's a good [tutorial](https://kotlinlang.org/docs/tutorials/native/basic-kotlin-native-app.html) to start. Our `hello_world.kt` program:

```kotlin
fun main() {
    println("Hello, World!")
}
```

Now we can compile and run it natively:

```console
$ kotlinc-native hello_world.kt -o hello_world

$ ./hello_world.kexe
Hello, World!

$ file hello_world.kexe
hello_world.kexe: Mach-O 64-bit executable x86_64
```

Ok, this works! But as you can see, because I ran this on my Mac, this created a Mac executable. For Docker, we will need a Linux executable. We can pass `-target linux` as a parameter to `kotlinc-native` to do that:

```console
$ kotlinc-native -target linux hello_world.kt -o hello_world_linux

$ ll *.kexe
-rwxr-xr-x  1 mreichelt  mreichelt   607K 25 Dez 19:06 hello_world.kexe
-rwxr-xr-x  1 mreichelt  mreichelt   375K 25 Dez 19:14 hello_world_linux.kexe

$ file hello_world_linux.kexe
hello_world_linux.kexe: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.16, BuildID[xxHash]=421d2b66bd8de945, not stripped
```

Ok, cool - we have a Linux binary! Docker will be so happy… 🎉

## Docker + Kotlin Native = ❤️

Let's pack this into a Docker image using a `Dockerfile`:

```docker
FROM ubuntu
COPY hello_world_linux.kexe /hello_world
ENTRYPOINT ["/hello_world"]
```

Now, we can build the Docker image & run it:

```console
$ docker build --tag hello_kotlin .
Sending build context to Docker daemon  5.179MB
Step 1/3 : FROM ubuntu
 ---> a2a15febcdf3
Step 2/3 : COPY hello_world_linux.kexe /hello_world
 ---> 9438118fba59
Step 3/3 : ENTRYPOINT ["/hello_world"]
 ---> Running in 2454425124e7
Removing intermediate container 2454425124e7
 ---> 47b7a9b05993
Successfully built 47b7a9b05993
Successfully tagged hello_kotlin:latest

$ docker run --rm hello_kotlin
Hello, World!

$ docker image list hello_kotlin
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
hello_kotlin        latest              47b7a9b05993        2 minutes ago       64.6MB
```

Yay, it works! Think of what you can do now:

- Compile Kotlin code to a native binary
  - you don't have to use the CLI compiler - this should work for your Gradle-builds from the IDE as well!
- no need for the JVM, so our Docker image already has **64.6MB** in size (not more than 600MB)! 🌟

And because Docker caches the lower layers (for us it's Ubuntu), there is no actual need to make this smaller. This will be the best result for most people, because if you need a shell or other Ubuntu tools, this image will give you that flexibility. But if you're courious how we could make this smaller, and you're willing to leave some comfort behind: read on!

## Honey, I Shrunk the Docker Image

Docker offers a way to create [images from scratch](https://docs.docker.com/develop/develop-images/baseimages/#create-a-simple-parent-image-using-scratch). So let's do that - we create a file `shrinked.Dockerfile`, and we'll build a new Docker image `hello_kotlin_shrinked` containing only our Kotlin Native executable!

```docker
FROM scratch
COPY hello_world_linux.kexe /hello_world
ENTRYPOINT ["/hello_world"]
```

So let's build and run that:

```console
$ docker build --tag hello_kotlin_shrinked . --file shrinked.Dockerfile
[…]
Successfully tagged hello_kotlin_shrinked:latest

$ docker run --rm hello_kotlin_shrinked
standard_init_linux.go:211: exec user process caused "no such file or directory"
```

Hmm, that didn't work out. The reason is simple: the Kotlin Native compiler created a dynamically linked binary, meaning this file contains references to some shared libraries (`.so` files on Linux, `.dll` on Windows). Bummer!

I couldn't find any option for the `kotlinc-native` compiler to build a static, self-contained executable. I played around a little, but I didn't find anything close to how Go creates self-contained binaries. I found this [excellent blog post](https://xebia.com/blog/create-the-smallest-possible-docker-container/) on how to do this from Go.

I also asked around in the [#kotlin-native](https://kotlinlang.slack.com/archives/C3SGXARS6) channel of the Kotlin Slack. I got a reply by Dominic Fischer that there's an `-include-binary` option which could do what I want. I didn't come around to try that out yet, but if I do I'll update this post. Thanks, Dominic!

So, our binary doesn't run because it can't find those shared libraries. What happens if we add those to the Docker image directly? We can find which are needed by using the `ldd` tool. Let's use our previously created, Ubuntu-based Docker image to debug this. We're overriding the entrypoint to be a `bash`. Also we can mount the current working directory to `/host` if we need this later.

```console
$ docker run --rm --volume $(pwd):/foo --entrypoint /bin/bash -it hello_kotlin

root@a34eeb5c7bef:/# ldd /hello_world
    linux-vdso.so.1 (0x00007ffc187bd000)
    libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007f1a39a92000)
    libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007f1a396f4000)
    libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007f1a394d5000)
    libgcc_s.so.1 => /lib/x86_64-linux-gnu/libgcc_s.so.1 (0x00007f1a392bd000)
    libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f1a38ecc000)
    /lib64/ld-linux-x86-64.so.2 (0x00007f1a39c96000)
```

Ok, so now we know which shared libraries we need! `linux-vdso.so.1` is a special library - it is directly provided by the Linux kernel. So we only need to care about the others.

I copied the entire Ubuntu content over to my host, and I created 2 empty directories `lib` and `lib64` and copied over only the mentioned files and symbolic links from the original Ubuntu image. Let's see how this looks:

![File structure of lib and lib64 directories](https://thepracticaldev.s3.amazonaws.com/i/g5g0qqyv850r1zhs2slz.png)

Now we can modify our `shrinked.Dockerfile` and add those libs:

```docker
FROM scratch
COPY lib /lib
COPY lib64 /lib64
COPY hello_world_linux.kexe /hello_world
ENTRYPOINT ["/hello_world"]
```

Let's see if we can now build and run this!

```console
$ docker build --tag hello_kotlin_shrinked . --file shrinked.Dockerfile
[…]
Successfully tagged hello_kotlin_shrinked:latest

$ docker run --rm hello_kotlin_shrinked
Hello, World!

$ docker images 'hello_kotlin*'
REPOSITORY              TAG                 IMAGE ID            CREATED              SIZE
hello_kotlin_shrinked   latest              a66abb1efa53        About a minute ago   4.54MB
hello_kotlin            latest              47b7a9b05993        3 hours ago          64.6MB
```

Wow, that's cool - we now have a Docker image of **4.54MB** in size that runs a Kotlin Native binary successfully. Party time! 🎉🎆🎄

## Conclusion

We looked at how to cross-compile a Kotlin program for Linux. Then we created a Docker image, using that program as an entrypoint. By leaving the JVM behind, we could already produce a Docker image of 64.6MB, most of which coming from the Ubuntu image itself.

Then, we entered the danger zone and looked at how we can strip most things of Ubuntu away. We included only the necessary libraries, and that way we got the image down to 4,54MB.

Some final note: If you compile your own (real) Kotlin Native program, you probably need more files than you might think. Using https connections? I bet you'll need the SSL root certificates provided in each Linux distro. Using some Kotlin Native libs? Maybe some of those add references to other native `.so` libs, and your program won't start otherwise. So always test sensibly!

**If you liked this post, please give it a ❤️ and follow me on [Twitter](https://twitter.com/mreichelt)!**
I would love to know more if you use this somewhere. Also, I'm pretty sure there must be some tools / tricks out there that would help in this matter. If you know something I don't: please write! 🙌

*Cover photo by [Denise Johnson](https://unsplash.com/@auntneecey) on [Unsplash](https://unsplash.com/photos/m4eISQg0e0E).*