When building a real application you often find yourself having to deal with different stages of the software. The most common stages are development, testing and production, but you can have many more. This means that you need a different environment to deploy the application on each of the current stages. You use different environments to be able to perform versioning, different configurations, test bug fixes and so on. This also poses challenges on upgrading environments, changing shared configuration or keeping track of the servers. I will show you how to do it with Tomcat.
The easiest way to set up multiple Tomcat instances is to duplicate the entire Tomcat folder and change a few configurations. I don’t advise doing it this way, since it’s harder to maintain, harder to spin up new instances and harder to upgrade. Instead, we will set up the instances in a much more flexible way, by duplicating only a few things and keeping a shared base folder for all instances.
Installation
You need Tomcat of course. Download it here.
I’ve used version 7.0, but this should also work with other versions. I’m also doing the setup in a Unix like environment. This can also be accomplished in a Windows box, but the commands need to be adjusted.
Unzip the installation folder to a directory of your choice. I just recommend to do it in a parent folder and you can use a name like tomcat
or server
.
Now, instead of using the unzipped folder, we are going to create a link to it, like this:
ln -s apache-tomcat-7.0.64/ current
Here is a sample:
| radcortez:tomcat radcortez$ pwd /usr/local/share/java/tomcat radcortez:tomcat radcortez$ ln -s apache-tomcat-7.0.64/ current radcortez:tomcat radcortez$ ls total 8 drwxr-xr-x 4 radcortez admin 136B Sep 24 01:49 . drwxr-xr-x@ 26 radcortez admin 884B Sep 24 01:38 .. drwxr-xr-x 13 radcortez admin 442B Sep 24 01:45 apache-tomcat-7.0.64 lrwxr-xr-x 1 radcortez admin 21B Sep 24 01:49 current -> apache-tomcat-7.0.64/ radcortez:tomcat radcortez$ |
Setup
To keep this simple, we are going to create two instances: development
and production
. But keep in mind that you can create as many as you want by making the necessary adjustments to the scripts.
Folders
Create a folder now named instances
or environments
. Inside, create a folder named development
:
| radcortez:tomcat radcortez$ mkdir instances radcortez:tomcat radcortez$ cd instances/ radcortez:instances radcortez$ mkdir development radcortez:instances radcortez$ ls total 0 drwxr-xr-x 4 radcortez admin 136B Sep 24 01:56 . drwxr-xr-x 5 radcortez admin 170B Sep 24 01:56 .. drwxr-xr-x 2 radcortez admin 68B Sep 24 01:56 development radcortez:instances radcortez$ pwd /usr/local/share/java/tomcat/instances radcortez:instances radcortez$ |
Now copy the folders conf
, logs
, temp
, webapps
and work
from the Tomcat install folder into development
and production
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | radcortez:instances radcortez$ ls total 0 drwxr-xr-x 4 radcortez admin 136B Sep 24 02:06 . drwxr-xr-x 5 radcortez admin 170B Sep 24 01:56 .. drwxr-xr-x 2 radcortez admin 68B Sep 24 02:06 development drwxr-xr-x 2 radcortez admin 68B Sep 24 02:06 production radcortez:instances radcortez$ cp -rf ../current/conf/ development/conf radcortez:instances radcortez$ cp -rf ../current/logs/ development/logs radcortez:instances radcortez$ cp -rf ../current/temp/ development/temp radcortez:instances radcortez$ cp -rf ../current/webapps/ development/webapps radcortez:instances radcortez$ cp -rf ../current/work/ development/work radcortez:instances radcortez$ ls development/ total 0 drwxr-xr-x 7 radcortez admin 238B Sep 24 02:08 . drwxr-xr-x 4 radcortez admin 136B Sep 24 02:06 .. drwxr-xr-x 9 radcortez admin 306B Sep 24 02:07 conf drwxr-xr-x 2 radcortez admin 68B Sep 24 02:07 logs drwxr-xr-x 3 radcortez admin 102B Sep 24 02:08 temp drwxr-xr-x 7 radcortez admin 238B Sep 24 02:08 webapps drwxr-xr-x 2 radcortez admin 68B Sep 24 02:08 work radcortez:instances radcortez$ |
If you wish, you can now remove these folders from the Tomcat install folder, but is not mandatory.
Home and Base
The idea here is to share the main Tomcat folders and each instance has a copy of their personal folders to not clash with each other. Tomcat defines two environment variables called CATALINA_HOME
and CATALINA_BASE
that allow us to do that.
Create a bin
folder in the instances development
. Add the following exec.sh
script:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | #!/bin/bash TOMCAT_HOME="$(dirname $0)/.." cd $TOMCAT_HOME && TOMCAT_HOME=$PWD && cd - &> /dev/null export TOMCAT_HOME export CATALINA_HOME="$(readlink -f "$TOMCAT_HOME/../../current")" export CATALINA_BASE="$(readlink -f "$TOMCAT_HOME")" export CATALINA_OPTS="-Dhttp.port=8080 $CATALINA_OPTS" export CATALINA_OPTS="-Dhttps.port=8443 $CATALINA_OPTS" export CATALINA_OPTS="-Dajp.port=8009 $CATALINA_OPTS" export CATALINA_OPTS="-Dshutdown.port=8005 $CATALINA_OPTS" echo "JAVA_HOME set to $JAVA_HOME" echo "CATALINA_BASE set to $CATALINA_BASE" echo "CATALINA_HOME set to $CATALINA_HOME" $CATALINA_HOME/bin/"$(basename "$0")" "$@" |
Note: If you are using MacOSX, you might need to install core-utils
using brew
and replace readlink
by greadlink
to achieve the proper behaviour.
This script is going to set up the proper configuration variables to point to our shared Tomcat and the specific instance folders. Note the properties http.port
, https.port
, ajp.port
and shutdown.port
are included in the CATALINA_OPTS
environment variable. With these we can pass specific configuration to the server.xml
file. Tomcat is smart enough to perform property replace substitution as long as you have the proper placeholders in place.
Files
All these operations are performed in the development
folder instance.
conf/server.xml
Edit the file conf/server.xml
do the following changes;
Replace | By |
---|
8080 | ${http.port} |
8443 | ${https.port} |
8009 | ${ajp.port} |
Note: Unfortunately the only place where property replacement doesn’t work is the shutdown port. I think this is a bug in Tomcat and should be fixed. So for now, we need to keep it hardcoded.
bin/exec.sh
On the bin folder, create links to exec.sh
to the following files: catalina.sh
, startup.sh
, shutdown.sh
.
| radcortez:bin radcortez$ ln -s exec.sh catalina.sh radcortez:bin radcortez$ ln -s exec.sh startup.sh radcortez:bin radcortez$ ln -s exec.sh shutdown.sh radcortez:bin radcortez$ ls total 32 drwxr-xr-x 6 radcortez admin 204B Sep 24 10:14 . drwxr-xr-x 8 radcortez admin 272B Sep 24 08:07 .. lrwxr-xr-x 1 radcortez admin 7B Sep 24 10:13 catalina.sh -> exec.sh -rw-r--r-- 1 radcortez admin 651B Sep 24 09:32 exec.sh lrwxr-xr-x 1 radcortez admin 7B Sep 24 10:14 shutdown.sh -> exec.sh lrwxr-xr-x 1 radcortez admin 7B Sep 24 10:13 startup.sh -> exec.sh radcortez:bin radcortez$ |
This will allow you to call the original Tomcat, but by calling the exec.sh
set up first. The magic is done by the line $CATALINA_HOME/bin/"$(basename "$0")" "$@"
in the exec.sh
script.
Run
The instance should be ready to be executed. Just run it as you would do it normally by executing sh catalina.sh run
or sh startup.sh
from the development
instance folder.
Additional Instances
Just duplicate the development
instance folder to a production
one and edit the bin/exec.sh
to update it with different ports. You can user 7080 for http, 7443 for https, 7009 for ajp and 7005 for the shutdown.
Since property replacement is not working properly for the shutdown port, we need to manually edit conf/server.xml
from the production
instance and replace 8005 by 7005. When this bug is fixed, and you actually use a property, you don’t have to worry about doing this.
Note: You might need to reestablish the proper links in the scripts catalina.sh
, startup.sh
and shutdown.sh
stored in the bin
folder.
After this, your second instance production
is ready to run. If you need more, just repeat the last steps, making sure to pick ports that don’t conflict with the instances already set up.
Perks
With this set up you can now:
- Create new instances easily with minimum changes. You can actually have one untouched unchanged instance, that you can use to copy from to create others.
- Update the Tomcat version, just by installing a new distribution and updating the link to
current
. - If you place jars in the
libs
folder of the HOME
installation, they become instantly available to all instances. - Instead of duplicating the
conf
folder, you can actually link to the one in HOME
and also share the configuration between all environments. Or just link to the files you want to share. - Remove an instance, by just deleting its
BASE
folder. - Also works for TomEE!
Alternatives
If you don’t like this set up, you can try using Docker. Check the following post: Get Into Docker.
Let me know if this was useful to you or if you had any trouble following the blog instructions!
Since I started this blog, I had the need to develop a couple of sample applications to showcase some of the topics I have been covering. Usually, some kind of Java EE application that needs to be deployed in a Java EE container. Even by providing instructions on how to setup the environment, it can be tricky for a newcomer. A few of my readers don’t have a Java EE container available in their local machine. Some don’t even have Java Development Kit installed. If I could provide the entire environment set up for you and you only need to execute it somehow, wouldn’t that be great? I do think so! Instead of distributing only the application, also distribute the environment needed for the application to run. We can do that using Docker.
A couple of weeks ago, I wrote the post Get into Docker. Now, this post is going to continue to explore one of the most interesting Docker features: the Docker Images. This is the answer to provide my reader with a complete environment with everything ready to run.
Docker Image
A Docker Image is a read only template used to create the Docker containers. Each image is built with a series of layers composing your final image. If you need to distribute something using Ubuntu and Apache, you start with a base Ubuntu image and add Apache on top.
Create a Docker Image file
I’m going to use one my latest application, the World of Warcraft Auction House, to show how we can package it into a Docker Image and distribute it to others. The easiest way is to create a Dockerfile. This is a simple plain text file that contains a set of instructions that tells Docker how to build our image. The instructions that you can use are well defined and straightforward. Check the Dockerfile reference page for a list of possible instructions. Each instruction adds a new layer to your Docker Images. Usually the Dockerfile is named Dockerfile
. Place it in a directory of your choice.
Base Image
Every Dockerfile needs to start with a FROM
instruction. We need to start from somewhere, so this indicates the base image that we are going to use to build our environment. If you were building a Virtual Machine you also had to start from somewhere, and you have to start by picking up the Operating System that you are going to use. With the Dockerfile it’s no different. Let’s add the following to the Dockerfile:
FROM debian:latest
Our base image will be the latest version of Debian, available here: Docker Hub Debian Repo.
Add what you need
The idea here is to build an environment that checkouts the code, build and execute the World of Warcraft Auction House sample. Can you figure out what you need? The JDK of course, to compile and run and Maven to perform the build. But these are not enough. You also need the Git command line client to checkout the code. For this part you need to know a little bit about Unix shell scripting.
Since we need to install the JDK and Maven, we need to download them into our image from somewhere. You can use the wget
command to do it. But wget
is not available in our base Debian image so we need to install it first. To run shell commands we use the RUN
instruction in the Dockerfile.
Install wget:
| RUN apt-get update && apt-get -y install wget git |
Install the JDK:
| RUN wget --no-check-certificate --header "Cookie: oraclelicense=accept-securebackup-cookie" http://download.oracle.com/otn-pub/java/jdk/8u40-b25/jdk-8u40-linux-x64.tar.gz && \ mkdir /opt/jdk && \ tar -zxf jdk-8u40-linux-x64.tar.gz -C /opt/jdk && \ update-alternatives --install /usr/bin/java java /opt/jdk/jdk1.8.0_40/bin/java 100 && \ update-alternatives --install /usr/bin/javac javac /opt/jdk/jdk1.8.0_40/bin/javac 100 && \ rm -rf jdk-8u40-linux-x64.tar.gz |
Install Maven:
| RUN wget http://mirrors.fe.up.pt/pub/apache/maven/maven-3/3.2.5/binaries/apache-maven-3.2.5-bin.tar.gz && \ tar -zxf apache-maven-3.2.5-bin.tar.gz -C /opt/ && \ rm -rf apache-maven-3.2.5-bin.tar.gz |
We also need to have Java and Maven accessible from anywhere in our image. As you would do to your local machine when setting the environment, you need to set JAVA_HOME
and add Maven binary to the PATH
. You can do this by using Docker ENV
instruction.
| ENV JAVA_HOME /opt/jdk/jdk1.8.0_40/ ENV PATH /opt/apache-maven-3.2.5/bin:$PATH |
Add the application
Now that we have the required environment for the World of Warcraft Auction House, we just need to clone the code and build it:
| RUN cd opt && \ git clone https://github.com/radcortez/wow-auctions.git wow-auctions WORKDIR /opt/wow-auctions/ RUN mvn clean install && \ cd batch && \ mvn wildfly:start |
We also want to expose a port, so you can access the application. You should use the listening http port of the application server. In this case, it’s 8080. You can do this in Docker with the EXPOSE
instruction:
I had to use a little trick here. I don’t want to download and install the application server, so I’m using the embedded Wildfly version of the Maven plugin. Now, as I told you before, each instruction of the Dockerfile adds a new layer to the image. In here I’m forcing a start and stop of the server, just for Maven to download the required dependencies and have them available in the image. If I didn’t do this, whenever I wanted to run the image, I would have to download all the server dependencies and the startup of the image would take considerably longer.
Run the application
The final instruction should be a CMD
to set the command to be executed when running the image:
| CMD git pull && cd batch && mvn wildfly:run |
In this case we want to make sure we are using the latest code, so we do a git pull
and then run the embedded Wildfly server. The deploy configuration has been already set up in Wildfly Maven plugin.
Complete Dockerfile
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | FROM debian:latest MAINTAINER Roberto Cortez <radcortez@yahoo.com> RUN apt-get update && apt-get -y install wget git RUN wget --no-check-certificate --header "Cookie: oraclelicense=accept-securebackup-cookie" http://download.oracle.com/otn-pub/java/jdk/8u40-b25/jdk-8u40-linux-x64.tar.gz && \ mkdir /opt/jdk && \ tar -zxf jdk-8u40-linux-x64.tar.gz -C /opt/jdk && \ update-alternatives --install /usr/bin/java java /opt/jdk/jdk1.8.0_40/bin/java 100 && \ update-alternatives --install /usr/bin/javac javac /opt/jdk/jdk1.8.0_40/bin/javac 100 && \ rm -rf jdk-8u40-linux-x64.tar.gz ENV JAVA_HOME /opt/jdk/jdk1.8.0_40/ RUN wget http://mirrors.fe.up.pt/pub/apache/maven/maven-3/3.2.5/binaries/apache-maven-3.2.5-bin.tar.gz && \ tar -zxf apache-maven-3.2.5-bin.tar.gz -C /opt/ && \ rm -rf apache-maven-3.2.5-bin.tar.gz ENV PATH /opt/apache-maven-3.2.5/bin:$PATH RUN cd opt && \ git clone https://github.com/radcortez/wow-auctions.git wow-auctions WORKDIR /opt/wow-auctions/ RUN mvn clean install && \ cd batch && \ mvn wildfly:start EXPOSE 8080 CMD git pull && cd batch && mvn wildfly:run |
Build the Dockerfile
To be able to distribute your image, you need to build your Dockerfile. What this is going to do, is to read every instruction, execute it and add a layer to your Docker Images. You only need to do this once, unless you change your Dockerfile. The CMD
instruction is not executed in the build, since it’s only used when you are actually running the image and executing the container.
To build the Dockerfile, I use the following command in the directory containing your Dockerfile:
docker build -t radcortez/wow-auctions .
The -t radcortez/wow-auctions
is to tag and name the image I’m building. You should use the format user/name. You should use the same user name that you register with Docker Hub.
Pushing the Image
Docker Hub is a Docker Image repository. It’s the same concept of Maven repositories for Java libraries. Download or upload images and you are good to go. The Docker Hub already contains a huge number of images ready to use, from simple Unix distributions, to full blown application servers.
We can now pick the image we build locally and upload it to Docker Hub. This will allow anyone to download and use this image. We can do it like this:
docker push radcortez/wow-auctions
Depending on the image size, this can take a few minutes.
Run the Image
Finally to run the image and the container we execute:
docker run -it --name wow-auctions -p 8080:8080 radcortez/wow-auctions
Since I’ve built the image locally first, this will run the CMD
radcortez/wow-auctions. Just by using the above command, the image is going to be downloaded and executed in your environment.
Conclusion
With Docker, is possible to distribute your own applications and have the required environment for the application to run properly created by you. It’s not exactly trivial, since you need some knowledge of Unix, but it’s shouldn’t be a problem.
My main motivation to use Docker here, was to simplify the distribution of my sample applications. It’s not unusual to receive a few reader emails asking for help to set up their environment. Sure, in this way you now have to install Docker too, but that’s the only thing you need. The rest, just leave it to me now!
Related Articles
Remember to check my introductory post about Docker:
Get Into Docker
Have you ever heard about Docker before? Most likely. If not, don’t worry, I’ll try to summarize it for you. Docker is probably one of the hottest technologies at the moment. It has the potential to revolutionize the way we build, deploy and distribute applications. At the same time, it’s already having a huge impact in the development process.
In some cases, the development environments can be so much complicated, that it’s hard to keep the consistency between the different team members. I’m pretty sure that most of us already suffered from the syndrome “Works on my Machine”, right? One way to deal with the problem is to build Virtual Machines (VM) with everything set up so you can distribute them through your team. But VM’s are slow, large and you cannot access them if they are not running.
What is Docker?
Short answer: it’s like a lightweight VM. In practice, it’s not the case, since Docker is different from a regular VM. Docker creates a container for your application, packaged with all of the required dependencies and ready to run. These containers run on a shared Linux kernel, but they are isolated from each other. This means that you don’t need the usual VM operating system, giving a considerable performance boost and shrinking the application size.
Let’s dig a little more into detail:
Docker Image
A Docker Image is a read only template used to create the Docker containers. Each image is built with a series of layers composing your final image. If you need to distribute something using Ubuntu and Apache, you start with a base Ubuntu image and add Apache on top. If you later want to upgrade to a Tomcat instance, you just add another layer to your image. Instead of distributing the entire image as you would with a VM, you just release the update.
Docker Registry
The Docker registry also called Docker Hub is a Docker Image repository. It’s the same concept of Maven repositories for Java libraries. Download or upload images and you are good to go. The Docker Hub already contains a huge number of images ready to use, from simple Unix distributions, to full blown application servers.
Docker Container
A Docker Container is the runtime component of the Docker Image. You can spin multiple containers from the same Docker Image in an isolated context. Docker containers can be run, started, stopped, moved, and deleted.
How do I start?
You need to install Docker of course. Please refer to the installation guides of Docker. They are pretty good and I had no problem installing the software. Make sure you follow the proper guide to your system.
Our first Docker Container
After having Docker installed, you can immediately type in your command line:
docker run -it -p 8080:8080 tomcat
You should see the following message:
Unable to find image ‘tomcat:latest’ locally
And a lot of downloads starting. Like Maven, when you build an application, it downloads the required libraries to run Tomcat, by reaching out to Docker Hub. It takes a while to download. (Great, one more thing to download the Internet. Luckily we can use ZipRebel, to download it quickly).
After everything is downloaded, you should see the Tomcat instance booting up, and you can access it by going to http://localhost:8080
in Linux boxes. For Windows and Mac users is slightly more complicated. Since Docker only works in a Linux environment, to be able to use it in Windows and Mac you need boot2docker (which you should have from the installation guide). This is in fact a VM that runs Docker on Linux completely from memory. To access the Docker containers you need to refer to this VM IP. You can get the IP with the command: boot2docker ip
.
Explaining the command:
docker run | The command to create and start a new Docker container. |
-it | To run in interactive mode, so you can see the after running the container. |
-p 8080:8080 | This is to map the internal container port to the outside host, usually your machine. Port mapping information can only be set on the container creation. If you don’t specify it, you need to check which port Docker assigned |
tomcat | Name of the image to run. This is linked to the Docker tomcat repository. This holds the instructions, so Docker knows how to run the server. |
Remember that if you stop and run again the same command, you are creating and running a new container.
Multiple Containers
You can run multiple Tomcat instances by issuing the following commands:
docker run -d -p 8080:8080 --name tomcat tomcat
docker run -d -p 9090:8080 --name web tomcat
These create two Tomcat containers named tomcat and web. Just remember to change the port mapping and the name. Adding a name is useful to control the container. If not, Docker will randomly generate one for you.
The -d
instructs Docker to run the container in the background. You can now control your container with the following commands:
docker ps | See a list of all the running Docker containers. Add -a to see all the containers. |
docker stop web | Stops the container named web.
|
docker start web | Starts the container named web.
|
docker rm web | Remove the container named web.
|
docker logs web | Shows the container named web logs.
|
Connecting to the Container
If you execute the command docker exec -it tomcat bash
, you will be able to connect to the container shell and explore the environment. You can for instance, verify the running processes with ps -ax
.
| radcortez:~ radcortez$ docker exec -it web bash root@75cd742dc39e:/usr/local/tomcat# ps -ax PID TTY STAT TIME COMMAND 1 ? Ssl+ 0:05 /usr/bin/java -Djava.util.logging.config.file=/usr/local/tomcat/conf/logging.properties -Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager -Djava.endorsed.dirs= 47 ? S 0:00 bash 51 ? R+ 0:00 ps -ax root@75cd742dc39e:/usr/local/tomcat# |
Interacting with the Container
Let’s add a file to the container:
echo "radcortez" > radcortez
Exit the container, but keep it running. Execute docker diff web
. You are going to see a bunch of files related to the tomcat temporary files, plus the file we just added. This command evaluates the file system differences between the running container and the origin image.
Conclusion
We only scratched the surface of Docker capabilities. It’s still soon to tell if Docker will become a mandatory tool. Currently it’s receiving major adoption from big players like Google, Microsoft or Amazon. Docker may end up failing in the end, but it sure opened up an old discussion which doesn’t have a clear answer yet.
Related Articles
Learn how to create, build and distribute your own Docker Images in this follow up post:
Distribute your applications with Docker Images