Apache Tomcat Server Configuration
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
Comments
Post a Comment