tl;dr – Step 1. Create Dockerfile, Step 2. Done!
As a team we wanted to get a Spring Boot REST API to become docker-ready. As in, get it to a place where we can take the docker image and launch it on Kubernetes. We wanted it to be horizontally scalable, portable, and be easily managed by a container orchestration technology. Include any buzzwords I missed.
If you want to learn how one might get a Spring Boot application to be Dockerized! Let’s get started!
- Creating your first Dockerfile (if applicable)
- Use multi-stage builds within your Dockerfile
- Setting up tomcat to run your application
- Setting up volumes locally for testing your newly built image
- Running your docker container locally!
- Then finally, launching it
Before jumping into the Docker half of the world, let’s define what we have for the application we are dockerizing:
- We have the Java Spring Boot application source
- We use a build automation tool to build our Spring application : Maven
The common installation method for a Spring Boot application is generally installed on a VM that has Tomcat installed. We move the WAR file (that comes from the Maven build) into the correct location and run the Tomcat server.
So for Docker, we initially leaned towards just passing in the WAR file that was generated through other CI tools and build the Tomcat portion into the docker image.
Instead, we decided that we would like the project built to an image as a unit/single operation. That would avoid us having to setup a place where the WAR files would eventually live and the Docker image that is built to be configured to pick-up the files at that location. We thought the single unit approach would be simpler to deal with and we utilized multi-stage builds to help with that.
Creating your first Dockerfile
There are of course a ton of resources on the internet regarding this. However, you came here to learn how to dockerize a Spring Boot application. If this is your first time creating a Dockerfile, don’t fear! I will go through some of the basics and get you up to speed.
Dockerfiles have some basic commands that we use to glue together and create these images. I will only go over the ones we will use, but there are more.
Now that you have a basic understanding of the base commands of a Dockerfile, we are gonna use a combination of the above to create one.
Maven and Multi-Stage Builds
Docker images can get fairly large, and we always have a goal of keeping image sizes small. Originally, the Docker community started commonly creating two Dockerfiles. One for the build portion, and one for the production-ready application. Eg. (Dockerfile.build and Dockerfile).
Thankfully, there is now a solution which allows us to throw out having to create two Dockerfiles, and use what is known as multi-stage builds .
As we originally discussed we are going to be building the source code using our preferred build automation tool, Maven. Then run the application using a web server. (eg. Tomcat).
As you can see, we use two of the keywords,
FROM keyword instructs Docker to start our image from that base image. In this case, we need the maven image (version 3.6.3). Make note of the as maven, as that will be coming handy later in the process. It allows us to name this stage, and in our case, we named it maven.
LABEL keyword allows us to, wait for it….
LABEL the image with some key/value pairs. We choose to include the company, maintainer email, and the application name.
Next we use the
WORKDIR keyword essentially anchors the directory we will be working in. All subsequent commands will be run from that base directory. If the directory isn’t created, it will create it for you. We chose the arbitrary path of
usr/src/app as the directory to move our source into.
COPY command is where the real work begins. We copy over our source files from our host machine, which in this case is the same directory as our Dockerfile. We specify that by using the
. to indicate that we want to copy the files from the root of “context of the build”. Which we supply to the docker build command. In our case, we supply it with the directory that contains the Dockerfile.
Now that the files are copied over into
/usr/src/app we will now actually build the project using Maven. We want to package the application into a WAR file but by running the
mvn package command it will also run the tests. Which is great because all of that happens in a single command in your CI.
Note: you will need to provide everything to the container that it needs to test the application, otherwise the tests will fail. If you do tests in others parts of your CI and want to skip them. You can use
mvn -Dmaven.test.skip=true package. instead
RUN command as noted earlier is to add layers to the Docker image. The result of this command will be a layer that is added to the image. Which is good for us, because we will need the WAR from this command in our second build stage for Tomcat. After
mvn package command runs. There will be a directory in
/usr/src/app/target that will contain our application
Tomcat and the Multi in the Multi-stage builds
This stage is where Multi-stage builds get it’s Multi! Now we are going to start off with a Tomcat image and begin setting up the required folders to run our application.
Also note that
# indicate comments within your Dockerfile.
A few interesting portions to this section of the Dockerfile.
We now use a
FROM keyword again. This begins the second stage. We are essentially telling the Dockerfile to start from “scratch” (kind of) from this image. In our example, it’ll be
We then utilize the
ARG keyword. We wanted to specify the where Docker should look to find some Tomcat configuration files we wanted to place within the image. Remember,
ARG are arguments you may pass in during build-time only. The default of
/docker instructs Docker that we should look in the
/docker folder within the build context from the host machine.
Next we see the
ENV keyword. These are environment variables that can be changed during runtime. However, we do have some defaults. Our application uses the
SAMPLE_APP_CONFIG environment variable to find out where the configuration files live. These environment variables are akin to the environment variables you would set on your host machine, such as the
PATH environment variable.
The last environment variable we set is the
CATALINA_OPTS which Tomcat uses to determine how much max/min memory, the metaspace to use, as well as the stack size the process can have.
Now all that’s left is to move the war file to the appropriate location and get it ready to run!
Again, let’s take it one step at time. We have used the
WORKDIR keyword to anchor the directory within the tomcat directory that contains the webapps. We will be moving the WAR file created in the first build stage to this location.
Let’s take at this
COPY command closely:
COPY --from=maven /usr/src/app/target/SampleApp.war /usr/local/tomcat/webapps/api.war
As you can see here we use the
--from=maven flag that we haven’t used before. If you remember, we made note of this in the original build stage. We named the first stage by specifying it in the
FROM keyword (
FROM maven:3.6.3 as maven). Since we used
as maven , we can now use it as a reference and access files we created in that stage. In our case, we created the WAR file and saved it to
/usr/src/app/target/. We then move it to the
/usr/local/tomcat/webapps/ folder and rename it to
api.war in the process.
Note: by naming it to api.war, we can now access the application at say localhost:8080/api vs localhost:8080/
We then move over some configuration files using the
COPY command again into the respective Tomcat folder, and anchor the working directory to the data folder.
We are finally nearing the end of the Dockerfile that kicks off the main command that you want the Docker image to run when it spins up a container.
We use the
EXPOSE keyword to specify which port we want Docker to enable networking for, and letting Docker know that there will be some traffic going thru this port.
Finally, we use the
ENTRYPOINT keyword to run the
catalina.sh run that kicks off the Tomcat server and deploys our application and make it ready to receive requests.
In a single place, this is how our Dockerfile looks like:
We placed this file and named it
Dockerfile in the root of our repository where the
pom.xml exists and ran the following command to begin building this Docker image.
docker build -t kbillen92/sample-api:latest .
-t is tagging the build with
. at the end is specifying the build context and where to look for the
Dockerfile, which happens to be the root of the directory to our application.
Once the image is built you will be able to see it by running the following command:
Alright!! You are now (almost) ready to finally run your Spring Boot Docker image!
I say almost because in most applications you need to save some sort of state such as images, documents, or some sort of data that you need to persist even if the docker container is not running. For that reason, we are also going to setup a volume to keep that persistent data.
If your application does not have the requirement of persistent data, you can skip the next step of creating a volume.
Setting up the volume
Run the following command to create the volume.
docker volume create --driver local --opt device=/d/DockerVolumes/sampleVolume --opt type=none --opt o=bind sampleVolume
Note: You will need to create that folder structure beforehand, otherwise the volume won’t be created.
We are ready to run it!
Running it locally
Since we have finally built our image and created the persistent volume (if applicable), we can now run the image.
docker run -d --mount source=sampleVolume,target=/var/lib/SampleApp -p 8080:8080 kbillen92/sample-api:latest
You can now run
docker ps to see if it’s running.
If you would like to access the bash terminal inside the container run the following command:
docker exec -it <container_id or name> /bin/bash
YOU HAVE CREATED AND LAUNCHED YOUR SPRING BOOT APPLICATION!!
The last step is to push this image to your Docker repository. If you have DockerHub account you can do the following commands:
docker login -u <username> -p <password> to first login.
docker push kbillen92/sample-api:latest to push it to DockerHub
Note: This will push it to a public repository. If you would like it to be private, please create the repository first
Now that you are all fancy with Docker, you will need to deploy your container somewhere. This brings about another technology that you will probably face, Kubernetes. It’s what everyone is talking about these days.
Let us know what you think!
Are You Ready for a DevOps Transformation?
While software continues to eat the world at an ever-increasing pace with DevOps, the challenges and struggles of companies implementing DevOps is very real. We all can overcome these challenges by working together, improving our tools, processes, knowledge and training our workforce.