We have to admit it, Java is still a popular programming language. Even if it may lack some of the fanciest features of modern languages, enterprises love it. But the history of Java should not stop us from modernizing our apps. In this Java Docker Tutorial, we will do exactly that, seeing how to create a container for tomcat.
Well, it is a Java Docker Tutorial after all. We assume you already know some Java and some Docker, even the basics. If you do, this is the place where you can glue things together. If you already know Java but you are lacking Docker knowledge, read this Docker tutorial first.
Building our Tomcat Package
In this post, we see how to create a container for a tomcat web application. That is, a web app served over HTTP or HTTPs, made in Java. Tomcat is the webserver application that will run your Java package.
Before we start working on our container, we need to create the .war
compressed archive for our app. This is a compressed folder that contains everything about our application, ready to run. Tomcat can read this file and publish the application.
Of course, if you already have a .war
file, you are ready to rock. Just skip to the next section,
Maven project structure
The best practice when it comes to building Java packages is to use Maven. This tool takes an XML configuration file that says how to build an application, and then builds it by looking at our project structure.
Of course, in order for all of this to work, you need to scaffold your files properly. This is how you should do it.
You should then put all your classes inside the java folder, following the Java package structure. For example, if your classes are in the package org.example
, you should create the subfolder org
, inside of it, the subfolder example
. Then, in this very last folder, you should add your classes.
Inside the WEB-INF
folder, you at least want to have the web.xml
file. This acts as an entry point for your .war package. It tells tomcat how to interact with your classes. It is in this file that you want to map associations between URLs and Servlets, for example.
And, finally, in the project root, you should have the pom.xml file. This file tells Maven how to build your project.
The web.xml file
Below, an example of web.xml that you may want to use for reference. Of course, you need to edit it if you want it to work. And, you also need to create the relevant classes.
<!DOCTYPE web-app PUBLIC
"-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/dtd/web-app_2_3.dtd" >
<web-app>
<display-name>Example project</display-name>
<!-- With <servlet>, we define a class that can process requests -->
<servlet>
<servlet-name>sampleServlet</servlet-name>
<display-name>Servlet to demonstrate the project is working</display-name>
<servlet-class>org.example.demo</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<!-- With <servlet-mapping>, we associate a <servlet> with a URL -->
<servlet-mapping>
<servlet-name>sampleServlet</servlet-name>
<url-pattern>/test-url</url-pattern>
</servlet-mapping>
</web-app>
The pom.xml
The great thing about pom.xml is that it can help you automate your build process. Think of it as an ancestor of CI/CD pipelines. For example, you can map dependencies inside of it.
Below, an example. You can mainly see three things:
- Description of the project (e.g. project name)
- Dependencies
- Build process and location of the source code
Of course, this example will also need to be adapted to your needs. This is just a Java Docker Tutorial, so going deeper on Maven is out of scope.
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example.demo</groupId>
<artifactId>demo</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>war</packaging>
<name>Demo Webapp</name>
<url>https://accelerates.it</url>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.0.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.4.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>5.4.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.maven</groupId>
<artifactId>maven-archiver</artifactId>
<version>3.4.0</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.1</version>
</dependency>
</dependencies>
<build>
<finalName>${project.artifactId}</finalName>
<sourceDirectory>src/main/java</sourceDirectory>
<testSourceDirectory>src/test/java</testSourceDirectory>
<testResources>
<testResource>
<directory>src/test/resources</directory>
</testResource>
</testResources>
<pluginManagement><!-- lock down plugins versions to avoid using Maven defaults (may be moved to parent pom) -->
<plugins>
<!-- Axis 2 build -->
<plugin>
<artifactId>maven-clean-plugin</artifactId>
<version>3.1.0</version>
</plugin>
<!-- see http://maven.apache.org/ref/current/maven-core/default-bindings.html#Plugin_bindings_for_war_packaging -->
<plugin>
<artifactId>maven-resources-plugin</artifactId>
<version>3.0.2</version>
</plugin>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.0</version>
</plugin>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.22.1</version>
</plugin>
<plugin>
<artifactId>maven-war-plugin</artifactId>
<version>3.2.2</version>
</plugin>
<plugin>
<artifactId>maven-install-plugin</artifactId>
<version>2.5.2</version>
</plugin>
<plugin>
<artifactId>maven-deploy-plugin</artifactId>
<version>2.8.2</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.22.1</version>
</plugin>
</plugins>
</pluginManagement>
</build>
</project>
Create your WAR Package
Once you are all set, in your project root you can run mvn war:war
to call Maven from the command line. This will tell tomcat that you want to get the .war
package.
mvn war:war
This will create your package in the target
folder by default.
Java Docker Tutorial: The Dockerfile
The Dockerfile is the build recipe for any container. It tells docker what to put inside of a container image. It is just a text file where you list a series of operations. So, in your project root, create a dockerfile
file (without any extension). Then, edit it with your favorite text editor (I love Microsoft VS Code, but Sublime is also a great choice).
The Basics of our Java Docker Container
The very first line of our Dockerfile should tell on which existing container image we want to base our container. We want to keep it simple, so we want to create a Linux Debian container, stripped out of any unnecessary stuff. So, the first line goes like this.
FROM debian:stretch
Then, we need to add all the dependencies that we need. For sure, we will need the Java Runtime (JRE), WGET and CURL to install tomcat, and unzip to work with our WAR package. I also addlibtcnative-1
, which improves tomcat performance in production.
Just after that, we need to define the environment variable for JAVA_HOME, mandatory to run Java. Since we are installing Java 8 OpenJDK with the default-jre,
we use the path you find below.
# Install packages
RUN \
apt-get update && \
apt-get upgrade -y && \
apt-get install -y default-jre && \
apt-get install -y libtcnative-1 && \
apt-get install -y wget && \
apt-get install -y curl && \
apt-get install -y unzip && \
apt-get install -y gettext-base
ENV JAVA_HOME="/usr/lib/jvm/java-8-openjdk-amd64/jre"
All good, now we need to install Tomcat.
How to Install Tomcat in a Docker Container
This is tricky but will save you a lot of time. Say we want Tomcat 9 in our application, but always to the latest patch and minor version. We need to download it from the Apache website, but the URL changes as the version progresses. Even worse, the URL of the non-latest version gets deleted whenever a new version comes out.
So, in this Java Docker Tutorial, we will see how to do things dynamically. We want docker to discover dynamically the latest version of tomcat, and use that one, every time we build the container image. We can do just that with the lines below.
RUN \
TOMCAT_VER=`curl --silent http://mirror.vorboss.net/apache/tomcat/tomcat-9/ | grep v9 -m 1 | awk '{split($5,c,">v") ; split(c[2],d,"/") ; print d[1]}'` && \
wget -N http://mirror.vorboss.net/apache/tomcat/tomcat-9/v${TOMCAT_VER}/bin/apache-tomcat-${TOMCAT_VER}.tar.gz &&\
tar xzf apache-tomcat-${TOMCAT_VER}.tar.gz && \
rm -f apache-tomcat-${TOMCAT_VER}.tar.gz && \
mv apache-tomcat-${TOMCAT_VER}/ /opt/tomcat
First, we fetch what is the latest version by extrapolating a string from the website. We store that in the TOMCAT_VER
environment variable. Then, we use that to fetch the compressed file, extract it, and put it into the /opt/tomcat
folder.
Just after that, we can set the CATALINA_HOME
and add it also to the path, so that we can start tomcat easily from any place of the terminal.
ENV CATALINA_HOME="/opt/tomcat" \
PATH="$PATH:/opt/tomcat/bin"
Copying the WAR file in the Docker Container
We need to add some flavor to our tomcat, we need to add our application. We do that by copying the .war
file into the folder containing web apps. Before that, however, we ensure no junk default app is present.
RUN rm -fr /opt/tomcat/webapps/*
Now we can actually do the copying.
COPY target/my-package-name.war /opt/tomcat/webapps/my-package-name.war
With this code, your application will be served at http://whatever/my-package-name
. Instead, if you want it to be at the root of your website (e.g. http://whatever/
), you should rename it to ROOT, like below.
COPY target/my-package-name.war /opt/tomcat/webapps/ROOT.war
You may also want to not copy the .war package as it is inside the web apps folder. Instead, you may want to extract it and copy the uncompressed folder. This is useful if you want to do some final adjustments, like injecting some other files inside it. If you want to do it, you should do it in the following way.
RUN mkdir /etc/temp
COPY in/target/my-package-name.war /etc/temp/my-package-name.war
RUN \
unzip /etc/temp/my-package-name.war -d /opt/tomcat/webapps/my-package-name && \
rm -fr /etc/temp
Okay, we are almost good, but now we need to spice our tomcat even a little bit more.
Configure Tomcat inside Docker
The best option we have is to create an XML file in the root of our project and name it tomcat-conf.xml
. In that file, we put all the configuration for tomcat, and then we use this file to override the default configuration that is already inside tomcat.
Again, this is a Java Docker Tutorial, so our goal here is not to learn how to configure Tomcat. Rather, we want to learn how to change its configuration inside Docker. Easy enough, we use the following command to copy the file. Note that the destination file and name must be exact.
COPY tomcat-conf.xml /opt/tomcat/conf/server.xml
We are almost all set, now we need to wrap up our container.
Finishing our Java Docker Container
At the end of our Dockerfile, we need to define what command to launch when starting the container, and which ports to expose. As a command, we obviously want to start tomcat. As ports, we normally expose port 80 and 443 by default, but you can tune it to adjust your needs.
We do that with the following lines.
CMD ["catalina.sh", "run"]
EXPOSE 80 443
Note that these are the default settings of the container image. You can override those when starting up a container if you want to.
Java Docker Tutorial Conclusion
In the end, Pushing a Java Tomcat application inside Docker is just a matter of the Docker file. In this Java Docker Tutorial, we saw how to do that by going step-by-step on our Dockerfile.
For your convenience, check the entire Dockerfile with comments on GitHub.com.