Prerequisites
1) Install and configure a Tomcat cluster, and set the following environment variables in ~/.bashrc:
$TOMCAT1_HOME=<your tomcat1 home folder>
$TOMCAT2_HOME=<your tomcat2 home folder>
2) Verify Linux's builtin apache is installed:
$ sudo systemctl status apache2
● apache2.service - The Apache HTTP Server
Loaded: loaded (/lib/systemd/system/apache2.service;
enabled; vendor preset: enabled)
Active: active (running) since Sun 2025-09-21 12:43:12 CEST;
Create a new website
Specify in .bashrc the home folder of our new website:
$ tail -n 3 ~/.bashrc
export TOMCAT1_HOME="/apps/myapp/clus1/tomcat1"
export TOMCAT2_HOME="/apps/myapp/clus1/tomcat2"
export MYSITE_HOME="/var/sites/mysite"
$ source ~/.bashrc
, and also set it in /etc/apache2/envvars:
$ tail -n 1 /etc/apache2/envvars
export MYSITE_HOME=/var/sites/mysite
Create our new website's conf and www/html folders:
$ sudo mkdir -p $MYSITE_HOME/conf
$ sudo mkdir -p $MYSITE_HOME/www/html
Create an index.html inside $MYSITE_HOME/www/html:
$ cat $MYSITE_HOME/www/html/index.html
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>MySite Default Page: It works</title>
</head>
<body>
Welcome to MySite
</body>
</html>
Create a virtual host for our site
We copy the default conf from built-in apache:
$ sudo cp /etc/apache2/sites-available/000-default.conf \
$MYSITE_HOME/conf/localhost.conf
, and add the following lines:
$ cat $MYSITE_HOME/conf/localhost.conf
<VirtualHost *:80>
ServerName localhost
ServerAdmin webmaster@localhost
DocumentRoot ${MYSITE_HOME}/www/html
<Directory ${MYSITE_HOME}/www/>
Options Indexes FollowSymLinks
AllowOverride None
Require all granted
</Directory>
ErrorLog ${APACHE_LOG_DIR}/error.log
CustomLog ${APACHE_LOG_DIR}/access.log combined
</VirtualHost>
Enable our site
We first make our site available:
$ sudo ln -s $MYSITE_HOME/conf/localhost.conf \
/etc/apache2/sites-available/local-localhost.conf
, and then we enable it:
$ sudo a2ensite local-localhost
Enabling site local-localhost.
To activate the new configuration, you need to run:
systemctl reload apache2
Finally, stop and restart apache:
$ sudo systemctl stop apache2
$ sudo systemctl start apache2
, and test that our new site works:
$ curl http://localhost
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>MySite Default Page: It works</title>
</head>
<body>
Welcome to MySite
</body>
</html>
Set up mod_jk
There are two modules that I know for our apache web server to communicate with our backend Tomcat servers: mod_proxy and mod_jk.
I'm going to use the mod_jk plugin, which will communicate via the AJP protocol.
Enable AJP
First thing is to enable the AJP connectors in our Tomcats, which are disabled by default.
For that, we need to go to server.xml and uncomment the AJP connector element:
$ cat $TOMCAT1_HOME/conf/server.xml
<!-- Define an AJP 1.3 Connector on port 8009 -->
<Connector protocol="AJP/1.3"
address="localhost"
port="8009"
redirectPort="8443"
maxParameterCount="1000"
secretRequired="false"
/>
N.B:
- I had to add secretRequired="false"
- The defaul value in address is "::1", but that doesn't work for me, so I set it to "localhost", although "127.0.0.1" will also do.
Do the same for tomcat2 but don't forget to use a different port, cause both tomcats are on the same machine:
<!-- Define an AJP 1.3 Connector on port 9009 -->
<Connector protocol="AJP/1.3"
address="127.0.0.1"
port="9009"
redirectPort="8443"
maxParameterCount="1000"
secretRequired="false"
/>
After restarting the tomcats, we should see both AJP ports in use:
$ netstat -nlp|grep -E "8009|9009"
tcp6 0 0 127.0.0.1:8009 :::* LISTEN 60789/java
tcp6 0 0 127.0.0.1:9009 :::* LISTEN 120768/java
So our situation is that our examples application is available via our tomcats:
$ curl -s -o /dev/null -w "%{http_code}\n" http://localhost:8080/examples/
200
$ curl -s -o /dev/null -w "%{http_code}\n" http://localhost:9080/examples/
200
but not yet via our apache:
$ curl -s -o /dev/null -w "%{http_code}\n" http://localhost/examples/
404
That's what we will try to achieve with mod_jk: that requests for examples app are forwarded to the tomcats.
Configure and enable mod_jk
First we copy the default mod_jk conf:
$ sudo cp /etc/apache2/mods-available/jk.conf $MYSITE_HOME/conf/jk.conf
$ sudo cp /etc/apache2/mods-available/jk.load $MYSITE_HOME/conf/jk.load
Now we create 2 files inside $MYSITE_HOME/conf:
- workers.properties: will contain pointers to our tomcat AJP connectors and will define a load-balancer type worker.
- uriworkermap.properties: will contain the mapping from /examples uri to our load-balancer.
$ cat $MYSITE_HOME/conf/workers.properties
worker.list=worker1, worker2, lb
# Set properties for worker1 (our tomcat1 server)
worker.worker1.type=ajp13
worker.worker1.host=localhost
worker.worker1.port=8009
# Set properties for worker2 (our tomcat2 server)
worker.worker2.type=ajp13
worker.worker2.host=localhost
worker.worker2.port=9009
worker.lb.type=lb
worker.lb.balance_workers=worker1, worker2
$ cat $MYSITE_HOME/conf/uriworkermap.properties
# Send everything for context /examples to load-balancer worker
/examples/*=lb
Now we point to these 2 files in our jk.conf file:
$ cat $MYSITE_HOME/conf/jk.conf
...
JkWorkersFile ${MYSITE_HOME}/conf/workers.properties
...
JkMountFile ${MYSITE_HOME}/conf/uriworkermap.properties
The configuration is finished.
Next, we make our module available:
$ sudo ln -s $MYSITE_HOME/conf/jk.conf \
/etc/apache2/mods-available/local-jk.conf
$ sudo ln -s $MYSITE_HOME/conf/jk.load \
/etc/apache2/mods-available/local-jk.load
We now have to disable the default mod_jk cause only one JkWorkersFile is permitted:
$ sudo a2dismod jk
Next, enable our custom jk and reload:
$ sudo a2enmod local-jk
$ sudo systemctl reload apache2
Now, the forwarding should work:
$ curl http://localhost/examples/
<!DOCTYPE HTML><html lang="en"><head>
<meta charset="UTF-8">
<title>Apache Tomcat Examples</title>
</head>
<body>
<p>
<h3>Apache Tomcat Examples</H3>
<p></p>
<ul>
<li><a href="servlets">Servlets examples</a></li>
<li><a href="jsp">JSP Examples</a></li>
<li><a href="websocket/index.xhtml">WebSocket Examples</a></li>
</ul>
</body></html>
Next, we will test that load-balancing, sticky sessions and fail-over are all working as expected:
Testing load-balancing
To test that sessions are balanced, we can use servlet SessionExample in the examples app.
I have modified the following file
$TOMCAT_HOME/webapps/examples/WEB-INF/classes/LocalStrings.properties
in both tomcats to include the literal "1" and "2" respectively at the end of the following property:
sessions.title=Sessions Example Tomcat
, so I can easily identify to which tomcat requests are being forwarded.
Running different requests should bounce alternatively (or nearly) between tomcat1 and tomcat2:
$ curl http://localhost/examples/servlets/servlet/SessionExample|\
grep "<title>Sessions Example"
<title>Sessions Example Tomcat 1</title>
$ curl http://localhost/examples/servlets/servlet/SessionExample|\
grep "<title>Sessions Example"
<title>Sessions Example Tomcat 2</title>
$ curl http://localhost/examples/servlets/servlet/SessionExample|\
grep "<title>Sessions Example"
<title>Sessions Example Tomcat 1</title>
$ curl http://localhost/examples/servlets/servlet/SessionExample|\
grep "<title>Sessions Example"
<title>Sessions Example Tomcat 2</title>
Testing sticky sessions
For sticky sessions to work, we have to set the jvmRoute attribute in the engine element in tomcat's server.xml to the name of the respective node from workers.properties file (worker1 and worker2 in our case):
<Engine jvmRoute="worker1" name="Catalina" ...>
<Engine jvmRoute="worker2" name="Catalina" ...>
$ grep -r --include="server.xml" "jvmRoute=" /apps/myapp/clus1
/tomcat2/conf/server.xml: <Engine name="Catalina" defaultHost="localhost" jvmRoute="worker2">
/tomcat1/conf/server.xml: <Engine name="Catalina" defaultHost="localhost" jvmRoute="worker1">
To test that requests belonging to the same session "stick" to the same server, we can send the cookie JSESSIONID filled with the ID returned by the first request and check both that ID returned is the same and receiving tomcat too:
$ curl http://localhost/examples/servlets/servlet/SessionExample|\
grep "Session ID" -B 1
<h3>Sessions Example Tomcat 1</h3>
Session ID: 160ECEE2FE688B6649DC109F03EA1C7B.worker1
$ curl --cookie "JSESSIONID=160ECEE2FE688B6649DC109F03EA1C7B.worker1"\
http://localhost/examples/servlets/servlet/SessionExample|\
grep "Session ID" -B 1
<h3>Sessions Example Tomcat 1</h3>
Session ID: 160ECEE2FE688B6649DC109F03EA1C7B.worker1
Testing fail-over
We will proceed with the following sequence
- send a request
- note which tomcat served it and which session ID is returned
- stop that tomcat
- send another request for the same session via JESSIONID cookie
- verify that the other tomcat has served it and the JESSSIONID returned is the same
$ curl http://localhost/examples/servlets/servlet/SessionExample|\
grep "<h3>Sessions Example" -A 1
<h3>Sessions Example Tomcat 1</h3>
Session ID: 2D3B488194BBC78479370196455D4384.worker1
$ /apps/myapp/clus1/tomcat1/shutdown.sh
Using CATALINA_BASE: /apps/myapp/clus1/tomcat1
Using CATALINA_HOME: /opt/apache-tomcat-9.0.109
Using CATALINA_TMPDIR: /apps/myapp/clus1/tomcat1/temp
Using JRE_HOME: /opt/jdk1.8.0_451
Using CLASSPATH: /opt/apache-tomcat-9.0.109/bin/bootstrap.jar:
/opt/apache-tomcat-9.0.109/bin/tomcat-juli.jar
Using CATALINA_OPTS:
$ curl --cookie "JSESSIONID=2D3B488194BBC78479370196455D4384.worker1"\
http://localhost/examples/servlets/servlet/SessionExample|\
grep "<h3>Sessions Example" -A 1
<h3>Sessions Example Tomcat 2</h3>
Session ID: 2D3B488194BBC78479370196455D4384.worker2
Top comments (0)