Docker SCRATCH base image mkdir issue
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: