DEV Community

Diogo Souza da Silva
Diogo Souza da Silva

Posted on

Efficient Clojure multistage docker images, with java and native-image

Here I explore a few optimization when building docker images for your clojure apps.

Image versions

One easy way to make it faster for you local development and for CI/CD is to just use smaller images, and to reuse images.

Using common public images make it more likely that you will use the same image over and over again, also pinning to the most specific version help assure the base image have not changed between builds. Choosing alpine or slim images can reduce the image size.

For the base image I use clojure:openjdk-13-tools-deps-slim-buster and openjdk:13-slim-buster. I prefer buster images over alpine due to compatibility with most native libs, and rumor has it that due to libc versions it can be faster.

Build cache

The next step is to leverage the docker image build cache, so the order of the steps you use for building the image matter.

You generally want to set non-changing configurations like ENV, WORKING_DIR and EXPOSE first.

To decouple installing the deps from actually building the artifact, the next thing you add is your deps files and install it.

The code is what changes most, so it goes last, right before actually building the uberjar.

Garbage collector and heap size

Not optimization of images but a tip.

Current versions of openjdk support running in containers, so it is best to use relative memory limits and container support with -XX:+UseContainerSupport and -XX:MaxRAMPercentage=85 just we don`t have to mess with Xmx or Xms at runtime anymore.

Remember that the JVM uses a little more memory than the heap, so give it some extra space.

Multistage

To further reduce the image size and remove clutter from base image, we can start with a JVM only image and copy the generated jar over. This will remove clojure specific tools, sources-code, intermediate artifacts and others.

Resulting Dockerfile

After applying these tips, here is the resulting Dockerfile:

Native image

Now, for bonus, we can also setup a native image using graalvm tools. This will reduce image size by a lot, as it does not depend on the JVM, and potentially reduce memory usage.

Note that native-image is only compatible with linux x86-64, and is new tech, so a lot o frameworks can break it. It also does not give better performance (latency, throughput, gc times…) comparing to JVM version.
Some flags may change depending on your tools of choice.

The native image will build upon the previous tips, but has to be based of java 11 instead of latest.

Here is the dockerfile:

Note that there is a lot of netty specific config there, as I use aleph for HTTP.

Opensource full example

All this experiments and other framework choices are available at my github klj-api project.

Hope it helped.

Top comments (0)