Want to learn How to Create a Docker Image for Java Application? Follow these easy steps in creating a docker image for a Java Spring Boot application. Time to dockerize!
Create Docker Image for a Java Application
There are different types of docker images for Java applications depending on the environment we find ourselves in.
- Docker containers for your Java Development Environment where it’s just you developing on your machine
- Docker containers which run full builds and/or tests on code branches that everyone checks code into (multiple programmers). Think CI – Continuous Integration pipeline.
- Docker containers that deploy your Java application in production (think CD – Continuous Delivery/Deployment pipeline)
In practice creating those environments (dev/test/prod) require special consideration. For example: Your container running your Java development environment needs more tooling like a jdk, IDE (possibly), 3rd party build tool, a source configuration tool etc..
There are are several ways to go about doing this but I have found the most practical being to place your developer tools on a single developer container. You can check out my own Java developer environment docker image using Eclipse with Spring’s STS 4 plugin here.
The CI and CD pipelines are lighter and faster to spin up than your developer environment. Your build containers will need a jdk, access to the source code and 3rd party build tooling. The production containers mostly need access to the jar/war and the jre instead of the jdk.
There are some interesting things we can do with multi-stage builds to merge the gap between CI and CD but I won’t be getting into that here. I’ve already covered building a Java development environment and CI pipeline in previous articles and YouTube videos so I’ll mainly focus on how you can containerize an already packaged and tested java application in a docker container.
How to put your Java Application into a Docker Container
It all starts with a Dockerfile which is a text file containing instructions on how to build a docker image. You can think of a Dockerfile as the lowest common denominator in this entire equation or flow in containerizing your Java application. Anyone who runs the built docker image can run your Java application because you’ve packaged everything it needs to run.
Step 1 … decide which environment the Java application will run in.
- Which platform? Will the application be running on Linux, Windows?
- Which O.S distribution .. Ubuntu, Alpine, windows server 2019?
- What Java version do you need .. 8, 11,15?
- Will you be using a licensed Java (Oracle) or free open sourced license for Java?
- Do you need the Java JDK or just the JRE?
The answers to these questions will drive you to select the appropriate docker base image. Step 1 is all about picking the perfect base docker image.
Choosing a Base Image for your Java Application
What’s a base image? It’s someone else’s built image (usually from a trusted vendor) onto which you can lay your own customization upon – your Java application in this case. Sometimes it’s referred to as the: parent image.
I have chosen Alpine Linux as my platform distribution with Azul’s Zulu OpenJDK for my open sourced Java. Alpine Linux is known for its secure and lightweight distribution – well suited for CI/CD pipelines. I only need the jre since I’m only going to run the application and I’ve chosen jre13. All the above questions have been answered now but where do I go to get a base docker image which has all this or any other variation thereof?
If you navigate to Docker Hub and search for openjdk, you will see tons of docker images already prepared which package up a platform with a java jdk or jre version. The combination of both will be identified by a repository image name and tag. You will then refer to this base image in your Dockerfile. This will allow you to lay down your Java application along with any configuration needed on top of it.
You have to read the Docker Hub documentation for whatever vendor you have selected. Azul/Zulu offers different combination of jdk/jre and platform distributions.
A vendor will identify or label a specific image with a docker tag which will be listed with a brief description. A tag is just some human readable ID/text that we use to set apart different docker image configurations/versions. They are tagged on after the repository name after a colon “:” character. For example, the Zulu distributions have the following major repositories which identify Alpine, CentOS, Debian and Ubuntu docker images …
- azul/zulu-openjdk-alpine
- azul/zulu-openjdk-centos
- azul/zulu-openjdk-debian
- azul/zulu-openjdk (Ubuntu)
Once you click on any of those links, you are brought to another page in which you can then read through the different docker images identified by their unique tags. If you clicked on “azul/zulu-openjdk-alpine” then you would see tags such as …
- azul/zulu-openjdk-alpine:latest
- azul/zulu-openjdk-alpine:15
- azul/zulu-openjdk-alpine:15.01-15.28.13
- azul/zulu-openjdk-alpine:15.01
- azul/zulu-openjdk-alpine:14
- azul/zulu-openjdk-alpine:13-jre (We will go with this – lightweight as compare to jdk thus smaller image size, smaller memory footprint, smaller attack surface and faster startup time)
I have a great pro tip which will save you huge headaches when it comes to correctly selecting a base image tag. Only MVP Java newsletter subscribers get access to exclusive articles like this one. Sign-up – it’s free!
In this tutorial, I have selected the base docker image repository:tag “azul/zulu-openjdk-alpine:13-jre“. Now this base image will be the first line of our Dockerfile. This file could have any filename however to keep things simple, we’ll keep with the commonly used filename “Dockerfile”.
1 2 3 4 |
# this is a comment! Step 1 FROM azul/zulu-openjdk-alpine:13-jre # Other stuff has to be added here ... eventually. |
Step 1 complete!
Check out the YouTube Video Tutorial version!
Dockerize your Spring Boot Java Application
We now have to get our Java application (which will also be a Spring Boot) into the container. Should you package a JAR or WAR in a Docker Container?
If your not building a web application then you only have one option – jar file. The confusion lies when you are building a web application and you see others deploying a web app as a jar file! Whether you deploy your web application as a jar or war really depends on its deployment environment.
If your deploying your application in your own developer environment or automated CI pipeline then it can be very beneficial to deploy it as a jar file. Why?
The jar file can be packaged to also contain an embedded in-memory web server like tomcat. This is the default behavior when packaging a Spring Boot web application. The uber/fat jar contains everything it needs to be able to deploy and run your Java application. There is no need to deploy your application to an external server. Very practical.
At other times you need to make your Java application server independent and deploy it on web/application servers like Weblogic, JBoss, Tomcat. This is probably the case for your production environment. In that case, you need to deploy your app as a war file. This involves running the web server in a docker container and then copying your war file at the correct location for it to be deployed.
You can check out the following resources I have already created on this subject here on YouTube – Deploy Spring Boot WAR to Tomcat in Docker Container.
I’ll cover deploying a jar file to keep it simple but the high level steps are the same.
Get your Spring Boot Application into Docker Container
We will take a scenario where we already have a packaged jar file named myApp.jar which we want to containerize. This Java application is from a previous tutorial covering the Java Optional Class I wrote here. I never dockerized it, so this is a good opportunity.
Second step is to COPY or ADD the jar file into the container by including it into the image. Only way to do that is to add a line in the Dockerfile which copies it to some destination.
1 2 3 4 |
FROM azul/zulu-openjdk-alpine:13-jre #Step 2 COPY myApp.jar /tmp |
The COPY directive will copy the file “myApp.jar” from the current working directory and send to to the /tmp directory inside the containers file system. Right now though, there is still no container since we are only laying down the instructions on how to build the image. Think of an image as a Java Class file (build time) and the container as the Java Object that’s instantiated (run time), if that helps.
You will surely come across the ADD directive which has added confusion because the next natural question is … What’s the difference between COPY and ADD commands in a Dockerfile?
The ADD directive could of been used instead of COPY in the above example and we would of not even noticed a difference. The ADD does a copy as well but adds extra functionality like unpacking archive files automatically and has the ability to download files via URL’s. Sounds great doesn’t it?! I thought so too until I got bit in the ass a couple of times with ADD. Now I don’t use ADD anymore. Why?
I use to use the Dockefile ADD command all the time to download dependencies via URL like the Java jdk from oracle, maven, git etc .. I had nice scripts to hand pick the latest or specific versions for customization. The ADD command would download them from the net, copy and unpack them for me in the container. I thought this would be more work initially but would pay off big time later on in terms of flexibility and maintenance. I was wrong!
What happened is that the vendors would change their URL paths or drop them all together (Oracle was the worst). I was able to build my images fine on the day I was developing my Dockerfile and scripts but then my images wouldn’t build weeks or months later because the URL’s would change or disappear.
So I swore I would only use ADD if I controlled the URL’s myself! Now, I actually download the archive files onto my machine and COPY them into the image. It also gives me the flexibility to go back to an old version which is not so easy with ADD if the vendor had taken it off their site!
So use COPY instead of ADD and if you need to copy over some .tar file then COPY it, unpack it and clean up after yourself (remove the archive) in order to reduce the size of image.
Configure Java Application in Docker Container
Do you need to provide any configuration along with your Java application? Any external properties, configuration files, test data, environmental variables etc ..? If so, now’s the time to add it to the image – Step 3.
You already know how to COPY a file (config, test data as an archive) into the image therefore that’s covered but if you then need to declare an environment variable to point to those files then youĺl need to do so like ..
1 2 3 4 5 6 7 |
FROM azul/zulu-openjdk-alpine:13-jre COPY myApp.jar /tmp #Step 3 COPY myconfigfile.txt /tmp ENV CONFIG_FILE /tmp/myconfigfile.txt |
The ENV Dockerfile directive sets up the Key=Value pair as your environmental variable. Your Java Application will be able to access it in the dockerized environment. How to configure your Java application is really a wide open topic since it’s very particular to your situation however knowing how to perform logging in a docker container is very important!
As Java developers, we take great care with our logging configuration. This can include anything from specifying logging levels, crafting log file names, log file rotation policies, max log file size etc .. The issue we now run into is that we should not be logging into the container’s filesystem! Why not?
This has to do with Docker logging best practice. It turns out that we should only be logging to STDOUT and STDERR. This is a very important topic and the last thing you want to do is modify your Java application code to change your entire logging configurations. Do not do that!
Instead, read my blog on the topic where I show you how to keep your Java application code untouched and use a nifty little trick or hack in the Dockerfile to redirect your logs to stdout and stderr. Check it out here – Docker Logging | Symlink Hack.
Alright, Java application configuration done … Step 3 complete.
Running a Java Application in a Docker Container
How do we bootstrap our Java application in a Docker container? Step 4 is all about telling Docker what the whole point of running this container is. We want to run myApp.jar!
There is a Dockerfile command called CMD which is there to execute any command inside the container at run time. There can only be one such command to execute on container startup. We know our Java application is sitting in /tmp but now we need to tell docker to invoke it by wrapping it in a Dockerfile CMD directive.
The CMD directive offers a default command to execute when the docker container is run. Think of it as a suggestion. This is good when you want to supply some default to the user that will run our container. If they don’t agree then they can override your suggestion very easily.
But what are we trying to achieve here? Are we a) creating a generic container image that will execute some default command which probably should be overridden by the user or b) creating a container to run a specific Java application every time its run? If you answered b) then great! Lets force this container to be treated as an executable – our java application. How do you treat a docker container as an executable?
You do so by using the Dockerfile ENTRYPOINT directive which does the same thing as CMD except it treats the container like an executable. This means that it is should not be overridden by some other command by the user (although still possible via extra step). This docker container must be treated as the “myApp.jar” executable command.
1 2 3 4 5 6 7 8 9 10 |
FROM azul/zulu-openjdk-alpine:13-jre COPY myApp.jar /tmp COPY myconfigfile.txt /tmp ENV CONFIG_FILE /tmp/myconfigfile.txt #Step 4 #CMD java -jar /tmp/myApp.jar ENTRYPOINT java -jar /tmp/myApp.jar |
I put both CMD and ENTRYPOINT in the Dockerfile just to show you how similar they are however, the CMD directive has been commented out. When running a java application in docker containers, it’s best to use ENTRYPOINT.
Building Your Dockerfile | Create a Docker Java Image
The best thing to do is create an empty directory and ensure that only the artifacts you need are there and nothing else. Docker will look into the current working directory and build a context out of it. The less garbage in there the better as it will result in a faster and leaner build process.
I created an empty directory “demo”. Once inside this directory, you can execute STEP 5 which builds a docker a java docker image – Step 5.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
$ pwd /tmp/demo $ ls Dockerfile myApp.jar myconfigfile.txt $ docker build -t my-dockerized-java-app:latest . Sending build context to Docker daemon 8.244MB Step 1/5 : FROM azul/zulu-openjdk-alpine:13-jre --- cb6c6216a7be Step 2/5 : COPY myApp.jar /tmp --- Using cache --- 438bd60500c0 Step 3/5 : COPY myconfigfile.txt /tmp --- Using cache --- 0bb6bce7b799 Step 4/5 : ENV CONFIG_FILE /tmp/myconfigfile.txt --- Using cache --- a37f6b18be1e Step 5/5 : CMD java -jar /tmp/myApp.jar --- Using cache --- 1a4e340225fa Successfully built 1a4e340225fa Successfully tagged my-dockerized-java-app:latest $ |
The above command built a docker image via the “docker build” command. The “-t” stands for –tag and is responsible for naming our image. I decided to use a the image name “my-dockerized-java-app” and added the optional tag “latest”. if you don’t specify a tag then the default assigned is “:latest”. Use whatever version scheme makes sense to you. if you want some examples, check out Docker Hub!
There is a dot “.” at the end of the command which is the meta-character for ‘present working directory’. This is how docker knows where to find all those files we’re copying and building the context from. Every Dockerfile instruction corresponds to an individual step in building the image layers. We see 5 steps in the output above (not to be confused with the steps I am listing out).
An important note .. Steps 1 through 4 (in the output above) will occur at build time and step 5 only at runtime … when we actually will run the container.
So where is the Docker image?
1 2 3 |
$ docker images my-dockerized-java-app REPOSITORY TAG IMAGE ID CREATED SIZE my-dockerized-java-app latest 1a4e340225fa 43 minutes ago 203MB |
Run Your Java Application in a Docker Container
With one simple command “docker container run“. The java spring boot application can be run in a docker container – Step 6.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
$ docker container run --rm my-dockerized-java-app . ____ _ __ _ _ /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \ ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \ \\/ ___)| |_)| | | | | || (_| | ) ) ) ) ' |____| .__|_| |_|_| |_\__, | / / / / =========|_|==============|___/=/_/_/_/ :: Spring Boot :: (v2.3.5.RELEASE) 2020-11-03 09:14:20.322 INFO 1 --- [ main] com.mvpjava.OptionalDemo : Starting OptionalDemo v0.0.1-SNAPSHOT on a7f6abcccf89 with PID 1 (/tmp/myApp.jar started by root in /) 2020-11-03 09:14:20.325 INFO 1 --- [ main] com.mvpjava.OptionalDemo : No active profile set, falling back to default profiles: default 2020-11-03 09:14:20.943 INFO 1 --- [ main] com.mvpjava.OptionalDemo : Started OptionalDemo in 1.013 seconds (JVM running for 1.454) Optional.of() threw NullPointerException because past in null No Conflicts detected getDefaultConflict{} has been called orElseGet conflict id: -1 Conflict{conflictId=1} Conflict{conflictId=2} Conflict{conflictId=87} |
The “--rm” is responsible for removing/cleaning up the container once it finishes running. We don’t want a whole bunch of docker container instances lying around taking up space after each invocation. The image name “my-dockerized-java-app” was used to tell the “docker container run” command which image to use. By default the “:latest” tag is chosen if not specified. The output of the application itself is specific to the Java Optional tutorial.
Remember that whole talk about CMD vs ENTRYPOINT? Since we used ENTRYPOINT in the Dockerfile, this container runs as an executable and we can’t accidentally override its executable nature. For example .. if we had used CMD instead then someone could override the containers default suggested command like this …
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
$ docker container run --rm my-dockerized-java-app ls bin dev etc home lib media mnt opt proc root run sbin srv sys tmp usr var |
The “ls” command overrode the CMD suggested default “java -jar /tmp/myApp.jar”. We don’t want this possibility for the users of this container therefore by using ENTRYPOINT this same command would have no effect. The java application would execute regardless. Of course there is still a way to override an ENTRYPOINT with the –-entrypoint command line argument.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
$ docker container run --entrypoint ls --rm my-dockerized-java-app bin dev etc home lib media mnt opt proc root run sbin srv sys tmp usr var |
I have only scratched the surface in regards to what can go into a Dockerfile and the arguments in running a Docker container. It’s a big Docker world but that is a good start in covering the high level steps with a few caveats.
Create Docker Image for Java Application | Summary
In order to run a Java application in a Docker container we have to go through the process known as containerizing or dockerizing the application. We covered the following 6 steps to make that happen.
- Select your docker base image
- COPY your Java application into image
- Provide any configuration your java application needs
- Launch your Java application
- Build your Java application Docker Image
- Run your Docker container with containerized java application
These high level steps don’t really change but rather increase in complexity as more and more details are added at each step. Deploying a java web application would not be so different.
If your interested in learning more docker or even getting docker certified then check out the following resource.
How I got Docker Certified Post.