Setup Tomcat 10 clustering with HAProxy load balancing for high availability

Advanced 45 min Apr 05, 2026 1,163 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

Automated install script

Run this to automate the entire setup

Don't want to manage this yourself?

We handle infrastructure for businesses that depend on uptime. Fully managed, with one fixed contact who knows your setup.

You get one fixed contact who knows your setup

Rotterdam 01:25 · reachable in a message, no ticket form