How to Use Apache as a Reverse Proxy for Tomcat

How to use Apache as a reverse proxy for Tomcat

Java web applications running on Tomcat are powerful, but Tomcat alone isn’t really built to be the public-facing edge of a production website. It doesn’t handle SSL termination gracefully, it’s clumsy at serving static assets efficiently, and running it directly on port 80 or 443 means running it as root — never a good idea. The standard fix is to put Apache in front of it as a reverse proxy, letting Apache handle the public traffic while Tomcat quietly does what it’s good at: running the Java application logic.

What Does “Reverse Proxy” Actually Mean Here?

A reverse proxy sits between the client and the backend server, forwarding requests to it and returning the backend’s response to the client as if the proxy had generated it itself. In this setup:

  1. A browser requests https://yourdomain.com/app
  2. Apache receives the request on port 443
  3. Apache forwards it internally to Tomcat on localhost:8080
  4. Tomcat processes the request and returns a response
  5. Apache passes that response back to the browser

The client never talks to Tomcat directly — it only ever sees Apache.

Why Put Apache in Front of Tomcat?

  • SSL termination — Apache’s mod_ssl handles HTTPS cleanly; Tomcat’s SSL configuration is more cumbersome by comparison.
  • Static content offloading — Apache serves images, CSS, and JS far more efficiently than Tomcat’s Java-based servlet engine.
  • Security isolation — Tomcat never needs to be exposed to the public internet directly; only Apache is.
  • Load balancing — Apache can distribute requests across multiple Tomcat instances.
  • URL flexibility — you can host multiple applications (some Java, some not) under one domain, routing by path.
  • Unified logging and access control — centralize logs, rate limiting, and auth at the Apache layer.

Prerequisites

  • A Linux server (Ubuntu 22.04/24.04 used in examples)
  • Root/sudo access
  • Java installed (OpenJDK 17 or 21 recommended for current Tomcat versions)
  • Basic familiarity with XML config files (Tomcat’s server.xml) and Apache’s config syntax

Step 1: Install Java and Tomcat

sudo apt update
sudo apt install openjdk-21-jdk -y
java -version

Download and install Tomcat (adjust the version number as needed):

cd /opt
sudo wget https://dlcdn.apache.org/tomcat/tomcat-10/v10.1.30/bin/apache-tomcat-10.1.30.tar.gz
sudo tar -xzvf apache-tomcat-10.1.30.tar.gz
sudo mv apache-tomcat-10.1.30 tomcat10

Create a dedicated non-root user for Tomcat — never run application servers as root:

sudo useradd -m -U -d /opt/tomcat10 -s /bin/false tomcat
sudo chown -R tomcat:tomcat /opt/tomcat10
sudo chmod +x /opt/tomcat10/bin/*.sh

Step 2: Create a systemd Service for Tomcat

sudo nano /etc/systemd/system/tomcat.service
[Unit]
Description=Apache Tomcat
After=network.target

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

Environment="JAVA_HOME=/usr/lib/jvm/java-21-openjdk-amd64"
Environment="CATALINA_PID=/opt/tomcat10/temp/tomcat.pid"
Environment="CATALINA_HOME=/opt/tomcat10"
Environment="CATALINA_BASE=/opt/tomcat10"

ExecStart=/opt/tomcat10/bin/startup.sh
ExecStop=/opt/tomcat10/bin/shutdown.sh

RestartSec=10
Restart=on-failure

[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable tomcat
sudo systemctl start tomcat
sudo systemctl status tomcat

Confirm Tomcat is listening locally:

curl http://localhost:8080

You should see the default Tomcat welcome page HTML returned.

Step 3: Install Apache and Required Proxy Modules

sudo apt install apache2 -y
sudo a2enmod proxy
sudo a2enmod proxy_http
sudo a2enmod proxy_ajp
sudo a2enmod ssl
sudo a2enmod headers
sudo systemctl restart apache2

mod_proxy_http handles standard HTTP proxying; mod_proxy_ajp is an alternative if you prefer the AJP protocol (faster in some setups, but HTTP proxying is simpler and sufficient for most cases).

Step 4: Configure the Virtual Host as a Reverse Proxy

sudo nano /etc/apache2/sites-available/tomcat-proxy.conf

    ServerName yourdomain.com

    ProxyPreserveHost On
    ProxyRequests Off

    ProxyPass "/" "http://localhost:8080/"
    ProxyPassReverse "/" "http://localhost:8080/"

    ErrorLog ${APACHE_LOG_DIR}/tomcat-proxy-error.log
    CustomLog ${APACHE_LOG_DIR}/tomcat-proxy-access.log combined

If you only want to proxy a specific application path rather than the whole domain:

ProxyPass "/myapp" "http://localhost:8080/myapp"
ProxyPassReverse "/myapp" "http://localhost:8080/myapp"

Enable the site:

sudo a2ensite tomcat-proxy.conf
sudo a2dissite 000-default.conf
sudo systemctl reload apache2

Visit http://yourdomain.com — you should now see Tomcat’s response, served transparently through Apache.

Step 5: Add HTTPS Termination

sudo apt install certbot python3-certbot-apache -y
sudo certbot --apache -d yourdomain.com

Certbot adds a block automatically. Ensure the proxy directives are duplicated there too, and add headers so Tomcat knows the original request was HTTPS:


    ServerName yourdomain.com

    SSLEngine on
    SSLCertificateFile /etc/letsencrypt/live/yourdomain.com/fullchain.pem
    SSLCertificateKeyFile /etc/letsencrypt/live/yourdomain.com/privkey.pem

    ProxyPreserveHost On
    ProxyPass "/" "http://localhost:8080/"
    ProxyPassReverse "/" "http://localhost:8080/"

    RequestHeader set X-Forwarded-Proto "https"

Without X-Forwarded-Proto, Tomcat applications that generate absolute URLs may incorrectly generate http:// links even when served over HTTPS.

Step 6: Configure Tomcat to Trust the Proxy (Optional but Recommended)

In conf/server.xml, add a RemoteIpValve so Tomcat correctly logs the real client IP instead of 127.0.0.1:


Restart Tomcat after the change:

sudo systemctl restart tomcat

Real-World Use Cases

  • Legacy Java enterprise apps — many internal business applications still run on Tomcat/Java EE stacks and need a modern HTTPS-capable front door.
  • Microservices gateways — Apache can route different URL paths to different Tomcat instances or even different backend technologies entirely.
  • Migrating monoliths — teams gradually moving off a Tomcat monolith often keep Apache as the stable routing layer while backend services change underneath.
  • Multi-tenant Java hosting — hosting providers running many customer WAR files on shared Tomcat instances, isolated behind per-domain Apache vhosts.

Troubleshooting Common Issues

502 Bad Gateway Tomcat isn’t running, or is listening on a different port than configured.

sudo systemctl status tomcat
curl http://localhost:8080

Proxy works but static assets (CSS/JS) are broken Usually caused by relative paths inside the Java app assuming it’s the site root when actually mounted at a subpath. Fix with a ProxyPassReverseCookiePath and confirm the app’s context path matches your proxy path.

Redirect loops after adding HTTPS Almost always missing X-Forwarded-Proto — the backend app redirects to http:// because it doesn’t know the original request was HTTPS.

Session stickiness issues with multiple Tomcat instances If load-balancing across more than one Tomcat node, enable mod_proxy_balancer with session affinity via the route attribute in Tomcat’s jvmRoute, or move sessions to a shared store (Redis, database).

Security Best Practices

  • Bind Tomcat to localhost only (or a private network interface) — never expose port 8080 directly to the internet.
  • Remove Tomcat’s default sample apps (/examples, /docs, /manager if unused) — they’re common attack surface.
  • Restrict access to the Tomcat Manager app to specific IPs if it must remain enabled.
  • Keep both Apache and Tomcat patched — reverse proxy setups are only as secure as the weakest link.
  • Use mod_security on the Apache layer for an additional WAF pass before requests ever reach Java code.
  • Set appropriate Header directives to strip or rewrite any internal server info leaking in responses.

Performance Optimization

  • Enable connection reuse with mod_proxy‘s keepalive settings: ProxySet keepalive=On
  • Serve static content directly from Apache instead of proxying it to Tomcat, using Alias for asset directories.
  • Tune Tomcat’s thread pool in server.xml to match expected concurrency:
  • Enable Apache’s mod_deflate for gzip compression at the edge, reducing bandwidth for JSON/HTML responses from Tomcat.
  • Use a connection pool (like Tomcat’s built-in DBCP) rather than opening new database connections per request.
  • Monitor with JMX or a tool like VisualVM to catch memory or thread pool exhaustion before it causes proxy timeouts.

Frequently Asked Questions

Should I use mod_proxy_http or mod_proxy_ajp? HTTP proxying is simpler to configure and debug, and performance differences are minimal for most applications. AJP made more sense historically when HTTP/1.0 overhead was higher; today HTTP proxying is the more common recommendation.

Can Apache load-balance across multiple Tomcat servers? Yes, using mod_proxy_balancer with a block listing multiple Tomcat worker nodes, for example:


    BalancerMember "http://192.168.1.10:8080" route=node1
    BalancerMember "http://192.168.1.11:8080" route=node2
    ProxySet lbmethod=byrequests


ProxyPass "/" "balancer://tomcatcluster/"
ProxyPassReverse "/" "balancer://tomcatcluster/"

Each node’s route value should match the jvmRoute attribute set in that Tomcat instance’s server.xml, which lets Apache maintain session affinity so a given user keeps hitting the same backend node for the life of their session.

What happens if a Tomcat node goes down while load balancing? mod_proxy_balancer marks a failed node as unavailable after a configurable number of failed attempts and routes traffic to the remaining healthy nodes. You can tune this with retry and failonstatus parameters on each BalancerMember, and monitor cluster health live via the built-in balancer-manager handler:


    SetHandler balancer-manager
    Require ip 127.0.0.1

This gives a simple web dashboard showing each node’s status, load, and session count — genuinely useful when debugging uneven traffic distribution.

Do I need to change Tomcat’s default port? Not necessarily — since Tomcat is only reachable via localhost, the default 8080 is fine as long as it’s not exposed externally.

Is this setup suitable for high-traffic production apps? Yes — this is a standard, widely used pattern in enterprise Java deployments, especially when paired with load balancing and proper JVM tuning.

Summary and Key Takeaways

Putting Apache in front of Tomcat gives you the best of both worlds: Apache’s mature HTTPS handling, static file serving, and access control, combined with Tomcat’s solid Java servlet execution. The setup boils down to installing both, enabling mod_proxy and mod_proxy_http, writing a ProxyPass/ProxyPassReverse pair, and layering HTTPS with proper forwarded headers on top.

Once running, the ongoing focus shifts to security isolation (never expose Tomcat directly), performance tuning (thread pools, keepalive, compression), and keeping both layers patched. This is a proven, boring-in-a-good-way architecture that scales from a single small app to enterprise multi-instance deployments.

References

Total
3
Shares

Leave a Reply

Previous Post
How to serve static files efficiently with Apache

How to Serve Static Files Efficiently with Apache

Next Post
How to set up Apache for serving WordPress websites

How to Set Up Apache for Serving WordPress Websites

Related Posts