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.
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.
Clone this Git repo and in your shell type the following to download and
configure the musl toolchain.
With the musl toolchain installed and on the Linux PATH, cd in to the helloworld folder.
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:
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:
- The executable generated by GraalVM Native Image using the
--static --libc=musloptions is a fully self-contained executable which can be confirmed by examining it withldd:
should result in:
not a dynamic executableThis 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.
- 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.
- The
upxcompressed 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.
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.45MBThese 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 WorldHello WorldContainerizing 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.
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:
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.upxThis 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.
The sizes of the scratch-based container images are again in proportion to the
executables.
REPOSITORY TAG IMAGE ID CREATED SIZE
jwebserver upx ddc3fc744630 1 second ago 5.45MB
jwebserver static 8e2434a288af 11 seconds ago 19.4MBThe 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.
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.