Faster Java containers with Docker multi-stage builds
Docker’s multi-stage build feature means Java containers can be smaller and more efficient.
Background
At MailSlurp we deploy Kotlin/Java Spring APIs in Docker containers to a Kubernetes cluster. Our API gives you unlimited private email addresses to test your app with.
Our containers are built with CircleCI. Recently we started to notice that containers were taking a long time to build and upload to our registry. Some were taking several minutes to upload and were up to 500mb in size.
We found out that the large size was because we copied all our source files, resources, and tests into the container in order to test and build it. We then shipped these containers straight to ECR.
Our Dockerfiles generally looked like this:
FROM openjdk:8-jdk as build
VOLUME /tmp
WORKDIR /ADD . .
RUN ./gradlew --stacktrace clean build -x testEXPOSE 5000ENTRYPOINT ["sh","-c","java -jar /app.jar"]
A better way
After some refactoring we were able to reduce our container size 10x to a much more reasonable 50mb. This decreased upload times and made our deployments faster.
We did this with Docker multi-stage builds; a feature that let’s you build an app in one container and then copy artifacts to another, slimmer container for execution.
Here is an example:
# build stage build the jar with all our resources
FROM openjdk:8-jdk as build
ARG VERSION
ARG JAR_PATH
VOLUME /tmp
WORKDIR /
ADD . .
RUN ./gradlew --stacktrace clean test build
RUN mv /$JAR_PATH /app.jar
# package stage
FROM openjdk:8-jdk-alpine
WORKDIR /# copy only the built jar and nothing else
COPY --from=build /app.jar /
ENV VERSION=$VERSION
ENV JAVA_OPTS=-Dspring.profiles.active=production
EXPOSE 5000
ENTRYPOINT ["sh","-c","java -jar -Dspring.profiles.active=production /app.jar"]
As you can see, the first build stage copies all our project files into a java container runs tests and builds a jar. We then copy only this jar into a small alpine-linux container and push this final container to ECR. That way we are not bundling our test-suite and resource files in our production container!
There you have it. The same approach should work for any framework.