Recently I wanted to create a docker image with Java 17. Since it is a very recent version, only a few images are available and it is not the smallest ones. To keep a light base image I created one using alpine as a builder image and SCRATCH as a base image for the final one:

ARG DISTRIBUTION=zulu17.28.13-ca-jre17.0.0-linux_musl_x64

(1)
FROM alpine:3.14 as builder
ARG DISTRIBUTION
ENV LANG='en_US.UTF-8' LANGUAGE='en_US:en' LC_ALL='en_US.UTF-8'
RUN wget --quiet "https://cdn.azul.com/zulu/bin/${DISTRIBUTION}.tar.gz" && \
    mkdir -p /opt/jre && \
    tar xf "${DISTRIBUTION}.tar.gz" --strip 1 -C /opt/jre

(2)
FROM scratch
ENV LANG='en_US.UTF-8' LANGUAGE='en_US:en' LC_ALL='en_US.UTF-8' JAVA_HOME=/opt/jre
ENV PATH="/opt/jre/bin:${PATH}"
COPY --from=builder /lib/ld-musl-x86_64.so.1 /lib/ld-musl-x86_64.so.1
COPY --from=builder /lib/libz.so.1 /lib/libz.so.1
COPY --from=builder /opt/jre /opt/jre
CMD [ "/opt/jre/bin/java", "--version" ]
1 The builder image just downloads and unpack the JRE in /opt/jre
2 The final image copies from the builder image the JRE, indeed, but also musl and libz (its dependencies).

This docker image works very well but when starting to run real applications I got weird issues:

java.io.IOException: Can't create temporary file /tmp/foo

This happent in a Kubernetes deployment so was not that easy to debug. So I created a local version of the image, ran it and it really failed.

Investigating I realized the /tmp folder was not existing so the default of the JVM was not working. Instead of setting java.io.tmpdir, I decided to add the folder in the image (empty). This enabled to move forward and run part of the applications but not the one using subfolders in /tmp and more important, not the ones running in Kubernetes without the same user than the one in the docker container (likely user 0, ie root using a COPY to create /tmp. You can COPY --chown to force a particular user but it just moves the problem since you don’t always know which one will be used for sub-images or Kubernetes deployments.

At that stage, I was thinking the issue was with Java so I digged into the source code and for Unix implementaton you end up on a native code:

#include <stat/stat.h>

...

mkdir(path, 0777);

To check I wrote a quick C++ main just trying to create a not existing folder with the previous setup - not the expected user - and it failed indeed, so the issue is really in the base image, not Java platform.

A quick and efficient fix is to change the final image to use alpine too:

FROM alpine:3.14
ENV LANG='en_US.UTF-8' LANGUAGE='en_US:en' LC_ALL='en_US.UTF-8' JAVA_HOME=/opt/jre
ENV PATH="/opt/jre/bin:${PATH}"
COPY --from=builder /opt/jre /opt/jre
CMD [ "/opt/jre/bin/java", "--version" ]

This simpler version - no need to copy libs since they are there in the base image - works perfectly. Indeed, it adds a layer to my final image (alpine:3.14) but I don’t have to worry about the fact the application creates or not temporary files. The added size is a few megs so it is really worth starting from a real distribution rather than SCRATCH until you are sure no temporary files are created.

Last point is I hit that for Java applications, but it is true for all languages indeed so don’t think that because you Graalified, JLinked your Java application or because you use Go, C++, NodeJS that you can’t hit it ;).

From the same author:

In the same category: