How to Set Up Nginx for a Java Application

How to Set Up Nginx for a Java Application

I’ve lost count of how many times I’ve been handed a Java application — a Spring Boot service, a Tomcat WAR file, or a plain old embedded Jetty app — and told “just put it behind Nginx.” It sounds simple, and honestly, once you’ve done it a few times, it is. But the first time I set this up, I burned an entire afternoon chasing a 502 Bad Gateway error that turned out to be a one-character typo in my proxy_pass directive. So I’m writing the guide I wish I’d had back then.

In this article, I’ll walk you through everything you need to put Nginx in front of a Java application: why you’d want to do it, how to configure it properly, how to test it, and how to avoid the mistakes that cost me hours.

Why Put Nginx in Front of a Java Application?

Java web servers — Tomcat, Jetty, WildFly, or Spring Boot’s embedded server — are perfectly capable of serving HTTP traffic on their own. So why bother with Nginx at all?

I use Nginx as a reverse proxy for a few reasons that have proven themselves over and over in production:

  • TLS termination. I’d much rather manage SSL certificates in one place (Nginx) than configure a Java keystore for every application I deploy.
  • Load balancing. When I run multiple instances of a Java app for redundancy, Nginx distributes traffic between them.
  • Static file serving. Nginx is dramatically faster at serving static assets (images, CSS, JS) than a Java servlet container.
  • Buffering and protection. Nginx can buffer slow client connections, protecting your Java app’s thread pool from being tied up by slow clients.
  • A unified entry point. If I’m running several services on one box, Nginx lets me route by path or hostname to different backend applications.

Requirements

Before we get into configuration, here’s what you’ll need:

  • A Linux server (I’ll use Ubuntu 22.04/24.04 conventions here, but the Nginx config itself is distro-agnostic)
  • Nginx installed (sudo apt install nginx on Debian/Ubuntu, or sudo dnf install nginx on RHEL/Fedora)
  • A running Java application listening on a local port — I’ll assume port 8080, which is the default for both Tomcat and Spring Boot
  • Root or sudo access
  • Basic familiarity with the command line

If your Java app isn’t running yet, start it first and confirm it’s reachable locally:

curl http://127.0.0.1:8080

If that doesn’t return anything, fix that before touching Nginx — a common mistake I see beginners make is debugging Nginx when the actual problem is that the Java app crashed or never started.

Understanding the Architecture

Here’s the mental model I use: Nginx sits on port 80 (or 443 for HTTPS) and listens for public traffic. When a request comes in, Nginx forwards it internally to your Java application running on its own port (commonly 8080). The Java app never needs to be exposed to the internet directly — it can (and should) bind only to 127.0.0.1.

Client → Nginx (port 80/443) → Java App (port 8080, localhost only)

This separation is the whole point. Nginx becomes the public face, and your Java process stays tucked away behind it.

Step 1: Install Nginx

On Debian-based systems:

sudo apt update
sudo apt install nginx -y

On RHEL-based systems:

sudo dnf install nginx -y
sudo systemctl enable --now nginx

Confirm Nginx is running:

sudo systemctl status nginx

You should see active (running). If you visit your server’s IP address in a browser now, you’ll see the default Nginx welcome page — that’s your confirmation the base install worked.

Step 2: Create a Server Block for Your Java App

Rather than editing the default config, I always create a dedicated server block. On Debian/Ubuntu, that means creating a file in /etc/nginx/sites-available/ and symlinking it into sites-enabled/. On RHEL-based systems, you’ll typically drop files directly into /etc/nginx/conf.d/.

Let’s create the config:

sudo nano /etc/nginx/sites-available/myjavaapp

Here’s a complete, working example configuration:

server {
    listen 80;
    server_name myjavaapp.example.com;

    access_log /var/log/nginx/myjavaapp.access.log;
    error_log /var/log/nginx/myjavaapp.error.log;

    location / {
        proxy_pass http://127.0.0.1:8080;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        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_cache_bypass $http_upgrade;
        proxy_read_timeout 90s;
    }
}

Let me explain what’s happening line by line, because I think understanding each directive is what separates “copy-pasted config” from “I actually know what I’m doing”:

  • proxy_pass http://127.0.0.1:8080; — this is the core directive. It tells Nginx where to forward requests.
  • proxy_http_version 1.1; combined with the Upgrade/Connection headers — these enable WebSocket support, which many Java frameworks (especially those using STOMP or Spring’s WebSocket support) rely on.
  • proxy_set_header Host $host; — without this, your Java app sees every request as coming from 127.0.0.1 instead of the real hostname, which breaks any logic based on Host.
  • X-Real-IP and X-Forwarded-For — these preserve the client’s real IP address so your application logs (and any rate-limiting logic) don’t just show Nginx’s IP for every request.
  • X-Forwarded-Proto — tells your app whether the original request was HTTP or HTTPS, which matters for frameworks that build absolute URLs or enforce secure cookies.
  • proxy_read_timeout 90s; — Java apps, especially ones doing database queries or calling external APIs, can be slower to respond than static content. I bump this up from Nginx’s default 60s to avoid premature timeouts.

Step 3: Enable the Site and Test the Configuration

On Debian/Ubuntu, symlink the config into sites-enabled:

sudo ln -s /etc/nginx/sites-available/myjavaapp /etc/nginx/sites-enabled/

Now test the configuration syntax before reloading — this is a step I never skip, because a bad config can take Nginx down entirely:

sudo nginx -t

You should see:

nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

If it passes, reload Nginx:

sudo systemctl reload nginx

Step 4: Configuring for Spring Boot Specifically

If you’re running Spring Boot with an embedded Tomcat or Netty server, there’s one extra thing I always set in application.properties or application.yml — telling Spring Boot to trust the forwarded headers from Nginx:

server.forward-headers-strategy=native

Without this, Spring’s HttpServletRequest.getScheme() and related methods won’t reflect the real protocol, which can cause issues with redirect URLs and secure cookie handling behind a reverse proxy.

Step 5: Configuring for Traditional Tomcat/WAR Deployments

If you’re deploying a WAR file into a standalone Tomcat instance, you’ll want to configure Tomcat’s Connector in server.xml to trust the proxy:

<Connector port="8080" protocol="HTTP/1.1"
           connectionTimeout="20000"
           redirectPort="8443"
           proxyName="myjavaapp.example.com"
           proxyPort="443"
           scheme="https"
           secure="true" />

This tells Tomcat that even though it’s receiving plain HTTP internally, it should behave as though requests arrived over HTTPS on the standard port — which keeps generated URLs and redirects correct.

Adding HTTPS with Let’s Encrypt

I don’t consider a Java app “production ready” behind Nginx until it has TLS. The easiest way I’ve found is Certbot:

sudo apt install certbot python3-certbot-nginx -y
sudo certbot --nginx -d myjavaapp.example.com

Certbot will automatically edit your server block to add the SSL certificate paths and redirect HTTP to HTTPS. After running it, your config will look something like this:

server {
    listen 443 ssl;
    server_name myjavaapp.example.com;

    ssl_certificate /etc/letsencrypt/live/myjavaapp.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/myjavaapp.example.com/privkey.pem;
    include /etc/letsencrypt/options-ssl-nginx.conf;
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;

    location / {
        proxy_pass http://127.0.0.1:8080;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        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;
    }
}

server {
    listen 80;
    server_name myjavaapp.example.com;
    return 301 https://$host$request_uri;
}

Certbot also sets up a renewal cron job/systemd timer automatically, so you generally don’t need to think about certificate expiry again.

Load Balancing Multiple Java Instances

If you’re running more than one instance of your Java app for redundancy or scaling, Nginx’s upstream block handles this cleanly:

upstream java_backend {
    least_conn;
    server 127.0.0.1:8080;
    server 127.0.0.1:8081;
    server 127.0.0.1:8082;
}

server {
    listen 80;
    server_name myjavaapp.example.com;

    location / {
        proxy_pass http://java_backend;
        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;
    }
}

I used least_conn here instead of the default round-robin because Java requests (especially ones hitting a database) can have uneven response times, and least-connections load balancing routes new requests to whichever backend currently has the fewest active connections — this tends to give more even load distribution in practice.

Testing Your Setup

Once everything’s in place, I go through this checklist every time:

  1. Test Nginx syntax: sudo nginx -t
  2. Check the Java app directly: curl http://127.0.0.1:8080 — confirm it responds before blaming Nginx for anything
  3. Check through Nginx: curl -I http://myjavaapp.example.com
  4. Check headers are being forwarded: add a temporary endpoint in your Java app that echoes back request headers, and confirm X-Forwarded-For and Host look correct
  5. Check HTTPS: curl -I https://myjavaapp.example.com
  6. Load test lightly: I usually run a quick ab -n 100 -c 10 https://myjavaapp.example.com/ just to make sure nothing falls over under light concurrent load

Troubleshooting Common Issues

502 Bad Gateway — This almost always means Nginx can’t reach your Java app. Check that the app is actually running (sudo ss -tlnp | grep 8080), that it’s bound to the right interface, and that your proxy_pass port matches.

504 Gateway Timeout — Your Java app is taking longer to respond than proxy_read_timeout allows. Either optimize the slow endpoint or increase the timeout.

413 Request Entity Too Large — If your app accepts file uploads, add client_max_body_size 20M; (adjust to your needs) inside the server block.

Redirects going to the wrong protocol/host — This is almost always a missing X-Forwarded-Proto or Host header, or a Spring Boot app that isn’t configured with forward-headers-strategy.

WebSocket connections failing — Double check the Upgrade and Connection headers are present exactly as shown above; a lot of guides online omit these and then WebSocket-based features silently break.

Security Considerations

A few things I always do on top of the base config:

  • Bind the Java app to localhost only. Never let Tomcat or Spring Boot listen on 0.0.0.0 if Nginx is meant to be the only entry point — otherwise attackers can bypass Nginx entirely.
  • Hide the Nginx version. Add server_tokens off; in the http block of /etc/nginx/nginx.conf so error pages and headers don’t leak your Nginx version.
  • Rate limit sensitive endpoints, like login forms, using limit_req_zone.
  • Set security headers like X-Content-Type-Options, X-Frame-Options, and Strict-Transport-Security at the Nginx layer so you don’t have to reimplement them in Java.
  • Keep both Nginx and your JVM patched. Old JVM versions and old Nginx versions both carry known CVEs.

Performance Tips

  • Enable gzip compression for text-based responses (gzip on;, gzip_types application/json text/css application/javascript;) — Java’s JSON responses compress very well.
  • Let Nginx serve static assets directly rather than proxying them through the JVM. If your app serves a /static/ path, add a dedicated location /static/ { root /var/www/myjavaapp; } block.
  • Tune worker processes — worker_processes auto; lets Nginx use all available CPU cores.
  • Use connection keep-alive to the upstream with keepalive 32; in the upstream block, which reduces the overhead of establishing new TCP connections to your JVM.

Real-World Use Case

A setup I’ve used more than once: a Spring Boot microservices backend with three replicas behind Nginx, TLS terminated at Nginx via Let’s Encrypt, static frontend assets served directly by Nginx, and API traffic proxied to the upstream pool with least-connections balancing. This gave us zero-downtime deploys (take one instance out of the upstream, redeploy it, add it back) without needing a dedicated load balancer product.

Best Practices Recap

  • Keep the Java process bound to localhost
  • Always forward Host, X-Real-IP, X-Forwarded-For, and X-Forwarded-Proto
  • Test config with nginx -t before every reload
  • Terminate TLS at Nginx, not in the JVM
  • Use upstream blocks for anything beyond a single instance
  • Monitor both the Nginx access/error logs and your Java app’s logs together when debugging

Setting up Nginx in front of a Java application isn’t complicated once you understand what each directive does — it’s really just a handful of proxy headers and a bit of TLS configuration. The time I spent debugging that 502 years ago taught me to always verify the backend independently before touching Nginx, and that one habit alone has saved me more hours than anything else in this guide.

Total
1
Shares

Leave a Reply

Previous Post
How to Set Up Nginx for a Laravel Application

How to Set Up Nginx for a Laravel Application

Next Post
How to Set Up Nginx for a Go Application

How to Set Up Nginx for a Go Application

Related Posts