An example showing how a simple Java application can be compiled to produce a very small Docker container image.
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, the 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 last tested with version 22.1. 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.
Next 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 using the provided script:
Running either of the 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 executableUnfortunately 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 straightforward:
% docker run --rm hello:static
Hello WorldHello WorldThere you have it. A fully functional, albeit minimal, Java application
compiled into a native Linux executable and packaged into a scratch 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.
