Apache Tomcat Server Configuration

Apache Tomcat Server Configuration Guide for DevOps

Introduction

Apache Tomcat remains one of the most popular Java application servers, powering millions of enterprise applications worldwide. For DevOps engineers, understanding Tomcat configuration is crucial for deploying, scaling, and maintaining Java web applications efficiently.

Tomcat Architecture Overview

Core Components:

  • Server: Top-level element representing entire Tomcat instance
  • Service: Groups Connectors and Engine together
  • Connector: Handles client connections (HTTP, AJP, etc.)
  • Engine: Request processing engine
  • Host: Virtual host configuration
  • Context: Web application configuration

Directory Structure:

tomcat/
├── bin/           # Startup/shutdown scripts
├── conf/          # Configuration files
├── lib/           # Libraries and JAR files
├── logs/          # Log files
├── webapps/       # Deployed applications
├── work/          # Temporary working files
└── temp/          # Temporary files
                

Key Configuration Files

File Purpose Location
server.xml Main configuration file conf/server.xml
web.xml Default web application deployment descriptor conf/web.xml
context.xml Default context configuration conf/context.xml
tomcat-users.xml User authentication and roles conf/tomcat-users.xml
logging.properties Logging configuration conf/logging.properties
catalina.properties Java classloader and security settings conf/catalina.properties

Server.xml Configuration

Basic Server Configuration:

<?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.AprLifecycleListener" SSLEngine="on" />
    <Listener className="org.apache.catalina.core.JreMemoryLeakPreventionListener" />
    
    <Service name="Catalina">
        
        <Connector port="8080" protocol="HTTP/1.1"
                   connectionTimeout="20000"
                   redirectPort="8443"
                   maxThreads="200"
                   minSpareThreads="10"
                   enableLookups="false"
                   acceptCount="100"
                   maxPostSize="2097152" />
        
        <Connector port="8009" protocol="AJP/1.3"
                   redirectPort="8443"
                   secretRequired="false" />
        
        <Engine name="Catalina" defaultHost="localhost">
            
            <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">
                
                <Context path="" docBase="myapp" reloadable="true">
                    <WatchedResource>WEB-INF/web.xml</WatchedResource>
                </Context>
                
                <Valve className="org.apache.catalina.valves.AccessLogValve"
                       directory="logs"
                       prefix="localhost_access_log" suffix=".txt"
                       pattern="%h %l %u %t "%r" %s %b" />
            </Host>
        </Engine>
    </Service>
</Server>
                

Advanced Connector Configuration:

<Connector port="8080" protocol="HTTP/1.1"
           connectionTimeout="20000"
           maxThreads="500"
           minSpareThreads="25"
           maxSpareThreads="75"
           acceptCount="1000"
           maxConnections="10000"
           compression="on"
           compressionMinSize="1024"
           compressableMimeType="text/html,text/xml,text/plain,text/css,text/javascript,application/json"
           server="Apache Tomcat"
           address="0.0.0.0"
           URIEncoding="UTF-8"
           useBodyEncodingForURI="true"
           enableLookups="false"
           disableUploadTimeout="true"
           connectionUploadTimeout="300000"
           keepAliveTimeout="60000"
           maxKeepAliveRequests="100" />
                

Performance Tuning Configuration

JVM Memory Settings:

# In bin/setenv.sh (create if doesn't exist)
export CATALINA_OPTS="$CATALINA_OPTS -server"
export CATALINA_OPTS="$CATALINA_OPTS -Xms2g -Xmx4g"
export CATALINA_OPTS="$CATALINA_OPTS -XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=512m"
export CATALINA_OPTS="$CATALINA_OPTS -XX:+UseG1GC"
export CATALINA_OPTS="$CATALINA_OPTS -XX:MaxGCPauseMillis=200"
export CATALINA_OPTS="$CATALINA_OPTS -XX:ParallelGCThreads=4"
export CATALINA_OPTS="$CATALINA_OPTS -XX:ConcGCThreads=2"
export CATALINA_OPTS="$CATALINA_OPTS -Djava.awt.headless=true"
export CATALINA_OPTS="$CATALINA_OPTS -Djava.security.egd=file:/dev/./urandom"

# For production with Docker
export JAVA_OPTS="-Xms1g -Xmx2g -XX:+UseContainerSupport -XX:MaxRAMPercentage=75.0"
                

Connector Thread Pool Tuning:

<Connector port="8080" protocol="HTTP/1.1"
           maxThreads="1000"
           minSpareThreads="100"
           maxSpareThreads="200"
           acceptCount="2000"
           maxConnections="10000"
           connectionTimeout="30000"
           keepAliveTimeout="60000"
           maxKeepAliveRequests="100"
           processorCache="2000"
           socketBuffer="9000"
           tcpNoDelay="true" />
                

Session Configuration:

<Context>
    <Manager className="org.apache.catalina.session.PersistentManager"
             saveOnRestart="true"
             maxActiveSessions="10000"
             minIdleSwap="300"
             maxIdleSwap="600"
             maxIdleBackup="0">
        <Store className="org.apache.catalina.session.FileStore"
               directory="../sessions"/>
    </Manager>
    
    <!-- Or for Redis session storage -->
    <Manager className="org.redisson.tomcat.RedissonSessionManager"
             configPath="${catalina.base}/conf/redisson.yaml"
             readMode="REDIS"
             updateMode="DEFAULT"
             broadcastSessionEvents="false"/>
</Context>
                

Security Configuration

Tomcat Users and Roles:

<?xml version="1.0" encoding="UTF-8"?>
<tomcat-users xmlns="http://tomcat.apache.org/xml"
              xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
              xsi:schemaLocation="http://tomcat.apache.org/xml tomcat-users.xsd"
              version="1.0">
    
    <role rolename="manager-gui"/>
    <role rolename="manager-script"/>
    <role rolename="manager-jmx"/>
    <role rolename="manager-status"/>
    <role rolename="admin-gui"/>
    <role rolename="admin-script"/>
    
    <user username="admin" password="{bcrypt}$2a$10$encryptedpassword" 
          roles="manager-gui,manager-script,manager-jmx,manager-status,admin-gui,admin-script"/>
    
    <user username="deployer" password="{bcrypt}$2a$10$differentencrypted" 
          roles="manager-script"/>
</tomcat-users>
                

Security Hardening:

# Remove default applications
rm -rf webapps/docs webapps/examples webapps/manager webapps/host-manager

# Secure server.xml shutdown port
<Server port="8005" shutdown="COMPLEX_PASSWORD_HERE">

# Disable server version disclosure
<Connector port="8080" server=" " />

# In web.xml, add security headers
<filter>
    <filter-name>httpHeaderSecurity</filter-name>
    <filter-class>org.apache.catalina.filters.HttpHeaderSecurityFilter</filter-class>
    <init-param>
        <param-name>antiClickJackingEnabled</param-name>
        <param-value>true</param-value>
    </init-param>
    <init-param>
        <param-name>antiClickJackingOption</param-name>
        <param-value>SAMEORIGIN</param-value>
    </init-param>
</filter>

<filter-mapping>
    <filter-name>httpHeaderSecurity</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>
                

Logging Configuration

Logback Configuration (Recommended):

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>${catalina.base}/logs/myapp.log</file>
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>${catalina.base}/logs/myapp.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
            <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                <maxFileSize>100MB</maxFileSize>
            </timeBasedFileNamingAndTriggeringPolicy>
            <maxHistory>30</maxHistory>
            <totalSizeCap>3GB</totalSizeCap>
        </rollingPolicy>
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>

    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>

    <logger name="com.myapp" level="DEBUG" />
    <logger name="org.springframework" level="INFO" />
    <logger name="org.apache.catalina" level="WARN" />

    <root level="INFO">
        <appender-ref ref="FILE" />
        <appender-ref ref="CONSOLE" />
    </root>
</configuration>
                

Access Log Configuration:

<Valve className="org.apache.catalina.valves.AccessLogValve" 
       directory="logs"
       prefix="access_log" 
       suffix=".txt"
       pattern="%{yyyy-MM-dd HH:mm:ss}t %a "%r" %s %b %D "%{Referer}i" "%{User-Agent}i""
       rotatable="true"
       conditionUnless="healthcheck"
       fileDateFormat="yyyy-MM-dd">
    <!-- JSON format for structured logging -->
    <pattern>{"timestamp":"%t","clientIp":"%a","method":"%m","uri":"%U","query":"%q","status":"%s","bytes":"%b","duration":"%D","referer":"%{Referer}i","userAgent":"%{User-Agent}i"}</pattern>
</Valve>
                

SSL/HTTPS Configuration

SSL Connector Configuration:

<Connector port="8443" 
           protocol="org.apache.coyote.http11.Http11NioProtocol"
           maxThreads="150" 
           SSLEnabled="true">
    
    <SSLHostConfig>
        <Certificate certificateKeystoreFile="conf/keystore.jks"
                     certificateKeystorePassword="changeit"
                     type="RSA" />
    </SSLHostConfig>
    
    <UpgradeProtocol className="org.apache.coyote.http2.Http2Protocol" />
</Connector>
                

Let's Encrypt with Certbot:

# Generate certificate
certbot certonly --standalone -d yourdomain.com

# Convert to Java Keystore
openssl pkcs12 -export -in /etc/letsencrypt/live/yourdomain.com/fullchain.pem \
               -inkey /etc/letsencrypt/live/yourdomain.com/privkey.pem \
               -out fullchain_and_key.p12 -name tomcat \
               -passout pass:yourpassword

keytool -importkeystore -deststorepass yourpassword \
        -destkeypass yourpassword -destkeystore keystore.jks \
        -srckeystore fullchain_and_key.p12 -srcstoretype PKCS12 \
        -srcstorepass yourpassword -alias tomcat

# Auto-renewal script
#!/bin/bash
certbot renew --pre-hook "systemctl stop tomcat" \
              --post-hook "systemctl start tomcat" \
              --deploy-hook "/opt/tomcat/update-certificate.sh"
                

Docker Containerization

Dockerfile Example:

FROM tomcat:9.0-jdk11-openjdk-slim

# Remove default applications
RUN rm -rf /usr/local/tomcat/webapps/*

# Install required tools
RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*

# Copy custom configuration
COPY conf/server.xml /usr/local/tomcat/conf/
COPY conf/web.xml /usr/local/tomcat/conf/
COPY conf/context.xml /usr/local/tomcat/conf/
COPY bin/setenv.sh /usr/local/tomcat/bin/

# Copy application WAR
COPY target/myapp.war /usr/local/tomcat/webapps/ROOT.war

# Create non-root user
RUN groupadd -r tomcat && useradd -r -g tomcat tomcat
RUN chown -R tomcat:tomcat /usr/local/tomcat
USER tomcat

# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=60s --retries=3 \
    CMD curl -f http://localhost:8080/health || exit 1

EXPOSE 8080
CMD ["catalina.sh", "run"]
                

Docker Compose:

version: '3.8'
services:
  tomcat:
    build: .
    ports:
      - "8080:8080"
      - "8443:8443"
    environment:
      - JAVA_OPTS=-Xms1g -Xmx2g -Dspring.profiles.active=prod
      - CATALINA_OPTS=-Djava.security.egd=file:/dev/./urandom
    volumes:
      - tomcat_logs:/usr/local/tomcat/logs
      - tomcat_data:/usr/local/tomcat/webapps
    networks:
      - app-network
    deploy:
      resources:
        limits:
          memory: 3G
          cpus: '2.0'
        reservations:
          memory: 1G
          cpus: '0.5'

  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf
    depends_on:
      - tomcat
    networks:
      - app-network

volumes:
  tomcat_logs:
  tomcat_data:

networks:
  app-network:
    driver: bridge
                

Kubernetes Deployment

Deployment Configuration:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: tomcat-app
  labels:
    app: tomcat
spec:
  replicas: 3
  selector:
    matchLabels:
      app: tomcat
  template:
    metadata:
      labels:
        app: tomcat
    spec:
      containers:
      - name: tomcat
        image: myregistry/tomcat-app:latest
        ports:
        - containerPort: 8080
        - containerPort: 8443
        env:
        - name: JAVA_OPTS
          value: "-Xms1g -Xmx2g -Dspring.profiles.active=prod"
        - name: CATALINA_OPTS
          value: "-Djava.security.egd=file:/dev/./urandom"
        resources:
          requests:
            memory: "1Gi"
            cpu: "500m"
          limits:
            memory: "2Gi"
            cpu: "1000m"
        livenessProbe:
          httpGet:
            path: /health
            port: 8080
          initialDelaySeconds: 60
          periodSeconds: 10
        readinessProbe:
          httpGet:
            path: /health
            port: 8080
          initialDelaySeconds: 30
          periodSeconds: 5
        volumeMounts:
        - name: logs
          mountPath: /usr/local/tomcat/logs
        - name: config
          mountPath: /usr/local/tomcat/conf/server.xml
          subPath: server.xml
      volumes:
      - name: logs
        emptyDir: {}
      - name: config
        configMap:
          name: tomcat-config
---
apiVersion: v1
kind: Service
metadata:
  name: tomcat-service
spec:
  selector:
    app: tomcat
  ports:
  - name: http
    port: 80
    targetPort: 8080
  - name: https
    port: 443
    targetPort: 8443
  type: LoadBalancer
                

ConfigMap for Configuration:

apiVersion: v1
kind: ConfigMap
metadata:
  name: tomcat-config
data:
  server.xml: |
    <?xml version="1.0" encoding="UTF-8"?>
    <Server port="8005" shutdown="SHUTDOWN">
        <Service name="Catalina">
            <Connector port="8080" protocol="HTTP/1.1"
                       maxThreads="500"
                       connectionTimeout="20000"
                       redirectPort="8443"/>
            <Engine name="Catalina" defaultHost="localhost">
                <Host name="localhost" appBase="webapps" 
                      unpackWARs="true" autoDeploy="true"/>
            </Engine>
        </Service>
    </Server>
  
  setenv.sh: |
    #!/bin/sh
    export JAVA_OPTS="$JAVA_OPTS -Xms1g -Xmx2g"
    export CATALINA_OPTS="$CATALINA_OPTS -Djava.security.egd=file:/dev/./urandom"
                

Monitoring & Management

JMX Configuration:

# In setenv.sh
export CATALINA_OPTS="$CATALINA_OPTS -Dcom.sun.management.jmxremote"
export CATALINA_OPTS="$CATALINA_OPTS -Dcom.sun.management.jmxremote.port=9090"
export CATALINA_OPTS="$CATALINA_OPTS -Dcom.sun.management.jmxremote.ssl=false"
export CATALINA_OPTS="$CATALINA_OPTS -Dcom.sun.management.jmxremote.authenticate=false"
export CATALINA_OPTS="$CATALINA_OPTS -Djava.rmi.server.hostname=localhost"

# For secure JMX
export CATALINA_OPTS="$CATALINA_OPTS -Dcom.sun.management.jmxremote.ssl=true"
export CATALINA_OPTS="$CATALINA_OPTS -Dcom.sun.management.jmxremote.ssl.need.client.auth=true"
export CATALINA_OPTS="$CATALINA_OPTS -Dcom.sun.management.jmxremote.access.file=../conf/jmxremote.access"
export CATALINA_OPTS="$CATALINA_OPTS -Dcom.sun.management.jmxremote.password.file=../conf/jmxremote.password"
                

Prometheus Metrics:

# Add Micrometer for Spring Boot applications
<dependency>
    <groupId>io.micrometer</groupId>
    <artifactId>micrometer-registry-prometheus</artifactId>
</dependency>

# Or use JMX Exporter
apiVersion: apps/v1
kind: Deployment
metadata:
  name: jmx-exporter
spec:
  template:
    spec:
      containers:
      - name: jmx-exporter
        image: bitnami/jmx-exporter:latest
        args:
        - -config.file=/etc/jmx-exporter/config.yaml
        - -jmx.url=service:jmx:rmi:///jndi/rmi://tomcat-service:9090/jmxrmi
        ports:
        - containerPort: 5556
        volumeMounts:
        - name: config
          mountPath: /etc/jmx-exporter
      volumes:
      - name: config
        configMap:
          name: jmx-exporter-config
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: jmx-exporter-config
data:
  config.yaml: |
    rules:
    - pattern: ".*"
                

Troubleshooting Common Issues

Memory Issues:

# Check memory usage
jstat -gc $(pgrep java) 1s

# Analyze heap dump
jmap -dump:live,format=b,file=heap.bin $(pgrep java)

# Check thread count
ps -eLf | grep java | wc -l

# Common JVM flags for memory issues
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/path/to/dumps
-XX:ErrorFile=/path/to/hs_err_pid%p.log
-XX:+PrintGCDetails
-XX:+PrintGCTimeStamps
-Xloggc:/path/to/gc.log
                

Performance Issues:

# Check thread dumps
kill -3 $(pgrep java)

# Monitor connection pool
netstat -an | grep 8080 | wc -l

# Check file descriptors
lsof -p $(pgrep java) | wc -l

# Database connection monitoring
# Add to context.xml
<Resource name="jdbc/myapp" 
          auth="Container"
          type="javax.sql.DataSource"
          maxTotal="100"
          maxIdle="30"
          maxWaitMillis="10000"
          username="dbuser"
          password="dbpass"
          driverClassName="com.mysql.cj.jdbc.Driver"
          url="jdbc:mysql://localhost:3306/myapp"
          validationQuery="SELECT 1"
          testOnBorrow="true"
          removeAbandonedOnBorrow="true"
          removeAbandonedTimeout="60"
          logAbandoned="true"/>
                

Useful Diagnostic Commands:

# Check Tomcat status
curl http://localhost:8080/manager/status?XML=true

# Check application health
curl http://localhost:8080/myapp/health

# Monitor access logs in real-time
tail -f logs/localhost_access_log.txt

# Check Catalina logs
tail -f logs/catalina.out

# Check for deadlocks
jstack $(pgrep java) | grep -A 15 "deadlock"

# Monitor GC activity
jstat -gcutil $(pgrep java) 1s
                

Best Practices Summary

  • Always use the latest stable Tomcat version with security patches
  • Configure proper JVM memory settings based on application requirements
  • Implement SSL/TLS for all production deployments
  • Use connection pooling and tune thread pools appropriately
  • Implement comprehensive logging and monitoring
  • Secure Tomcat by removing default applications and changing defaults
  • Use containerization for consistent deployments
  • Implement health checks and proper shutdown procedures

Additional Resources

© 2025 Tomcat Configuration Guide for DevOps. All rights reserved.

Comments

Popular posts from this blog

Real-world Terraform scenarios to test and improve your Infrastructure as Code skills

Azure Kubernetes Service (AKS) Complete Guide

Automate Your DevOps Documentation: `iac-to-docs` Lands on PyPI with AI Power