DEV Community

Diogo Souza da Silva
Diogo Souza da Silva

Posted on

3

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:

FROM clojure:openjdk-13-tools-deps-slim-buster as builder
WORKDIR /usr/src/app
# install cambada builder, as it will not change
RUN clj -Sdeps '{:deps {luchiniatwork/cambada {:mvn/version "1.0.0"}}}' -e :ok
# install main deps, sometimes change
COPY deps.edn /usr/src/app/deps.edn
RUN clj -e :ok
# add files and build, change often
COPY resources/ /usr/src/app/resources
COPY src/ /usr/src/app/src
RUN clj -A:uberjar
# use clean base image
FROM openjdk:13-slim-buster
# set static config
ENV PORT 8080
EXPOSE 8080
# set the command, with proper container support
CMD ["java","-XX:+UseContainerSupport","-XX:MaxRAMPercentage=85","-XX:+UnlockExperimentalVMOptions","-XX:+UseZGC","-jar","/usr/src/app/app.jar"]
# copy the ever changing artifact
COPY --from=builder /usr/src/app/target/app-1.0.0-SNAPSHOT-standalone.jar /usr/src/app/app.jar
view raw Dockerfile hosted with ❤ by GitHub

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:

FROM clojure:openjdk-11-tools-deps as builder
WORKDIR /usr/src/app
RUN clj -Sdeps '{:deps {luchiniatwork/cambada {:mvn/version "1.0.0"}}}' -e :ok
COPY deps.edn /usr/src/app/deps.edn
RUN clj -e :ok
COPY resources/ /usr/src/app/resources
COPY src/ /usr/src/app/src
RUN clj -A:uberjar# the native-image tool
FROM oracle/graalvm-ce:19.3.1-java11 as native
RUN gu install native-image
WORKDIR /usr/src/app
COPY --from=builder /usr/src/app/target/app-1.0.0-SNAPSHOT-standalone.jar /usr/src/app/app.jar# lots of specific flags
RUN native-image --allow-incomplete-classpath --static --enable-http --no-fallback --no-server --initialize-at-build-time --report-unsupported-elements-at-runtime --initialize-at-run-time=io.netty.channel.epoll.EpollEventArray,io.netty.channel.unix.Errors,io.netty.channel.unix.IovArray,io.netty.channel.unix.Socket,io.netty.channel.epoll.Native,io.netty.channel.epoll.EpollEventLoop,io.netty.util.internal.logging.Log4JLogger -jar app.jar app
# use clean image
FROM scratch
COPY --from=native /usr/src/app/app /app
CMD ["/app"]

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.

AWS GenAI LIVE image

Real challenges. Real solutions. Real talk.

From technical discussions to philosophical debates, AWS and AWS Partners examine the impact and evolution of gen AI.

Learn more

Top comments (0)

Billboard image

Create up to 10 Postgres Databases on Neon's free plan.

If you're starting a new project, Neon has got your databases covered. No credit cards. No trials. No getting in your way.

Try Neon for Free →

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay