Custom JRE for Clojure app distribution

Modular Java was introduced in JDK 9, and considered to be the largest architectural change in Java, ever. The core of Java libraries were split into several modules, sits in $JAVA_HOME/jmods. The bloated, monolithic rt.jar is no more.

Modular Java requires you to include a module-info.java for a module. As of Clojure 1.9, there is no plan (AFAIK) for module support. However, we can still benefit from the new architecture. By using the jlink tool, we can create a customized Java runtime, contains certain modules. The minimal one, which has java.base module, is only 29MB. And this JRE, is capable for running most of Clojure application. You can still use its java executable as before.

I created lein-jlink to manage these kind of customized JRE for clojure development. Put lein-jlink in :plugins of your project.clj:

(defproject jlinktest "0.1.0-SNAPSHOT"
  ...
  :plugins [lein-jlink "0.2.0-SNAPSHOT"])

By running lein jlink init, a default (minimal) JRE is created in target/default/jlink. (the target path contains your profile name)

Now, adding a hello world ring app to our application:

in project.clj

:dependencies [[org.clojure/clojure "1.9.0"]
              [info.sunng/ring-jetty9-adapter "0.10.4"]]

in src/jlinktest/core.clj:

(ns jlinktest.core
  (:gen-class)
  (:require [ring.adapter.jetty9 :refer [run-jetty]]))

(defn app [req]
  "<h1>It works</h1>")

(defn -main
  "I don't do a whole lot ... yet."
  [& args]
  (run-jetty app {:port 8080}))

This app can be run with lein jlink run, which uses the java executable from our customized JRE.

$ lein jlink run
Defaulting Uptime to NOIMPL due to (java.lang.UnsupportedOperationException) Implementation not available in this environment
2018-01-28 23:51:12.571:INFO::main: Logging initialized @-1ms to org.eclipse.jetty.util.log.StdErrLog
2018-01-28 23:51:12.695:INFO:oejs.Server:main: jetty-9.4.8.v20171121, build timestamp: 2017-11-22T05:27:37+08:00, git hash: 82b8fb23f757335bb3329d540ce37a2a2615f0a8
2018-01-28 23:51:12.742:INFO:oejs.AbstractConnector:main: Started ServerConnector@2e645fbd{HTTP/1.1,[http/1.1]}{0.0.0.0:8080}
2018-01-28 23:51:12.744:INFO:oejs.Server:main: Started @-1ms

This environment has no java.util.logging package so you can see Jetty is fallback to stderr for logging. But it still fully functional on serving http requests.

To visualize the benefit on distributing our Clojure app, we can assemble a distributable environment, using lein jlink assemble. It runs lein uberjar and copy the jar into JRE directory, then put a shortcut script for running our app.

If you care about the size of your application, it's only 37MB totally for this hello world ring web application, includes runtime.

This path can be added into a docker base image, like ubuntu:

FROM ubuntu:16.04

ADD target/default/jlink /opt/jlinktest
ENTRYPOINT /opt/jlinktest/bin/jlinktest

No need for installing openjdk-8-jre. The result image size is 159MB, the ubuntu base takes 121MB from it.

Note that you may want to run it on a minimal base like alpine. alpine is based on musl-libc. If your development environment is glibc based (in most this is true), it won't work on alpine. We will need to use a glibc variant and add required packages:

FROM frolvlad/alpine-glibc

RUN apk add --no-cache libstdc++

ADD target/default/jlink /opt/jlinktest
ENTRYPOINT /opt/jlinktest/bin/jlinktest

This runnable image size is, just 50.7MB!

So with jlink and modular Java, you can ship your Clojure app with this minimal runtime. This could change Java ecosystem and enable even more use cases for it.