Setup Tomcat 10 clustering with HAProxy load balancing for high availability

Advanced 45 min Apr 05, 2026 92 views
Ubuntu 24.04 Debian 12 AlmaLinux 9 Rocky Linux 9

Configure Apache Tomcat 10 clustering with HAProxy load balancing to distribute traffic across multiple application server instances. Implement session replication and automatic failover for production-grade Java web applications with high availability and zero-downtime deployments.

Prerequisites

  • Root or sudo access
  • Minimum 4GB RAM
  • Java application deployment experience
  • Basic understanding of HTTP load balancing

What this solves

Tomcat clustering with HAProxy provides high availability and load distribution for Java web applications by running multiple Tomcat instances that share session data and automatically handle failover. This setup eliminates single points of failure, improves application performance through load distribution, and enables zero-downtime deployments for mission-critical applications.

Step-by-step configuration

Update system packages

Start by updating your package manager to ensure you have the latest security patches and package versions.

sudo apt update && sudo apt upgrade -y
sudo dnf update -y

Install Java 17 and required packages

Install OpenJDK 17 and HAProxy. Java 17 is the recommended LTS version for Tomcat 10 with improved performance and security features.

sudo apt install -y openjdk-17-jdk haproxy wget curl
sudo dnf install -y java-17-openjdk-devel haproxy wget curl

Download and install Tomcat 10

Download the latest Tomcat 10 release and create the installation directory structure with proper permissions.

cd /opt
sudo wget https://archive.apache.org/dist/tomcat/tomcat-10/v10.1.28/bin/apache-tomcat-10.1.28.tar.gz
sudo tar -xzf apache-tomcat-10.1.28.tar.gz
sudo mv apache-tomcat-10.1.28 tomcat10
sudo useradd -r -s /bin/false tomcat
sudo chown -R tomcat:tomcat /opt/tomcat10

Create Tomcat cluster directories

Set up separate directories for each Tomcat instance in the cluster with proper ownership and permissions.

sudo mkdir -p /opt/tomcat-cluster/{node1,node2}
sudo cp -r /opt/tomcat10/* /opt/tomcat-cluster/node1/
sudo cp -r /opt/tomcat10/* /opt/tomcat-cluster/node2/
sudo chown -R tomcat:tomcat /opt/tomcat-cluster
sudo chmod +x /opt/tomcat-cluster/node1/bin/*.sh
sudo chmod +x /opt/tomcat-cluster/node2/bin/*.sh

Configure Tomcat Node 1

Configure the first Tomcat instance with clustering enabled, unique ports, and session replication settings.

<?xml version="1.0" encoding="UTF-8"?>
<Server port="8005" shutdown="SHUTDOWN">
  <Listener className="org.apache.catalina.startup.VersionLoggerListener" />
  <Listener className="org.apache.catalina.core.JreMemoryLeakPreventionListener" />
  <Listener className="org.apache.catalina.mbeans.GlobalResourcesLifecycleListener" />
  <Listener className="org.apache.catalina.core.ThreadLocalLeakPreventionListener" />

  <GlobalNamingResources>
    <Resource name="UserDatabase" auth="Container"
              type="org.apache.catalina.UserDatabase"
              description="User database that can be updated and saved"
              factory="org.apache.catalina.users.MemoryUserDatabaseFactory"
              pathname="conf/tomcat-users.xml" />
  </GlobalNamingResources>

  <Service name="Catalina">
    <Connector port="8080" protocol="HTTP/1.1"
               connectionTimeout="20000"
               redirectPort="8443"
               maxThreads="200"
               minSpareThreads="10" />

    <Connector port="8009" protocol="AJP/1.3" redirectPort="8443" />

    <Engine name="Catalina" defaultHost="localhost" jvmRoute="node1">
      
      <Cluster className="org.apache.catalina.ha.tcp.SimpleTcpCluster"
               channelSendOptions="8">
        
        <Manager className="org.apache.catalina.ha.session.DeltaManager"
                 expireSessionsOnShutdown="false"
                 notifyListenersOnReplication="true"/>
        
        <Channel className="org.apache.catalina.tribes.group.GroupChannel">
          <Membership className="org.apache.catalina.tribes.membership.McastService"
                      address="228.0.0.4"
                      port="45564"
                      frequency="500"
                      dropTime="3000"/>
          <Receiver className="org.apache.catalina.tribes.transport.nio.NioReceiver"
                    address="auto"
                    port="4000"
                    autoBind="100"
                    selectorTimeout="5000"
                    maxThreads="6"/>
          <Sender className="org.apache.catalina.tribes.transport.ReplicationTransmitter">
            <Transport className="org.apache.catalina.tribes.transport.nio.PooledParallelSender"/>
          </Sender>
          <Interceptor className="org.apache.catalina.tribes.group.interceptors.TcpFailureDetector"/>
          <Interceptor className="org.apache.catalina.tribes.group.interceptors.MessageDispatchInterceptor"/>
        </Channel>
        
        <Valve className="org.apache.catalina.ha.tcp.ReplicationValve"/>
        <Valve className="org.apache.catalina.ha.session.JvmRouteBinderValve"/>
        
        <Deployer className="org.apache.catalina.ha.deploy.FarmWarDeployer"
                  tempDir="/tmp/war-temp/"
                  deployDir="/tmp/war-deploy/"
                  watchDir="/tmp/war-listen/"
                  watchEnabled="false"/>
                  
        <ClusterListener className="org.apache.catalina.ha.session.ClusterSessionListener"/>
      </Cluster>

      <Realm className="org.apache.catalina.realm.LockOutRealm">
        <Realm className="org.apache.catalina.realm.UserDatabaseRealm"
               resourceName="UserDatabase"/>
      </Realm>

      <Host name="localhost"  appBase="webapps"
            unpackWARs="true" autoDeploy="true">
        <Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs"
               prefix="localhost_access_log" suffix=".txt"
               pattern="%h %l %u %t &quot;%r&quot; %s %b" />
      </Host>
    </Engine>
  </Service>
</Server>

Configure Tomcat Node 2

Configure the second Tomcat instance with different ports but same clustering configuration for session replication.

<?xml version="1.0" encoding="UTF-8"?>
<Server port="8006" shutdown="SHUTDOWN">
  <Listener className="org.apache.catalina.startup.VersionLoggerListener" />
  <Listener className="org.apache.catalina.core.JreMemoryLeakPreventionListener" />
  <Listener className="org.apache.catalina.mbeans.GlobalResourcesLifecycleListener" />
  <Listener className="org.apache.catalina.core.ThreadLocalLeakPreventionListener" />

  <GlobalNamingResources>
    <Resource name="UserDatabase" auth="Container"
              type="org.apache.catalina.UserDatabase"
              description="User database that can be updated and saved"
              factory="org.apache.catalina.users.MemoryUserDatabaseFactory"
              pathname="conf/tomcat-users.xml" />
  </GlobalNamingResources>

  <Service name="Catalina">
    <Connector port="8081" protocol="HTTP/1.1"
               connectionTimeout="20000"
               redirectPort="8444"
               maxThreads="200"
               minSpareThreads="10" />

    <Connector port="8010" protocol="AJP/1.3" redirectPort="8444" />

    <Engine name="Catalina" defaultHost="localhost" jvmRoute="node2">
      
      <Cluster className="org.apache.catalina.ha.tcp.SimpleTcpCluster"
               channelSendOptions="8">
        
        <Manager className="org.apache.catalina.ha.session.DeltaManager"
                 expireSessionsOnShutdown="false"
                 notifyListenersOnReplication="true"/>
        
        <Channel className="org.apache.catalina.tribes.group.GroupChannel">
          <Membership className="org.apache.catalina.tribes.membership.McastService"
                      address="228.0.0.4"
                      port="45564"
                      frequency="500"
                      dropTime="3000"/>
          <Receiver className="org.apache.catalina.tribes.transport.nio.NioReceiver"
                    address="auto"
                    port="4001"
                    autoBind="100"
                    selectorTimeout="5000"
                    maxThreads="6"/>
          <Sender className="org.apache.catalina.tribes.transport.ReplicationTransmitter">
            <Transport className="org.apache.catalina.tribes.transport.nio.PooledParallelSender"/>
          </Sender>
          <Interceptor className="org.apache.catalina.tribes.group.interceptors.TcpFailureDetector"/>
          <Interceptor className="org.apache.catalina.tribes.group.interceptors.MessageDispatchInterceptor"/>
        </Channel>
        
        <Valve className="org.apache.catalina.ha.tcp.ReplicationValve"/>
        <Valve className="org.apache.catalina.ha.session.JvmRouteBinderValve"/>
        
        <Deployer className="org.apache.catalina.ha.deploy.FarmWarDeployer"
                  tempDir="/tmp/war-temp/"
                  deployDir="/tmp/war-deploy/"
                  watchDir="/tmp/war-listen/"
                  watchEnabled="false"/>
                  
        <ClusterListener className="org.apache.catalina.ha.session.ClusterSessionListener"/>
      </Cluster>

      <Realm className="org.apache.catalina.realm.LockOutRealm">
        <Realm className="org.apache.catalina.realm.UserDatabaseRealm"
               resourceName="UserDatabase"/>
      </Realm>

      <Host name="localhost"  appBase="webapps"
            unpackWARs="true" autoDeploy="true">
        <Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs"
               prefix="localhost_access_log" suffix=".txt"
               pattern="%h %l %u %t &quot;%r&quot; %s %b" />
      </Host>
    </Engine>
  </Service>
</Server>

Create systemd services for Tomcat instances

Create systemd service files to manage each Tomcat instance with proper JVM settings and automatic restart capabilities.

[Unit]
Description=Apache Tomcat 10 Node 1
After=network.target

[Service]
Type=forking
User=tomcat
Group=tomcat

Environment=JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64
Environment=CATALINA_PID=/opt/tomcat-cluster/node1/temp/tomcat.pid
Environment=CATALINA_HOME=/opt/tomcat-cluster/node1
Environment=CATALINA_BASE=/opt/tomcat-cluster/node1
Environment="CATALINA_OPTS=-Xms512M -Xmx1024M -server -XX:+UseParallelGC"
Environment="JAVA_OPTS=-Djava.awt.headless=true -Djava.security.egd=file:/dev/./urandom"

ExecStart=/opt/tomcat-cluster/node1/bin/startup.sh
ExecStop=/opt/tomcat-cluster/node1/bin/shutdown.sh

RestartSec=10
Restart=always

[Install]
WantedBy=multi-user.target
[Unit]
Description=Apache Tomcat 10 Node 2
After=network.target

[Service]
Type=forking
User=tomcat
Group=tomcat

Environment=JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64
Environment=CATALINA_PID=/opt/tomcat-cluster/node2/temp/tomcat.pid
Environment=CATALINA_HOME=/opt/tomcat-cluster/node2
Environment=CATALINA_BASE=/opt/tomcat-cluster/node2
Environment="CATALINA_OPTS=-Xms512M -Xmx1024M -server -XX:+UseParallelGC"
Environment="JAVA_OPTS=-Djava.awt.headless=true -Djava.security.egd=file:/dev/./urandom"

ExecStart=/opt/tomcat-cluster/node2/bin/startup.sh
ExecStop=/opt/tomcat-cluster/node2/bin/shutdown.sh

RestartSec=10
Restart=always

[Install]
WantedBy=multi-user.target
Note: On RHEL-based systems, change the JAVA_HOME path to /usr/lib/jvm/java-17-openjdk to match the OpenJDK installation directory.

Configure HAProxy load balancer

Configure HAProxy to distribute traffic between Tomcat instances with sticky sessions, health checks, and automatic failover.

global
    log stdout local0
    chroot /var/lib/haproxy
    stats socket /run/haproxy/admin.sock mode 660 level admin
    stats timeout 30s
    user haproxy
    group haproxy
    daemon

defaults
    mode http
    log global
    option httplog
    option dontlognull
    option log-health-checks
    timeout connect 5000
    timeout client 50000
    timeout server 50000
    errorfile 400 /etc/haproxy/errors/400.http
    errorfile 403 /etc/haproxy/errors/403.http
    errorfile 408 /etc/haproxy/errors/408.http
    errorfile 500 /etc/haproxy/errors/500.http
    errorfile 502 /etc/haproxy/errors/502.http
    errorfile 503 /etc/haproxy/errors/503.http
    errorfile 504 /etc/haproxy/errors/504.http

frontend tomcat_frontend
    bind *:80
    bind *:443 ssl crt /etc/ssl/certs/example.com.pem
    redirect scheme https if !{ ssl_fc }
    
    # Add headers for backend servers
    http-request set-header X-Forwarded-Proto https if { ssl_fc }
    http-request set-header X-Forwarded-Proto http if !{ ssl_fc }
    
    default_backend tomcat_cluster

backend tomcat_cluster
    balance roundrobin
    
    # Enable sticky sessions using JSESSIONID cookie
    cookie JSESSIONID prefix nocache
    
    # Health check configuration
    option httpchk GET /
    http-check expect status 200
    
    # Tomcat server definitions
    server node1 127.0.0.1:8080 check cookie node1 inter 3000 fall 3 rise 2
    server node2 127.0.0.1:8081 check cookie node2 inter 3000 fall 3 rise 2
    
    # Session affinity backup
    option redispatch
    retries 3

HAProxy statistics page

listen stats bind *:8404 stats enable stats uri /stats stats refresh 30s stats admin if TRUE

Create test application with session support

Deploy a simple test application that demonstrates session replication across the cluster.

sudo mkdir -p /opt/tomcat-cluster/node1/webapps/cluster-test
sudo mkdir -p /opt/tomcat-cluster/node2/webapps/cluster-test
<%@ page session="true" %>
<%@ page import="java.util.*" %>
<html>
<head>
    <title>Tomcat Cluster Test</title>
</head>
<body>
    <h1>Tomcat Cluster Session Test</h1>
    
    <p><strong>Server Info:</strong></p>
    <ul>
        <li>Server: <%= request.getLocalAddr() + ":" + request.getLocalPort() %></li>
        <li>JVM Route: <%= System.getProperty("jvmRoute", "unknown") %></li>
        <li>Session ID: <%= session.getId() %></li>
        <li>Session New: <%= session.isNew() %></li>
        <li>Time: <%= new Date() %></li>
    </ul>
    
    <p><strong>Session Data:</strong></p>
    <%
        Integer visitCount = (Integer) session.getAttribute("visitCount");
        if (visitCount == null) {
            visitCount = 1;
        } else {
            visitCount++;
        }
        session.setAttribute("visitCount", visitCount);
    %>
    <p>Visit Count: <%= visitCount %></p>
    
    <p><a href="index.jsp">Refresh Page</a></p>
</body>
</html>
sudo cp /opt/tomcat-cluster/node1/webapps/cluster-test/index.jsp /opt/tomcat-cluster/node2/webapps/cluster-test/
sudo chown -R tomcat:tomcat /opt/tomcat-cluster/node*/webapps/cluster-test

Add web.xml for session replication

Configure the web application to enable session replication across the cluster.

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
         http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
         version="4.0">
         
    <display-name>Cluster Test Application</display-name>
    
    <!-- Enable session replication -->
    <distributable/>
    
    <welcome-file-list>
        <welcome-file>index.jsp</welcome-file>
    </welcome-file-list>
</web-app>
sudo mkdir -p /opt/tomcat-cluster/node2/webapps/cluster-test/WEB-INF
sudo cp /opt/tomcat-cluster/node1/webapps/cluster-test/WEB-INF/web.xml /opt/tomcat-cluster/node2/webapps/cluster-test/WEB-INF/
sudo chown -R tomcat:tomcat /opt/tomcat-cluster

Configure firewall rules

Open the necessary ports for HAProxy, Tomcat instances, and cluster communication.

sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw allow 8404/tcp
sudo ufw allow 4000:4001/tcp
sudo ufw allow from 228.0.0.4
sudo firewall-cmd --permanent --add-port=80/tcp
sudo firewall-cmd --permanent --add-port=443/tcp
sudo firewall-cmd --permanent --add-port=8404/tcp
sudo firewall-cmd --permanent --add-port=4000-4001/tcp
sudo firewall-cmd --permanent --add-source=228.0.0.4
sudo firewall-cmd --reload

Start and enable services

Start both Tomcat instances and HAProxy, then enable them to start automatically on system boot.

sudo systemctl daemon-reload
sudo systemctl enable --now tomcat-node1
sudo systemctl enable --now tomcat-node2
sudo systemctl enable --now haproxy

Test cluster failover

Test session replication

Verify that sessions are replicated between cluster nodes and HAProxy handles failover correctly.

# Test the application
curl -c cookies.txt -b cookies.txt http://localhost/cluster-test/

Check HAProxy stats

curl http://localhost:8404/stats

Test failover by stopping one node

sudo systemctl stop tomcat-node1 curl -c cookies.txt -b cookies.txt http://localhost/cluster-test/

Restart the stopped node

sudo systemctl start tomcat-node1

Monitor cluster communication

Check the Tomcat logs to verify cluster membership and session replication.

sudo tail -f /opt/tomcat-cluster/node1/logs/catalina.out
sudo tail -f /opt/tomcat-cluster/node2/logs/catalina.out
sudo journalctl -u haproxy -f

Performance optimization

Tune JVM memory settings

Optimize JVM heap settings based on your application requirements and available system memory.

# Update the CATALINA_OPTS line for production workloads
Environment="CATALINA_OPTS=-Xms1024M -Xmx2048M -server -XX:+UseG1GC -XX:+UseStringDeduplication"
sudo systemctl daemon-reload
sudo systemctl restart tomcat-node1
sudo systemctl restart tomcat-node2

Optimize HAProxy configuration

Fine-tune HAProxy settings for better performance and connection handling.

# Add these optimizations to the backend section
backend tomcat_cluster
    balance roundrobin
    cookie JSESSIONID prefix nocache
    
    # Performance optimizations
    option httpchk GET /cluster-test/
    http-check expect status 200
    
    # Connection optimization
    option http-server-close
    option forwardfor
    
    # Timeout optimization
    timeout server 30s
    timeout connect 3s
    
    server node1 127.0.0.1:8080 check cookie node1 inter 5000 fall 2 rise 3 maxconn 300
    server node2 127.0.0.1:8081 check cookie node2 inter 5000 fall 2 rise 3 maxconn 300

Verify your setup

# Check service status
sudo systemctl status tomcat-node1
sudo systemctl status tomcat-node2
sudo systemctl status haproxy

Test cluster connectivity

curl -I http://localhost/cluster-test/ curl http://localhost:8404/stats

Verify Tomcat clustering

sudo netstat -tlnp | grep :4000 sudo netstat -tlnp | grep :4001

Check cluster logs

sudo grep -i "member added" /opt/tomcat-cluster/node*/logs/catalina.out

Common issues

SymptomCauseFix
503 Service UnavailableTomcat instances not respondingCheck systemctl status and logs: sudo journalctl -u tomcat-node1
Sessions not replicatingMissing distributable tag in web.xmlAdd <distributable/> to web.xml and restart
Cluster nodes not joiningFirewall blocking multicastOpen ports 4000-4001 and allow multicast address 228.0.0.4
HAProxy health checks failingIncorrect health check pathVerify health check URL matches deployed application path
High memory usageJVM heap settings too largeAdjust -Xms and -Xmx values based on available RAM
SSL certificate errorsMissing or invalid certificateGenerate proper SSL certificate or use Let's Encrypt

Next steps

Automated install script

Run this to automate the entire setup

Need help?

Don't want to manage this yourself?

We handle managed cloud infrastructure for businesses that depend on uptime. From initial setup to ongoing operations.