In October 2012, while developing ContactCampaigns.com on my local workstation, I hit a classic enterprise infrastructure hurdle: running an Apache Tomcat JVM application behind an Apache HTTP Server reverse proxy. In this comprehensive systems administration and architectural runbook, we examine why binding Tomcat directly to privileged port 80 is an anti-pattern, evaluate mod_proxy_http versus mod_proxy_ajp, configure production-hardened VirtualHosts, and implement connection pooling and SSL offloading.
The Developer's Reflection: The Problem with Port 80
My original 2012 development note captured a fundamental architectural realization:
"So I am working on this ContactCampaigns.com site in my spare time to keep my skills sharp. Looks like I need to setup Apache as a proxy, even for development purposes... The problem here is running Tomcat on port 80. I much rather master the art of configuring Apache correctly."
Running Apache Tomcat directly on privileged TCP port 80 (or 443) introduces severe architectural vulnerabilities:
- Privileged User Execution: In UNIX/Linux environments, binding to ports below 1024 historically required root privileges. Running a complex Java Virtual Machine (JVM) process as root violates the principle of least privilege—a single remote code execution bug in a servlet could compromise the entire host.
- Inefficient Static Asset Delivery: Tomcat is optimized for dynamic Java servlet execution, thread pool lifecycle management, and enterprise business logic. Forcing Tomcat to serve static CSS, JavaScript, fonts, and images wastes expensive JVM worker threads that should be reserved for transactional processing.
- Single-Application Rigidity: Direct port binding prevents hosting multiple independent applications or domains on the same server IP. A reverse proxy allows a single front-facing web server to route dozens of subdomains and tenant applications cleanly.
Protocol Evaluation: mod_proxy_http vs mod_proxy_ajp
When connecting Apache HTTP Server (httpd) to Tomcat, systems engineers historically chose between two proxy modules:
| Architecture Dimension | mod_proxy_http (Standard HTTP) | mod_proxy_ajp (Apache JServ Protocol) |
|---|---|---|
| Protocol Type | Standard HTTP/1.1 or HTTP/2 | Binary Packet Protocol (AJPv13) |
| CPU Overhead | Minimal parsing overhead on modern hardware | Slightly lower parsing overhead via binary headers |
| Security Profile | Mature, standard TLS encryption support | Vulnerable to CVE-2020-1938 ("Ghostcat") if unauthenticated |
| Observability | Easily inspected via tcpdump, Wireshark, or curl | Requires specialized AJP protocol decoders |
| Modern Industry Standard | Recommended (Universally Supported) | Legacy (Deprecated in many modern stacks) |
The Ghostcat Advisory: In February 2020, security researchers uncovered Ghostcat (CVE-2020-1938), a critical vulnerability in the AJP connector that allowed unauthenticated remote attackers to read arbitrary files inside web applications and achieve remote code execution. As a result, modern industry best practices mandate using mod_proxy_http over local loopback interfaces with TLS encryption.
Production Apache VirtualHost Configuration
Below is a battle-tested, production-hardened Apache VirtualHost configuration routing traffic to Tomcat with connection pooling, header forwarding, and SSL offloading:
<VirtualHost *:80>
ServerName contactcampaigns.com
ServerAlias www.contactcampaigns.com
# Redirect all plaintext HTTP traffic to secure HTTPS
RewriteEngine On
RewriteCond %{HTTPS} off
RewriteRule ^/(.*)$ https://%{HTTP_HOST}/$1 [R=301,L]
</VirtualHost>
<VirtualHost *:443>
ServerName contactcampaigns.com
ServerAlias www.contactcampaigns.com
SSLEngine on
SSLCertificateFile /etc/letsencrypt/live/contactcampaigns.com/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/contactcampaigns.com/privkey.pem
SSLProtocol all -SSLv3 -TLSv1 -TLSv1.1
SSLCipherSuite HIGH:!aNULL:!MD5:!3DES
# Preserve original client Host header for Tomcat virtual host mapping
ProxyPreserveHost On
ProxyRequests Off
# Forward client IP, protocol, and port to Java servlets
RequestHeader set X-Forwarded-Proto "https"
RequestHeader set X-Forwarded-Port "443"
# Static Asset Offloading: Serve directly from Apache cache, bypassing Tomcat
DocumentRoot "/var/www/contactcampaigns/static"
<Directory "/var/www/contactcampaigns/static">
Options -Indexes +FollowSymLinks
AllowOverride None
Require all granted
</Directory>
# Exclude static paths from proxy routing
ProxyPass /css/ !
ProxyPass /js/ !
ProxyPass /images/ !
ProxyPass /favicon.ico !
# Reverse Proxy to Tomcat on localhost:8080 with connection pooling
<Proxy "balancer://tomcatcluster">
BalancerMember "http://127.0.0.1:8080/emailcampaigner" route=node1 loadfactor=1 connectiontimeout=5 timeout=60 retry=10 acquire=3000 max=100 keepalive=On
</Proxy>
ProxyPass / balancer://tomcatcluster/
ProxyPassReverse / http://127.0.0.1:8080/emailcampaigner/
ProxyPassReverse / balancer://tomcatcluster/
ErrorLog "/var/log/apache2/contactcampaigns_error.log"
CustomLog "/var/log/apache2/contactcampaigns_access.log" combined
</VirtualHost>
Tomcat Connector Configuration: server.xml
For Tomcat to correctly interpret forwarded headers, recognize secure HTTPS schemes, and generate proper absolute URLs in redirects, the <Connector> in Tomcat's conf/server.xml must declare proxy awareness:
<!-- conf/server.xml Connector Configuration for Apache HTTP Proxy -->
<Connector port="8080"
protocol="HTTP/1.1"
connectionTimeout="20000"
redirectPort="8443"
address="127.0.0.1"
proxyName="contactcampaigns.com"
proxyPort="443"
scheme="https"
secure="true"
URIEncoding="UTF-8"
maxThreads="200"
minSpareThreads="25" />
<!-- Enable RemoteIpValve to populate request.getRemoteAddr() with real client IP -->
<Valve className="org.apache.catalina.valves.RemoteIpValve"
internalProxies="127.0.0.1|::1"
remoteIpHeader="x-forwarded-for"
proxiesHeader="x-forwarded-by"
protocolHeader="x-forwarded-proto" />
Crucial Configuration Details:
address="127.0.0.1": Restricts Tomcat's listener strictly to the local loopback interface. External requests from the public internet cannot bypass Apache to reach Tomcat directly.RemoteIpValve: Inspects incomingX-Forwarded-ForandX-Forwarded-Protoheaders, ensuring thatrequest.getRemoteAddr()reflects the real visitor's IP address rather than the proxy's IP.
Modern Comparison: Apache vs Nginx Ingress
While Apache HTTP Server pioneered reverse proxy architectures, modern infrastructure frequently pairs Tomcat or Node.js backends with Nginx at the edge:
- Event-Driven vs Process-Per-Connection: Apache's classic prefork or worker MPM models allocate worker threads per connection. Nginx's asynchronous, non-blocking event loop handles tens of thousands of idle keep-alive connections with minuscule memory consumption.
- Dynamic Configuration Reloading: Both Apache (
apache2ctl graceful) and Nginx (nginx -s reload) support zero-downtime configuration reloads without dropping active client connections. - Ingress-Level Rate Limiting: Deploying edge reverse proxies shields application servers like Tomcat from volumetric attacks using native token-bucket rate limiting (e.g.
limit_req_zone).
