Let's say you're building an image for a Java application. Your project is setup like this:

  • You're using Maven as your package manager, and so your project has a pom.xml in its root directory.
  • Your source code is in a "src" folder in your project's root directory.
  • Your project is setup so that it compiles to "my-app.jar".

For this project, you might write a Dockerfile like the one below:

FROM maven:3.6.3-jdk-11-slim
COPY src /home/app/src
COPY pom.xml /home/app
RUN mvn -f /home/app/pom.xml clean package
WORKDIR /home/app/target
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "my-app.jar"]

However, the resulting image size is actually much larger than it needs to be. The image will have Maven, JDK tools, and your source code in it, which are needed to compile the jar. But running the jar requires only the jar and a JRE.

You can cut out the unneeded bloat via a multi-stage build, which is demonstrated by the following Dockerfile:

FROM maven:3.6.3-jdk-11-slim AS build
COPY src /home/app/src
COPY pom.xml /home/app
RUN mvn -f /home/app/pom.xml clean package

FROM openjdk:11-jre-slim
COPY --from=build /home/app/target/my-app.jar /usr/local/lib/my-app.jar
WORKDIR /usr/local/lib
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "my-app.jar"]

Here's what's happening:

  • You'll notice that there are two FROM instructions. This Dockerfile effectively creates two images.
  • In the first FROM instruction, the AS build establishes an alias to use later in reference to this image.
  • The first image compiles the jar.
  • The second image uses a base image that provides only a JRE.
  • When copying the jar into the second image, a "--from=build" argument is used, which tells it to copy the file from the "build" image instead of the host.
  • Only the files in the second image are present in the containers created by docker run.