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

Target Architecture

text
  Browser

     │  HTTPS :443  (or HTTP :80)

  Nginx  ── proxy_pass ──►  Tomcat 127.0.0.1:8080
     │                           │
  static files              hello.war (/hello)
  /var/www/...
ComponentListensRole
Nginx0.0.0.0:80, 0.0.0.0:443Public edge, TLS, static assets
Tomcat127.0.0.1:8080Servlet container, WAR execution

Step 1: Bind Tomcat to Localhost

In conf/server.xml:

xml
<Connector port="8080" protocol="HTTP/1.1"
           address="127.0.0.1"
           connectionTimeout="20000"
           redirectPort="8443" />

Restart Tomcat:

bash
sudo systemctl restart tomcat
ss -tlnp | grep 8080

Expected: 127.0.0.1:8080, not 0.0.0.0:8080.

Direct test (on the server):

bash
curl http://127.0.0.1:8080/hello/api

Step 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):

nginx
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:

bash
sudo ln -s /etc/nginx/sites-available/tomcat-hello /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx

Test:

bash
curl http://app.example.com/hello/api
curl -H "Host: app.example.com" http://127.0.0.1/hello/api

Code explanation:

  • location /hello/ and proxy_pass …/hello/ — both trailing slashes preserve path /hello/api
  • include proxy_params — sets Host, 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):

nginx
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 (deploy api.war)
  • / → static SPA; no hit to Tomcat for assets
  • Faster static delivery and simpler caching than Tomcat default servlet

Verify:

bash
curl -I http://shop.example.com/
curl http://shop.example.com/api/health

Compare to Nginx SPA + Spring Boot—same shape, Tomcat replaces embedded JAR.

Essential Proxy Headers

If proxy_params is missing, add manually:

nginx
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:

HeaderUse
HostCorrect links and vhost routing
X-Forwarded-ProtoRedirects to https:// when TLS ends at Nginx
X-Forwarded-ForClient IP in app logs (needs RemoteIpValve for full Tomcat integration)

Spring Boot WAR on Tomcat: enable forwarded headers in Boot:

properties
server.forward-headers-strategy=framework

Plain Servlets: read headers manually or add a Filter.

HTTPS with Let's Encrypt

After HTTP proxy works:

bash
sudo apt install certbot python3-certbot-nginx
sudo certbot --nginx -d app.example.com

Certbot 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 /:

nginx
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:

nginx
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:

nginx
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

  1. Tomcat unit — app on 127.0.0.1:8080
  2. Nginx — public ports

Check both:

bash
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/api

Both should return 200 (or your expected code).

Troubleshooting 502 Bad Gateway

CauseCheckFix
Tomcat downsystemctl status tomcatStart Tomcat
Wrong bind addressss -tlnp | grep 8080address="127.0.0.1" or match Nginx upstream
Wrong context pathcurl http://127.0.0.1:8080/hello/apiFix WAR name or location
Nginx typonginx -tFix config
Firewall loopbackrareEnsure local proxy allowed

Nginx error.log:

bash
sudo tail -20 /var/log/nginx/error.log

Tomcat logs:

bash
tail -30 $CATALINA_HOME/logs/catalina.$(date +%Y-%m-%d).log

Logging: Edge vs Container

LogShows
/var/log/nginx/access.logPublic client IP, URL, TLS
logs/localhost_access_log.*Often 127.0.0.1, servlet path

See Logging and Access Log.

End-to-End Checklist

  • Tomcat on 127.0.0.1:8080 only
  • WAR responds on loopback with correct context path
  • Nginx proxy_pass + proxy_params
  • nginx -t passes; public URL works
  • HTTPS via Certbot (production)
  • Firewall blocks public 8080

What Comes Next

ChapterTopic
13 — JVM and Connector tuningThreads and heap under load
18 — Project: Nginx + TomcatHands-on lab
20 — TroubleshootingExtended 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.