Apache Tomcat has been running Java web applications for decades, and it still shows up everywhere from legacy enterprise systems to modern Spring Boot deployments that opt to run as WAR files inside Tomcat rather than an embedded server. What Tomcat doesn’t do particularly well on its own is handle modern TLS termination, static asset caching, and clean production URLs. That’s where Nginx comes in.
This guide covers setting up Nginx as a reverse proxy in front of Tomcat, including proper handling of Tomcat’s context paths, session behavior, and the specific headers Tomcat needs to generate correct redirect URLs.
Why Reverse Proxy Tomcat with Nginx
Tomcat can serve HTTPS directly through its own connector configuration, but doing certificate management, HTTP/2, and static file caching inside server.xml is clunky compared to Nginx, which was built for exactly that. A typical production setup runs Tomcat bound to localhost:8080 (HTTP, no TLS) and lets Nginx handle everything client-facing: TLS termination, compression, static file serving, and request routing to potentially multiple Tomcat instances or applications.
Requirements
- Tomcat installed and running, reachable internally on its default port (
8080for HTTP). - Nginx installed on the same host or a separate front-end server.
- A domain name pointing at the Nginx server.
- A TLS certificate (Let’s Encrypt/Certbot is the standard free option).
- Access to Tomcat’s
conf/server.xmlif adjusting the connector for proxy awareness.
Install Nginx:
sudo apt update && sudo apt install nginx -y
Confirm Tomcat is listening:
sudo ss -tlnp | grep 8080
Step 1: Configure Tomcat’s Connector for Proxy Awareness
Open conf/server.xml in the Tomcat installation directory (commonly /opt/tomcat/conf/server.xml or /etc/tomcat9/server.xml on Debian-based packages).
Locate the HTTP connector block and add proxy-related attributes:
<Connector port="8080" protocol="HTTP/1.1"
connectionTimeout="20000"
redirectPort="8443"
proxyName="app.example.com"
proxyPort="443"
scheme="https"
secure="true" />
proxyNameandproxyPorttell Tomcat what hostname and port to use when generating absolute URLs (redirects, form actions), rather than exposing its internal bind address.scheme="https"andsecure="true"make Tomcat treat the connection as secure for the purposes of things likerequest.isSecure()in application code, even though Nginx is the one actually terminating TLS.
Restart Tomcat after saving:
sudo systemctl restart tomcat
Step 2: Write the Nginx Configuration
Create the config file:
sudo nano /etc/nginx/sites-available/tomcat.conf
Complete example:
server {
listen 80;
server_name app.example.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
server_name app.example.com;
ssl_certificate /etc/letsencrypt/live/app.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/app.example.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
client_max_body_size 25M;
# Serve static assets directly from disk if they live outside the WAR,
# otherwise remove this block and let Tomcat serve everything.
location /static/ {
alias /var/www/app/static/;
expires 30d;
add_header Cache-Control "public";
}
location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Port $server_port;
proxy_read_timeout 120s;
proxy_send_timeout 120s;
proxy_connect_timeout 30s;
proxy_buffering on;
proxy_buffer_size 8k;
proxy_buffers 8 8k;
}
}
If the application is deployed under a specific context path instead of the ROOT context (for example /myapp instead of /), point the location block at that path explicitly:
location /myapp/ {
proxy_pass http://127.0.0.1:8080/myapp/;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
}
Enable and test:
sudo ln -s /etc/nginx/sites-available/tomcat.conf /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
Step 3: Testing the Setup
Basic connectivity check:
curl -Ik https://app.example.com/
In the browser:
- Load the application’s login or landing page, and confirm session cookies are set with the
Secureflag (visible in dev tools under Application/Cookies). - Submit a login form and check that any post-login redirect stays on the correct external domain rather than falling back to
localhost:8080or an internal IP — this confirmsproxyName/proxyPortare working correctly. - If the app uses WebSockets (common with Spring’s STOMP/SockJS setups), verify live features work; if not already added, WebSocket proxying needs its own headers (see below).
For applications using WebSockets, extend the config:
location /ws/ {
proxy_pass http://127.0.0.1:8080/ws/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
}
Troubleshooting
Redirects go to http://localhost:8080/... instead of the real domain This means Tomcat’s connector isn’t proxy-aware. Confirm proxyName, proxyPort, and scheme="https" were added correctly to the connector in server.xml, and that Tomcat was actually restarted after the change.
502 Bad Gateway Confirm Tomcat is actually running and bound to the expected port:
sudo systemctl status tomcat
sudo ss -tlnp | grep 8080
Check Tomcat’s own logs for startup errors:
tail -f /opt/tomcat/logs/catalina.out
404 errors for an application that works fine when accessed directly on port 8080 Usually a context path mismatch between the location block in Nginx and the actual deployed context. Confirm the WAR’s deployed path with:
ls /opt/tomcat/webapps/
Session gets lost after login (repeated login prompts) Check that cookies aren’t being duplicated or dropped due to a Host header mismatch. Also verify Tomcat’s useHttpOnly and cookie settings in context.xml aren’t conflicting with how Nginx passes headers.
Large file uploads fail with 413 Request Entity Too Large Increase client_max_body_size in the Nginx server block, and check Tomcat’s own maxPostSize and maxSwallowSize connector attributes, which can independently cap upload size even if Nginx allows it through.
Security Considerations
- Bind Tomcat’s connector to
127.0.0.1once the proxy is verified working, using theaddressattribute on the connector, so it’s unreachable except through Nginx. - Disable the Tomcat Manager and Host Manager apps in production unless actively needed, and if they are needed, restrict access by IP at the Nginx layer rather than relying solely on Tomcat’s own authentication.
- Set standard security headers:
add_header X-Frame-Options SAMEORIGIN;
add_header X-Content-Type-Options nosniff;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
- Hide the Tomcat version banner by editing
catalina.jar‘sServerInfo.propertiesor using theServerheader rewrite in Nginx:
proxy_hide_header Server;
add_header Server "web";
- Keep Tomcat patched. A reverse proxy doesn’t protect against vulnerabilities in the application server itself; it only controls the network path to it.
Performance Tips
- Serve static assets directly through Nginx rather than routing them through Tomcat’s servlet container whenever the app’s build process outputs them to a known directory — this is dramatically faster and reduces load on JVM threads.
- Enable gzip compression for text-based responses:
gzip on;
gzip_types text/plain text/css application/javascript application/json;
gzip_min_length 1024;
- Tune Tomcat’s connector thread pool (
maxThreads,acceptCount) to match expected concurrency; Nginx can queue and buffer requests, but Tomcat still needs enough worker threads to process them. - Use keepalive connections between Nginx and Tomcat to avoid the overhead of establishing a new TCP connection per request:
upstream tomcat_backend {
server 127.0.0.1:8080;
keepalive 32;
}
Then reference proxy_pass http://tomcat_backend; and add proxy_set_header Connection ""; to enable persistent connections.
Real-World Use Cases
- Legacy enterprise Java applications that can’t easily be rearchitected but still need modern TLS and a professional-looking domain.
- Multiple WAR deployments on one Tomcat instance, each proxied under a different subdomain or path prefix by Nginx.
- Blue-green deployments, where Nginx switches traffic between two Tomcat instances running different application versions with a simple config reload.
- API gateways for Spring Boot WAR deployments, where Nginx handles rate limiting and routing across several backend Tomcat services.
Best Practices Checklist
- Set
proxyName,proxyPort, andscheme="https"on the Tomcat connector whenever it’s placed behind a reverse proxy. - Pass
X-Forwarded-For,X-Forwarded-Proto, andHostheaders consistently. - Bind Tomcat to
127.0.0.1once the proxy path is confirmed working. - Serve static assets directly from Nginx where possible instead of through the servlet container.
- Match context paths exactly between the deployed WAR and the Nginx
locationblock. - Use keepalive connections to the upstream to reduce connection overhead under load.
- Disable or restrict the Tomcat Manager application in production.
Tomcat behind Nginx is one of the more mature, well-trodden reverse proxy setups out there, but the two details that trip people up are almost always the same: forgetting to make the connector proxy-aware (proxyName/proxyPort/scheme), and mismatched context paths between the WAR deployment and the Nginx config. Get those two things right, and everything else is standard reverse proxy configuration.
