Nginx Reverse Proxy for Tomcat
Introduction
Production Tomcat should usually not face the public internet on port 8080. Nginx listens on 80/443, terminates TLS, serves static files efficiently, and proxies HTTP to Tomcat on 127.0.0.1:8080. This chapter wires that stack for a WAR with context path /hello, covers path and header rules Tomcat cares about, and shows verification steps—extending Nginx reverse proxy basics from the Tomcat + WAR angle.
Prerequisites
- Tomcat Security Basics — Tomcat bound to localhost
- Deploying WAR and webapps —
hello.warat/hello - Nginx installed — Installing Nginx
- Proxy headers and path rules — recommended reading
Target Architecture
Browser
│
│ HTTPS :443 (or HTTP :80)
▼
Nginx ── proxy_pass ──► Tomcat 127.0.0.1:8080
│ │
static files hello.war (/hello)
/var/www/...| Component | Listens | Role |
|---|---|---|
| Nginx | 0.0.0.0:80, 0.0.0.0:443 | Public edge, TLS, static assets |
| Tomcat | 127.0.0.1:8080 | Servlet container, WAR execution |
Step 1: Bind Tomcat to Localhost
In conf/server.xml:
<Connector port="8080" protocol="HTTP/1.1"
address="127.0.0.1"
connectionTimeout="20000"
redirectPort="8443" />Restart Tomcat:
sudo systemctl restart tomcat
ss -tlnp | grep 8080Expected: 127.0.0.1:8080, not 0.0.0.0:8080.
Direct test (on the server):
curl http://127.0.0.1:8080/hello/apiStep 2: Basic Nginx Proxy (Preserve Context Path)
When your WAR is hello.war → context /hello, public URL should include /hello unless you deploy as ROOT.
/etc/nginx/sites-available/tomcat-hello (Ubuntu/Debian style):
server {
listen 80;
server_name app.example.com;
location /hello/ {
proxy_pass http://127.0.0.1:8080/hello/;
include /etc/nginx/proxy_params;
}
}Enable and reload:
sudo ln -s /etc/nginx/sites-available/tomcat-hello /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginxTest:
curl http://app.example.com/hello/api
curl -H "Host: app.example.com" http://127.0.0.1/hello/apiCode explanation:
location /hello/andproxy_pass …/hello/— both trailing slashes preserve path/hello/apiinclude proxy_params— setsHost,X-Forwarded-*headers
Warning
Trailing Slash Mismatch
location /hello/ with proxy_pass http://127.0.0.1:8080 (no path) also forwards /hello/api intact. Mixing slash rules incorrectly strips or duplicates /hello—see path rules.
Step 3: Proxy Entire Site (ROOT WAR)
If frontend.war is deployed as ROOT (URL / on Tomcat):
server {
listen 80;
server_name app.example.com;
location / {
proxy_pass http://127.0.0.1:8080;
include /etc/nginx/proxy_params;
}
}Public: http://app.example.com/api → Tomcat /api.
Split Static Frontend + Tomcat API
Common pattern: Nginx serves React/Vue dist/, Tomcat serves /api/ WAR or context.
Code explanation:
/api/→ Tomcat context/api(deployapi.war)/→ static SPA; no hit to Tomcat for assets- Faster static delivery and simpler caching than Tomcat default servlet
Verify:
curl -I http://shop.example.com/
curl http://shop.example.com/api/healthCompare to Nginx SPA + Spring Boot—same shape, Tomcat replaces embedded JAR.
Essential Proxy Headers
If proxy_params is missing, add manually:
proxy_http_version 1.1;
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;Why Tomcat / Servlet apps care:
| Header | Use |
|---|---|
Host | Correct links and vhost routing |
X-Forwarded-Proto | Redirects to https:// when TLS ends at Nginx |
X-Forwarded-For | Client IP in app logs (needs RemoteIpValve for full Tomcat integration) |
Spring Boot WAR on Tomcat: enable forwarded headers in Boot:
server.forward-headers-strategy=frameworkPlain Servlets: read headers manually or add a Filter.
HTTPS with Let's Encrypt
After HTTP proxy works:
sudo apt install certbot python3-certbot-nginx
sudo certbot --nginx -d app.example.comCertbot adds listen 443 ssl and redirect from 80. Tomcat stays on HTTP localhost—no Java keystore required.
Details: Nginx HTTPS and Let's Encrypt.
Strip Prefix: Public /api → Tomcat /
When Nginx exposes /api/ but the WAR is ROOT with controllers at /:
location /api/ {
proxy_pass http://127.0.0.1:8080/;
include /etc/nginx/proxy_params;
}Request GET /api/users → upstream GET /users.
Only use when your app truly expects /users, not /api/users.
Timeouts and Upload Size
Large uploads or slow reports:
location /hello/ {
proxy_pass http://127.0.0.1:8080/hello/;
include /etc/nginx/proxy_params;
proxy_connect_timeout 60s;
proxy_send_timeout 120s;
proxy_read_timeout 120s;
client_max_body_size 20m;
}Align client_max_body_size with Tomcat maxPostSize / app multipart limits—see Nginx file uploads.
WebSocket Through Nginx (Preview)
If your Tomcat app uses WebSocket:
location /hello/ws/ {
proxy_pass http://127.0.0.1:8080/hello/ws/;
include /etc/nginx/proxy_params;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}Full treatment: Nginx WebSocket proxy.
Why HTTP proxy, Not AJP
Historically AJP (port 8009) connected Apache httpd to Tomcat. Today HTTP proxy_pass is simpler, works with any upstream, and matches Spring Boot / Node patterns.
Leave AJP commented out in server.xml unless you maintain legacy config.
systemd: Start Order
- Tomcat unit — app on
127.0.0.1:8080 - Nginx — public ports
Check both:
sudo systemctl status tomcat nginx
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8080/hello/api
curl -s -o /dev/null -w '%{http_code}\n' -H "Host: app.example.com" http://127.0.0.1/hello/apiBoth should return 200 (or your expected code).
Troubleshooting 502 Bad Gateway
| Cause | Check | Fix |
|---|---|---|
| Tomcat down | systemctl status tomcat | Start Tomcat |
| Wrong bind address | ss -tlnp | grep 8080 | address="127.0.0.1" or match Nginx upstream |
| Wrong context path | curl http://127.0.0.1:8080/hello/api | Fix WAR name or location |
| Nginx typo | nginx -t | Fix config |
| Firewall loopback | rare | Ensure local proxy allowed |
Nginx error.log:
sudo tail -20 /var/log/nginx/error.logTomcat logs:
tail -30 $CATALINA_HOME/logs/catalina.$(date +%Y-%m-%d).logLogging: Edge vs Container
| Log | Shows |
|---|---|
/var/log/nginx/access.log | Public client IP, URL, TLS |
logs/localhost_access_log.* | Often 127.0.0.1, servlet path |
End-to-End Checklist
- Tomcat on
127.0.0.1:8080only - WAR responds on loopback with correct context path
- Nginx
proxy_pass+proxy_params -
nginx -tpasses; public URL works - HTTPS via Certbot (production)
- Firewall blocks public 8080
What Comes Next
| Chapter | Topic |
|---|---|
| 13 — JVM and Connector tuning | Threads and heap under load |
| 18 — Project: Nginx + Tomcat | Hands-on lab |
| 20 — Troubleshooting | Extended 502/504 guide |
FAQ
Same config as Spring Boot JAR on 8080?
Almost identical proxy_pass—difference is Tomcat runs multiple WARs with different context paths on one JVM.
Hide /hello from public URL?
Deploy as ROOT, or use location / with rewrite rules—design URL scheme before deploy.
404 from Nginx but Tomcat direct works?
server_name mismatch, wrong location, or missing Host header in test—use curl -H "Host: …".
Need client IP in Tomcat access log?
Configure RemoteIpValve and trust Nginx IP—or rely on Nginx access log for client IP.
Two domains, one Tomcat?
Two server blocks in Nginx, same proxy_pass, or different paths; Tomcat Host aliases advanced—single domain is enough for learning.
CORS?
Handle in app (Spring @CrossOrigin) or Nginx add_header—prefer app for API semantics.