In this article I am going to show how to build a Docker image containing a Java application using Maven. As an example, I will use a Spring Boot web application, which will be packaged into a Docker image. The application packaged does not need to be a Java application, but can be anything for which a Docker image can be created. I have, for instance, used the process described to make it possible to build regular Docker images on a Jenkins build server.
The Example Spring Boot Application
The Spring Boot web application I will use as an example Java application is created as follows:
- Use this Spring Initializr link to generate the bare-bones Spring Boot web project with Thymeleaf templating support.
- Open the Spring Boot project in your favourite IDE.
- In src/main/java in the package se.ivankrizsan.spring, create a package named “controllers”.
- In the new package, implement the class HelloController as this:
package se.ivankrizsan.spring.controllers; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import java.util.Date; /** * Simple Spring MVC controller that says hello. */ @Controller public class HelloController { /* Constant(s): */ protected static final String HELLO_VIEW_NAME = "hello"; protected static final String GREETING_PLACEHOLDER = "greeting"; @RequestMapping("/hello") public String hello( @RequestParam(value="name", required=false, defaultValue="Anonymous") final String inName, final Model inModel) { final StringBuilder theMessageBuilder = new StringBuilder(); theMessageBuilder .append("Hello ") .append(inName) .append(", the time is now ") .append(new Date().toString()) .append("."); inModel.addAttribute(GREETING_PLACEHOLDER, theMessageBuilder.toString()); return HELLO_VIEW_NAME; } }
- In src/main/resources/templates, create a file named hello.html with the following contents:
<!DOCTYPE HTML> <html xmlns:th="http://www.thymeleaf.org"> <head> <title>Greetings!</title> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/> </head> <body> <p th:text="${greeting}"/> </body> </html>
The example application can now be started by running the SpringbootDockerimageApplication class. To test the application, enter the http://localhost:8080/hello?name=Ivan URL in a local browser. There should be a greeting in the browser window on the following format:
Hello Ivan, the time is now Sat Feb 11 22:57:33 CET 2017.
Prepare for Docker Image Creation
Setting up to create a Docker image for a Java application consists of two steps; the first step is much like for any Docker image in that a Docker file is created and the static contents of the Docker image is gathered/created. The second part consists of adding a Maven profile to the project’s pom.xml file.
Docker Directory and Its Contents
The Docker file and any additional static contents of the Docker image is created in the source of the project. In this example it is in the src/main/resources/docker directory. The location of this directory can be changed by modifying a property, as we will see later.
- Create a directory named “docker” in the src/main/resources directory.
- In the src/main/resources/docker directory, create a directory named “application”.
- In the src/main/resources/docker/application directory, create two directories named “bin” and “lib”.
- In the src/main/resources/docker/application/bin directory, create a file named “start-app.sh” with the following contents:
#! /bin/sh java -jar ${JAR_PATH}
- In the src/main/resources/docker directory, create a file name “Dockerfile” with this contents:
# Docker image that contains a Spring Boot application. # FROM anapsix/alpine-java:8_jdk_unlimited # Absolute path to the JAR file to be launched when a Docker container is started. ENV JAR_PATH=/application/lib/springboot-webapp.jar # Create directory to hold the application and all its contents in the Docker image. RUN mkdir /application # Copy all the static contents to be included in the Docker image. COPY ./application/ /application/ # Make all scripts in the bin directory executable. Includes start-script. RUN chmod +x /application/bin/*.sh # Web port. EXPOSE 8080 CMD [ "/application/bin/start-app.sh" ]
Modify the Dockerfile accordingly if, for instance, you want to use another base image.
Maven Profile
To create the Docker image containing the Spring Boot application, I’ll use the following Maven plug-ins:
- maven-resources-plugin
Copies files to a directory in which the Docker image will be built.
The files needed to build the Docker image are copied to a directory in the target directory of the project. - maven-dependency-plugin
Copies the JAR file containing the application and any additional Maven artifacts to the Docker image build directory. - Spotify’s docker-maven-plugin
Removes the Docker image produced by the project. Needed since, at the time of writing, the Maven plug-in used to build the Docker image will not overwrite an existing Docker image. - Spotify’s dockerfile-maven-plugin
Builds the Docker image.
In the project’s Maven pom.xml file, I use a Maven profile to contain what is needed to build the Docker image. I have tried to rely on properties in the profile for configuration of the Docker image build in order to make it easier to adapt the profile to different Java applications.
The complete Docker image building Maven profile looks like this:
<profiles> <!-- This profile builds a Docker image with the Spring Boot application. Before being able to build a Docker image using Maven, the environment variable DOCKER_HOME need to be set to the endpoint of the local Docker API. Example *nix: export set DOCKER_HOME=http://localhost:2375 The Docker image is built using the following command: mvn -Pdockerimage package If a Docker image with the image name and tag (project version) already exists then one of the following may happen depending on the environment in which the build is run: - The existing image is given the image name and tag <none>. - No new Docker image is generated, but the existing image is retained. The suggested approach is to first delete any existing Docker image using the following Maven command before generating a new image: mvn -Pdockerimage clean --> <profile> <id>dockerimage</id> <dependencies> <!-- Here you declare dependencies to additional artifacts that are to be copied into the Docker image. No need to add a dependency to the Spring Boot application JAR file here. --> </dependencies> <properties> <!-- Name of Docker image that will be built. --> <docker.image.name>springboot-webapp</docker.image.name> <!-- Directory that holds Docker file and static content necessary to build the Docker image. --> <docker.image.src.root>src/main/resources/docker</docker.image.src.root> <!-- Directory to which the Docker image artifacts and the Docker file will be copied to and which will serve as the root directory when building the Docker image. --> <docker.build.directory>${project.build.directory}/docker</docker.build.directory> </properties> <build> <plugins> <!-- Copy the directory containing static content to build directory. --> <plugin> <artifactId>maven-resources-plugin</artifactId> <executions> <execution> <id>copy-resources</id> <phase>package</phase> <goals> <goal>copy-resources</goal> </goals> <configuration> <outputDirectory>${docker.build.directory}</outputDirectory> <resources> <resource> <directory>${docker.image.src.root}</directory> <filtering>false</filtering> </resource> </resources> </configuration> </execution> </executions> </plugin> <!-- Copy the JAR file containing the Spring Boot application to the application/lib directory. --> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-dependency-plugin</artifactId> <executions> <execution> <id>copy</id> <phase>package</phase> <goals> <goal>copy</goal> </goals> <configuration> <artifactItems> <artifactItem> <!-- Specify groupId, artifactId, version and type for the artifact you want to package in the Docker image. In the case of a Spring Boot application, these are the same as the project group id, artifact id and version. --> <groupId>${project.groupId}</groupId> <artifactId>${project.artifactId}</artifactId> <version>${project.version}</version> <type>jar</type> <overWrite>true</overWrite> <<outputDirectory>${docker.build.directory}/application/lib</outputDirectory> <!-- Specify the destination name as to have one and the same name to refer to in the Docker file. --> <destFileName>springboot-webapp.jar</destFileName> </artifactItem> <!-- Add additional artifacts to be packaged in the Docker image here. --> </artifactItems> <outputDirectory>${docker.build.directory}</outputDirectory> <overWriteReleases>true</overWriteReleases> <overWriteSnapshots>true</overWriteSnapshots> </configuration> </execution> </executions> </plugin> <!-- Remove any existing Docker image with the image name and image tag (project version) configured in the properties. --> <plugin> <groupId>com.spotify</groupId> <artifactId>docker-maven-plugin</artifactId> <version>0.4.13</version> <executions> <execution> <id>remove-image</id> <phase>clean</phase> <goals> <goal>removeImage</goal> </goals> <configuration> <imageName>${docker.image.name}</imageName> <imageTags> <imageTag>${project.version}</imageTag> </imageTags> <verbose>true</verbose> </configuration> </execution> </executions> </plugin> <!-- Build the Docker image. --> <plugin> <groupId>com.spotify</groupId> <artifactId>dockerfile-maven-plugin</artifactId> <version>1.2.2</version> <executions> <execution> <id>default</id> <phase>package</phase> <goals> <goal>build</goal> </goals> </execution> </executions> <configuration> <contextDirectory>${project.build.directory}/docker</contextDirectory> <writeTestMetadata>false</writeTestMetadata> <dockerInfoDirectory></dockerInfoDirectory> <verbose>true</verbose> <forceCreation>true</forceCreation> <imageName>${docker.image.name}</imageName> <repository>${docker.image.name}</repository> <tag>${project.version}</tag> <forceTags>true</forceTags> <pullNewerImage>false</pullNewerImage> <imageTags> <imageTag>${project.version}</imageTag> </imageTags> <dockerDirectory>${project.build.directory}/docker</dockerDirectory> </configuration> </plugin> </plugins> </build> </profile> </profiles>
If we look at the different parts of the Maven profile, we can see:
- There is a <dependencies> element.
Add any additional Maven dependencies to, for instance, third party libraries that you want to include in the Docker image in this element. - In the <properties> element, there is a property named docker.image.name.
This property contains the name of the Docker image that will be produced. I expect this property to be modified for each application. The Docker image tag will be the project’s version. - The next property in the <properties> element is docker.image.src.root.
This property holds the relative path to the directory that contains the Dockerfile and any additional static contents that is to be included in the Docker image. - The final property in the <properties> element is docker.build.directory.
This property holds the path to the directory in which the Docker image will be built. - In the <build> section of the profile, the first plug-in is the maven-resources-plugin.
This plug-in is used to copy static contents from the directory pointed at by the docker.image.src.root property to the directory which location is specified by the docker.build.directory property. The entire directory structure is preserved. - The second plug-in in the <build> section is the maven-dependency-plugin.
Using this plug-in, the JAR file that contains the Spring Boot application built by the project is copied to the application/lib directory in the directory in which the Docker image will be built. Recall that the location of the application JAR file is also stored into the environment variable JAR_PATH in the Dockerfile.
Note that a destination filename is specified for the application JAR, in order to have a fixed filename to refer to in the Dockerfile. - The third plug-in in the <build> section is the docker-maven-plugin.
Since the current version of the dockerfile-maven-plugin that we will discuss later does not overwrite existing Docker images, I have included this plug-in to be able to remove any existing Docker image when cleaning the Maven project. - The final plug-in is the dockerfile-maven-plugin.
This is the plug-in that is responsible for building the Docker image. The main reason that I chose this plug-in over the alternatives is that it allows me to create a Docker image from a Dockerfile – just like it works when I am creating Docker images by hand.
Create the Docker Image
If we now open a terminal window and go to the directory that contains the pom.xml file of the example Spring Boot web application, we can build the Docker image using this Maven command:
mvn -Pdockerimage package
After some time, the message BUILD SUCCESS should appear in the terminal and if you list the Docker images, you should see the new image listed:
$ docker images REPOSITORY TAG IMAGE ID CREATED SIZE springboot-webapp 0.0.1-SNAPSHOT 16efcb5f8335 3 minutes ago 194.3 MB
Use the Docker Image
To start a Docker container using the newly produced Docker image, I use the command:
docker run -p 8080:8080 springboot-webapp:0.0.1-SNAPSHOT
Opening the URL http://localhost:8080/hello?name=Ivan (replace localhost with the appropriate IP if you run Docker in a virtual machine) in a browser, I see the same type of greeting that I saw when I was running the Spring Boot web application outside of the Docker container.
Happy coding!