tiny-java-containers

An couple of examples showing how a simple Java application and a simple web server can be compiled to produce very small Docker container images.

The smallest container images contains just an executable. But since there's nothing in the container image except the executable, including no libc or other shared libraries, an executable has to be fully statically linked with all needed libraries and resources.

To support static linking libc, GraalVM Native Image supports using the "lightweight, fast, simple, free" musl libc implementation.

NOTE: GraalVM Native Image also supports dynamically linked and "mostly static" executables not described here.

Prerequisites

You'll need GraalVM Native Image installed. This code was tested with a preview build of GraalVM 22.3 for JDK 19. You'll also need Docker installed and running. It should work fine with podman but it has not been tested.

These instructions have only been tested on Linux amd64.

Setup

Clone this Git repo and in your shell type the following to download and configure the musl toolchain.

$ source setup-musl.sh

Hello World

With the musl toolchain installed and on the Linux PATH, cd in to the helloworld folder.

cd helloworld

Using the build-hello.sh script, compile a simple single Java class Hello World application with javac, compile the generated .class file into a fully statically linked native Linux executable, compress the executable with upx, and package both the static executable and the compressed executable into scratch Docker container images:

$ ./build-hello.sh

The Executables

Running either of the hello executable you can see they are functionally equivalent. They just print "Hello World". But there are a few points worth noting:

  1. The executable generated by GraalVM Native Image using the --static --libc=musl options is a fully self-contained executable which can be confirmed by examining it with ldd:

$ ldd hello.static

should result in:

	not a dynamic executable

This means that it does not rely on any libraries in the host operating system environment making it easier to package in a variety of Docker container images.

Unfortunately upx compression renders ldd unable to list the shared libraries of an executable, but since we compressed the statically linked executable we can be confident it is also statically linked.

  1. Both executables are the result of compiling a Java bytecode application into native machine code. The uncompressed executable is only 5.2MB! There's no JVM, no jars, no JIT compiler and none of the overhead it imposes. Both start extremely fast as there is effectively no startup cost.
  2. The upx compressed executable is about 60% smaller, 1.5MB vs. 5.2MB! With upx the application self-extracts but so quickly as to have minimal impact on startup time.

Container Images

The sizes of the scratch-based container images are in proportion to the executables.

REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
hello               upx                 935e5e3549e6        1 second ago        1.51MB
hello               static              4d41b253b760        4 seconds ago       5.45MB

These are tiny container images and yet they contain fully functional and deployable (although fairly useless 😉) applications.

The Dockerfiles that generated them simply copy the executable into the container image and set the executable as the ENTRYPOINT. E.g.,

FROM scratch
COPY hello.upx /
ENTRYPOINT ["/hello.upx"]

Running them is straight forward:

$ docker run --rm hello:static

Hello World

$ docker run --rm hello:upx

Hello World

A Simple Web Server

Containerizing Hello World is not that interesting so let's move on to something you could deploy as a service. We'll take the Simple Web Server introduced in JDK 18 and build a containerized executable that serves up web pages.

How small can a containerized Java web server be? Would you believe a measly 5.5MB? Let's see.

Let's move from the helloworld folder over to the jwebserver folder.

cd ../jwebserver

Using the build-jwebserver.sh script, compile the jdk.httpserver module (with a default main that start the server) with GraalVM Native Image into a fully statically linked native Linux executable, compress the executable with upx, and package both the static executable and the compressed executable into scratch Docker container images, just like we did in the Hello World example:

$ ./build-jwebserver.sh

The Executables

As before, we'll produce two executables, one fully statically linked and that same executable compressed with upx.

$ ls -lh jwebserver.static jwebserver.upx

-rwxrwxr-x. 1 opc opc  19M Sep 15 17:00 jwebserver.static
-rwxrwxr-x. 1 opc opc 5.2M Sep 15 17:00 jwebserver.upx

This time the upx compressed executable is about 75% smaller, 5.2MB vs. 19MB!

Running either one will start a web server listening on port 8000. It will server up the index.html file in the current directory you can fetch using curl, wget, or a browser.

Container Images

The sizes of the scratch-based container images are again in proportion to the executables.

docker images jwebserver

REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
jwebserver          upx                 ddc3fc744630        1 second ago        5.45MB
jwebserver          static              8e2434a288af        11 seconds ago      19.4MB

The Dockerfiles that generated them simply copy the executable into the container image and set the executable as the ENTRYPOINT. E.g.,

FROM scratch
COPY jwebserver.upx /
COPY index.html /web/index.html
ENTRYPOINT ["/jwebserver.upx", "-b", "0.0.0.0", "-d", "/web"]

Running them is straight forward:

$ docker run --rm -p8000:8000 jwebserver:static

or

$ docker run --rm -p8000:8000 jwebserver:upx

Using your favourite tools you can hit http://localhost:8000 to fetch the index.html file.

Wrapping Up

There you have it. Fully functional, albeit minimal, Java applications compiled into native Linux executables and packaged into scratch-based container image thanks to GraalVM Native Image's support for fully static linking with the musl libc.

To explore other linking options compatible with other base container images check out Static and Mostly Static Images in the GraalVM docs.