Linux Alpine docker image is based on super tiny Linux kernel BusyBox and features and package manager, apk, making easy to install build dependencies. This is particularly important for static compile languages like haskell and go as example but also for java and scala.
Scala is also a statically compiled language for the jvm and although we think of it as requiring just the jdk the typical way to build a scala application is with sbt which requires the jvm but the build also downloads a considerable number of sbt core dependencies. Hence, the safe and reliable way for reproducing build a project anywhere is to start from equal sbt installation. The best way to ensure that is to start from a pre-loaded sbt docker image.
Ideally we shouldn't deploy the build system but just the artifact in a reliable and reproducible way. Here, I'll describe two ways to do just that.
FROM takari-oss-alpine
WORKDIR /root
ARG SBT_VERSION
ENV SBT_HOME=/usr/local/sbt
ENV PATH=${PATH}:${SBT_HOME}/bin
ADD https://dl.bintray.com/sbt/native-packages/sbt/$SBT_VERSION/sbt-$SBT_VERSION.tgz .
RUN tar -xf sbt-$SBT_VERSION.tgz && mv ./sbt-launcher-packaging-$SBT_VERSION /usr/local/sbt
RUN sbt version
A typical docker build command would be docker build --no-cache --squash --build-arg SBT_VERSION=0.13.13 takari-oss-alpine-sbt:8_0.13.13
Note that the command --squash may not be available in your docker version but if it is we should use it as it would compressed the different committed layers into just one and would reduce uploading time and size of the resulting image.
The resulting sbt base image above from the previous step is quite large already at 376MB. Let's use it to build a known app and see what's the result. Let's build autodimension as example.
FROM takari-oss-alpine-sbt:8_0.13.13
ENV CASSANDRA_USERNAME=cassandra CASSANDRA_PASSWORD=cassandra
ENV rest_interface=8080
RUN mkdir -p /root/usr/src/wm_auto_dimension/
COPY . /root/usr/src/wm_auto_dimension
WORKDIR /root/usr/src/wm_auto_dimension
RUN mkdir -p /log/autodimension && \
mkdir -p /etc/autodimension && \
cp -r src/main/resources /etc/autodimension
RUN sbt -Dsbt.override.build.repos=true -Dsbt.repository.config=repositories assembly
VOLUME ["/etc/autodimension","/log/autodimension"]
EXPOSE ${rest_interface}
ENTRYPOINT ["java","-jar","target/scala-2.11/autodimension.jar"]
CMD ["/etc/autodimension/resources/qa-config.conf"]
You'll build autodimension with docker build --no-cache --squash -t autodimension:build
typically.
This is a fine image we could use to run autodimension but it is huge clocking 734MB. This is where the build pattern comes in. We'll prepare a different dockerfile in the same root directory. We'll call it Dockerfile.deploy.
FROM openjdk:8-jre-alpine
ENV CASSANDRA_USERNAME=cassandra CASSANDRA_PASSWORD=cassandra
ENV rest_interface=8080
WORKDIR /root/
RUN mkdir -p /log/autodimension && \
mkdir -p /opt/autodimension && \
mkdir app
COPY app app
VOLUME ["/opt/autodimension","/log/autodimension"]
EXPOSE ${rest_interface}
ENTRYPOINT ["java","-jar","./app/autodimension.jar"]
CMD ["/app/resources/qa-config.conf"]
And a build script to copy the artifact over from the build image leaving behind the build system and deploying just what's needed to run the application.
#!/bin/sh
echo Building autodimension:build
docker build -t autodimension:build .
mkdir app
docker create --name extract autodimension:build
docker cp extract:/root/usr/src/wm_auto_dimension/target/scala-2.11/autodimension.jar ./app
docker cp extract:/root/usr/src/wm_auto_dimension/src/main/resources ./app/resources
docker rm -f extract
echo Building autodimension:$1
docker build --no-cache -t autodimension:$1 . -f Dockerfile.deploy
rm -r ./app
Now you can build the deployable docker image with build.sh 0.13.13 resulting in an image of size 161MB. This is now a very reasonable image size for a java/scala application.
The builder pattern is such a success and widespread that docker has decided to address the problem. What's the problem? Well, it relies in an intermediate shell script to do the building and while this will work extremely well with build servers it isn't friendly for kubernetes and other container driven cloud deployments since often is not possible to provide these instructions.
So, how would our Dockerfile look with multi-stage building? Like this
FROM takari-oss-alpine-sbt:8_0.13.13 as builder
RUN mkdir -p /root/usr/src/wm_auto_dimension/
COPY . /root/usr/src/wm_auto_dimension
WORKDIR /root/usr/src/wm_auto_dimension
RUN sbt -Dsbt.override.build.repos=true -Dsbt.repository.config=repositories assembly
FROM openjdk:8-jre-alpine
ARG VERSION
ENV CASSANDRA_USERNAME=cassandra CASSANDRA_PASSWORD=cassandra
ENV rest_interface=8080
WORKDIR /root/
RUN mkdir -p /log/autodimension && \
mkdir -p /opt/autodimension && \
mkdir app
COPY --from=builder /root/usr/src/wm_auto_dimension/target/scala-2.11/autodimension.jar ./app
COPY --from=builder /root/usr/src/wm_auto_dimension/src/main/resources ./app/resources
VOLUME ["/opt/autodimension","/log/autodimension"]
EXPOSE ${rest_interface}
ENTRYPOINT ["java","-jar","app/autodimension.jar"]
CMD ["/app/resources/qa-config.conf"]
You can build this image with docker build --no-cache --squash --build-arg VERSION=2.1.1 -t autodimension:multi . -f Dockerfile.multi
and you'll find that the resulting image is also 161MB but now we did it all with one Dockerfile and there was no need for an intermediate shell script.
Multi-stage building are available for docker version >= 17.05.