diff --git a/.github/workflows/tiny-java-containers.yml b/.github/workflows/tiny-java-containers.yml index fdb1ab08d..affa938c1 100644 --- a/.github/workflows/tiny-java-containers.yml +++ b/.github/workflows/tiny-java-containers.yml @@ -30,6 +30,10 @@ jobs: # ./setup-musl.sh # + # Download upx + # + ./setup-upx.sh + # # Hello World # cd helloworld diff --git a/tiny-java-containers/README.md b/tiny-java-containers/README.md index c91e4fa4a..70062ead1 100644 --- a/tiny-java-containers/README.md +++ b/tiny-java-containers/README.md @@ -3,7 +3,7 @@ This example shows 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 +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. @@ -20,21 +20,37 @@ App](images/youtube.png)](https://youtu.be/6wYrAtngIVo) ## Prerequisites +* x86 Linux (but the few binary dependencies could easily be changed for aarch64) * Docker installed and running. It should work fine with [podman](https://podman.io/) but it has not been tested. * [GraalVM for JDK 21](https://www.graalvm.org/downloads/) > We recommend Oracle GraalVM for the best experience. It is licensed under the [GraalVM Free Terms and Conditions (GFTC)](https://www.oracle.com/downloads/licenses/graal-free-license.html) license, which permits use by any user including commercial and production use. -GraalVM Community Edition JDK 21 works too, but Native Image generated executables sizes will differ. +GraalVM Community Edition for JDK 21 works too, but Native Image generated executables sizes will differ. > These instructions have only been tested on Linux x64. ## Setup +You need the following zlib packages installed: +* zlib.x86_64 +* zlib-devel.x86_64 +* zlib-static.x86_64 + +On Oracle Linux, you can install with: +```sh +sudo yum install -y zlib.x86_64 +sudo yum install -y zlib-devel.x86_64 +sudo yum install -y zlib-static.x86_64 +``` + Clone this Git repo and in your Linux shell type the following to download and configure the `musl` toolchain. ![](images/keyboard.jpg) `./setup-musl.sh` +Download [upx](https://upx.github.io/): + +![](images/keyboard.jpg) `./setup-upx.sh` ## Hello World @@ -62,7 +78,8 @@ 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`: + `--static --libc=musl` options is a fully self-contained executable which can be + confirmed by examining it with `ldd`: ![](images/keyboard.jpg) `ldd hello` @@ -80,22 +97,26 @@ noting: executable, you can be confident it is also statically linked. 2. 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 + native machine code. The uncompressed executable is only ~6.3MB! There's no JVM, no JARs, no JIT compiler and none of the overhead it imposes. Both start extremely fast as there is minimal startup cost. -3. The `upx` compressed executable is about 60% smaller, 1.5MB vs. 5.2MB! With - `upx`` the application self-extracts quickly but does incur a cost of about - 100ms for decompression. See this blog for a deep dive on [GraalVM Native Image and UPX](https://medium.com/graalvm/compressed-graalvm-native-images-4d233766a214). +3. The `upx` compressed executable is over 70% smaller, 1.7MB vs. 6.3MB! With + `upx` the application self-extracts quickly but does incur a cost of about + 100ms for decompression. See this blog for a deep dive on [GraalVM Native + Image and + UPX](https://medium.com/graalvm/compressed-graalvm-native-images-4d233766a214). ### Container Images The size of the `scratch`-based container image is slightly more than the `hello.upx` executable. +![](images/keyboard.jpg) `docker images hello` + ```shell -REPOSITORY TAG IMAGE ID CREATED SIZE -hello upx 935e5e3549e6 1 second ago 1.51MB +REPOSITORY TAG IMAGE ID CREATED SIZE +hello upx 4d122bd39a8a About a minute ago 1.78 MB ``` This is a tiny container image and yet it contains a fully functional and @@ -167,29 +188,29 @@ When complete you can see the sizes of the various versions: ```shell REPOSITORY TAG IMAGE ID CREATED SIZE -jwebserver distroless-java-base.jlink fae0bb62eca7 6 minutes ago 74.9MB -jwebserver scratch.static-upx 676069a2a359 6 minutes ago 5.43MB -jwebserver alpine.static 14e748264a99 6 minutes ago 25MB -jwebserver distroless-static.static 5591e1a2658a 6 minutes ago 21.8MB -jwebserver scratch.static ef1ad68037ec 7 minutes ago 19.4MB -jwebserver distroless-base.mostly cc8612887001 7 minutes ago 39.7MB -jwebserver distroless-java-base.dynamic d2f802cf3def 8 minutes ago 58.7MB +jwebserver distroless-java-base.jlink 414d84f8b7c7 22 minutes ago 132 MB +jwebserver scratch.static-upx 47aabdd14c04 22 minutes ago 4.71 MB +jwebserver alpine.static 783ab3a60248 22 minutes ago 23.4 MB +jwebserver distroless-static.static c894f14d4068 22 minutes ago 18.7 MB +jwebserver scratch.static 034cfbdf3577 22 minutes ago 15.7 MB +jwebserver distroless-base.mostly e99811e574d3 22 minutes ago 37.6 MB +jwebserver distroless-java-base.dynamic 72a210e3c705 23 minutes ago 50.6 MB ``` Sorting by size, it's clear that the fully statically linked GraalVM Native -Image generated executable that's compressed and packaged on `scratch` is the -smallest at just 5.43MB, only 7% of the size of the `jlink` version running on -the JVM. +Image generated executable that's compressed and packaged on `scratch` +(`scratch.static-upx`) is the smallest at just 4.71MB, less than 4% of the size +of the `jlink` version (`distroless-java-base.jlink`) running on the JVM. | Base Image | App Version | Size (MB) | | -------------------- | ---------------------------------- | --------- | -| Distroless Java Base | jlink | 74.90 | -| Distroless Java Base | native dynamic linked | 58.70 | -| Distroless Base | native *mostly* static linked | 39.70 | -| Alpine | native *fully* static | 25.00 | -| Distroless Static | native *fully* static | 21.80 | -| Scratch | native *fully* static | 19.40 | -| Scratch | *compressed* native *fully* static | 5.43 | +| Distroless Java Base | jlink | 132.00 | +| Distroless Java Base | native *dynamic* linked | 50.60 | +| Distroless Base | native *mostly* static linked | 37.60 | +| Alpine | native *fully* static linked | 23.40 | +| Distroless Static | native *fully* static linked | 18.70 | +| Scratch | native *fully* static linked | 15.70 | +| Scratch | *compressed* native *fully* static | 4.71 | Running a container image is straight forward, just remember to map the ports, e.g.: @@ -204,10 +225,11 @@ the index.html file. ## Wrapping Up -Fully functional, albeit minimal, Java "microservice" was compiled +A fully functional, albeit minimal, Java "microservice" was compiled into a native Linux executable and packaged into Distroless, Alpine, and `scratch`-based container images thanks to GraalVM Native Image's support for various linking options including fully static linking with the `musl` libc. To learn more about linking options check out [Static and Mostly Static -Images](https://www.graalvm.org/latest/reference-manual/native-image/guides/build-static-executables/) in the GraalVM docs. \ No newline at end of file +Images](https://www.graalvm.org/latest/reference-manual/native-image/guides/build-static-executables/) +in the GraalVM docs. \ No newline at end of file diff --git a/tiny-java-containers/clean.sh b/tiny-java-containers/clean.sh index 2319bcc1b..b12d90e89 100755 --- a/tiny-java-containers/clean.sh +++ b/tiny-java-containers/clean.sh @@ -1,7 +1,9 @@ #!/bin/sh -set -x +set +e rm -rf x86_64-linux-musl-native zlib-* -./helloworld/clean.sh -./jwebserver/clean.sh \ No newline at end of file +cd helloworld +./clean.sh || true +cd ../jwebserver +./clean.sh || true \ No newline at end of file diff --git a/tiny-java-containers/helloworld/build.sh b/tiny-java-containers/helloworld/build.sh index 4c543e0e9..bdf27515e 100755 --- a/tiny-java-containers/helloworld/build.sh +++ b/tiny-java-containers/helloworld/build.sh @@ -1,20 +1,20 @@ #!/bin/sh +set -e + TOOLCHAIN_DIR=`pwd`/../x86_64-linux-musl-native CC=${TOOLCHAIN_DIR}/bin/gcc PATH=${TOOLCHAIN_DIR}/bin:${PATH} -set -x - # Compile Java source file javac Hello.java # Compile Java bytecodes into a fully statically linked executable -native-image --static --libc=musl -o hello Hello -rm *.txt +native-image -Ob --static --libc=musl -o hello Hello +rm -rf *.txt # Create a compressed version of the executable -upx --lzma --best hello -o hello.upx +../upx --lzma --best hello -o hello.upx # Package the compressed executable in a simple scratch container image docker build . -t hello:upx \ No newline at end of file diff --git a/tiny-java-containers/helloworld/clean.sh b/tiny-java-containers/helloworld/clean.sh index d0251edcd..67c85f9bb 100755 --- a/tiny-java-containers/helloworld/clean.sh +++ b/tiny-java-containers/helloworld/clean.sh @@ -1,6 +1,8 @@ #!/bin/sh -rm hello hello.upx -rm *.txt -rm *.class -docker images -q hello | awk '{print($1)}' | xargs docker rmi +set +e + +rm -rf hello hello.upx +rm -rf *.txt +rm -rf *.class +docker images -q hello | awk '{print($1)}' | xargs docker rmi || true diff --git a/tiny-java-containers/jwebserver/Dockerfile.alpine.static b/tiny-java-containers/jwebserver/Dockerfile.alpine.static index 12e67d22a..531014b67 100644 --- a/tiny-java-containers/jwebserver/Dockerfile.alpine.static +++ b/tiny-java-containers/jwebserver/Dockerfile.alpine.static @@ -1,4 +1,4 @@ -FROM alpine +FROM alpine:3 COPY jwebserver.static / COPY index.html /web/index.html EXPOSE 8000 diff --git a/tiny-java-containers/jwebserver/Dockerfile.distroless-base.mostly b/tiny-java-containers/jwebserver/Dockerfile.distroless-base.mostly index 71eef355a..6eafde0f3 100644 --- a/tiny-java-containers/jwebserver/Dockerfile.distroless-base.mostly +++ b/tiny-java-containers/jwebserver/Dockerfile.distroless-base.mostly @@ -1,4 +1,4 @@ -FROM gcr.io/distroless/base-debian11 +FROM gcr.io/distroless/base-debian12 COPY jwebserver.mostly / COPY index.html /web/index.html EXPOSE 8000 diff --git a/tiny-java-containers/jwebserver/Dockerfile.distroless-java-base.dynamic b/tiny-java-containers/jwebserver/Dockerfile.distroless-java-base.dynamic index 35479f7c6..3bd4fd105 100644 --- a/tiny-java-containers/jwebserver/Dockerfile.distroless-java-base.dynamic +++ b/tiny-java-containers/jwebserver/Dockerfile.distroless-java-base.dynamic @@ -1,4 +1,4 @@ -FROM gcr.io/distroless/java-base-debian11 +FROM gcr.io/distroless/java-base-debian12 COPY jwebserver.dynamic / COPY index.html /web/index.html EXPOSE 8000 diff --git a/tiny-java-containers/jwebserver/Dockerfile.distroless-java-base.jlink b/tiny-java-containers/jwebserver/Dockerfile.distroless-java-base.jlink index 0acb508e6..fe737c7c6 100644 --- a/tiny-java-containers/jwebserver/Dockerfile.distroless-java-base.jlink +++ b/tiny-java-containers/jwebserver/Dockerfile.distroless-java-base.jlink @@ -1,4 +1,4 @@ -FROM gcr.io/distroless/java-base-debian11 +FROM gcr.io/distroless/java-base-debian12 COPY jwebserver-jlink /usr/lib/java COPY index.html /web/index.html EXPOSE 8000 diff --git a/tiny-java-containers/jwebserver/Dockerfile.distroless-static.static b/tiny-java-containers/jwebserver/Dockerfile.distroless-static.static index e1b02d502..d36c12ac1 100644 --- a/tiny-java-containers/jwebserver/Dockerfile.distroless-static.static +++ b/tiny-java-containers/jwebserver/Dockerfile.distroless-static.static @@ -1,4 +1,4 @@ -FROM gcr.io/distroless/static-debian11 +FROM gcr.io/distroless/static-debian12 COPY jwebserver.static / COPY index.html /web/index.html EXPOSE 8000 diff --git a/tiny-java-containers/jwebserver/build-all.sh b/tiny-java-containers/jwebserver/build-all.sh index 899180b8f..1d80247f1 100755 --- a/tiny-java-containers/jwebserver/build-all.sh +++ b/tiny-java-containers/jwebserver/build-all.sh @@ -1,6 +1,4 @@ -#!/bin/sh - -set -x +#!/bin/sh ./build-dynamic.sh ./build-mostly.sh diff --git a/tiny-java-containers/jwebserver/build-dynamic.sh b/tiny-java-containers/jwebserver/build-dynamic.sh index 404ee16ac..730a9bd0d 100755 --- a/tiny-java-containers/jwebserver/build-dynamic.sh +++ b/tiny-java-containers/jwebserver/build-dynamic.sh @@ -1,7 +1,7 @@ #!/bin/sh # compile with fully dynamically linked shared libraries -native-image -m jdk.httpserver -o jwebserver.dynamic +native-image -Ob -m jdk.httpserver -o jwebserver.dynamic # Distroless Java Base-provides glibc and other libraries needed by the JDK docker build . -f Dockerfile.distroless-java-base.dynamic -t jwebserver:distroless-java-base.dynamic diff --git a/tiny-java-containers/jwebserver/build-jlink.sh b/tiny-java-containers/jwebserver/build-jlink.sh index 5fc07b70c..bfc40b675 100755 --- a/tiny-java-containers/jwebserver/build-jlink.sh +++ b/tiny-java-containers/jwebserver/build-jlink.sh @@ -6,7 +6,7 @@ jlink \ --add-modules jdk.httpserver \ --verbose \ --strip-debug \ - --compress 2 \ + --compress zip-9 \ --no-header-files \ --no-man-pages \ --strip-java-debug-attributes \ diff --git a/tiny-java-containers/jwebserver/build-mostly.sh b/tiny-java-containers/jwebserver/build-mostly.sh index 2ab74124d..2a0db4092 100755 --- a/tiny-java-containers/jwebserver/build-mostly.sh +++ b/tiny-java-containers/jwebserver/build-mostly.sh @@ -1,6 +1,6 @@ #!/bin/sh -native-image -H:+StaticExecutableWithDynamicLibC -m jdk.httpserver -o jwebserver.mostly +native-image -Ob -H:+UnlockExperimentalVMOptions -H:+StaticExecutableWithDynamicLibC -m jdk.httpserver -o jwebserver.mostly # Distroless Base (provides glibc) docker build . -f Dockerfile.distroless-base.mostly -t jwebserver:distroless-base.mostly diff --git a/tiny-java-containers/jwebserver/build-static.sh b/tiny-java-containers/jwebserver/build-static.sh index a23ed8f4a..34b92a47a 100755 --- a/tiny-java-containers/jwebserver/build-static.sh +++ b/tiny-java-containers/jwebserver/build-static.sh @@ -4,7 +4,7 @@ TOOLCHAIN_DIR=`pwd`/../x86_64-linux-musl-native CC=${TOOLCHAIN_DIR}/bin/gcc PATH=${TOOLCHAIN_DIR}/bin:${PATH} -native-image --static --libc=musl -m jdk.httpserver -o jwebserver.static +native-image -Ob --static --libc=musl -m jdk.httpserver -o jwebserver.static # Scratch-nothing docker build . -f Dockerfile.scratch.static -t jwebserver:scratch.static @@ -17,7 +17,7 @@ docker build . -f Dockerfile.alpine.static -t jwebserver:alpine.static # Compress with UPX rm -f jwebserver.static-upx -upx --lzma --best -o jwebserver.static-upx jwebserver.static +../upx --lzma --best -o jwebserver.static-upx jwebserver.static # Scratch--fully static and compressed docker build . -f Dockerfile.scratch.static-upx -t jwebserver:scratch.static-upx \ No newline at end of file diff --git a/tiny-java-containers/jwebserver/clean.sh b/tiny-java-containers/jwebserver/clean.sh index a150bfbfd..da4bd3c0d 100755 --- a/tiny-java-containers/jwebserver/clean.sh +++ b/tiny-java-containers/jwebserver/clean.sh @@ -1,5 +1,8 @@ #!/bin/sh +set +e + rm -rf jwebserver-jlink/ rm jwebserver.dynamic jwebserver.mostly jwebserver.static jwebserver.static-upx +rm -rf svm*.md docker images jwebserver -q | grep -v TAG | awk '{print($1)}' | xargs docker rmi \ No newline at end of file diff --git a/tiny-java-containers/setup-musl.sh b/tiny-java-containers/setup-musl.sh index 1e6f56b27..6759d6bbb 100755 --- a/tiny-java-containers/setup-musl.sh +++ b/tiny-java-containers/setup-musl.sh @@ -1,5 +1,7 @@ #!/bin/sh +set -e + ZLIB_VERSION=1.2.13 TOOLCHAIN_DIR=`pwd`/x86_64-linux-musl-native diff --git a/tiny-java-containers/setup-upx.sh b/tiny-java-containers/setup-upx.sh new file mode 100755 index 000000000..04d93d1c8 --- /dev/null +++ b/tiny-java-containers/setup-upx.sh @@ -0,0 +1,13 @@ +#!/bin/sh + +set -e + +UPX_VERSION=4.2.2 +UPX_ARCHIVE=upx-${UPX_VERSION}-amd64_linux.tar.xz + +wget -q https://github.com/upx/upx/releases/download/v${UPX_VERSION}/${UPX_ARCHIVE} +tar -xJf ${UPX_ARCHIVE} +rm -rf ${UPX_ARCHIVE} +mv upx-${UPX_VERSION}-amd64_linux/upx . +rm -rf upx-${UPX_VERSION}-amd64_linux +