How to Set Up Nginx for a Java Application

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:

Requirements

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

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

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:

Performance Tips

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

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.

Exit mobile version