Post

Configuring NGINX as a Reverse Proxy

Setting up NGINX as a reverse proxy in front of a Java application, with SSL termination and the configuration bits I always forget.

Configuring NGINX as a Reverse Proxy

There’s a certain type of problem that comes up every time you deploy a Java application to a VM — someone asks “but how does SSL work?” and “how do users actually reach this thing?”, and you end up spending an afternoon configuring NGINX.

NGINX sits in front of your application and handles the things your application shouldn’t have to care about — SSL termination, request buffering, and routing traffic to the backend. Your Java process just listens on localhost:8080 and doesn’t need to know anything about certificates.

Basic reverse proxy

Install on Ubuntu/Debian:

1
$ sudo apt-get install nginx

Create /etc/nginx/sites-available/myapp:

1
2
3
4
5
6
7
8
9
10
11
12
server {
    listen 80;
    server_name myapp.example.com;

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

Enable it:

1
2
3
$ sudo ln -s /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/
$ sudo nginx -t
$ sudo systemctl reload nginx

Always run nginx -t before reloading. It validates the config and saves you from taking down a running site with a typo.

What those headers actually do

Without X-Real-IP, your application sees 127.0.0.1 as the client IP for every single request, because every request comes from NGINX on localhost. This made one of our audit logs completely useless until someone noticed.

X-Forwarded-Proto tells the backend whether the original request was HTTP or HTTPS. Without it, your app will generate redirect URLs with the wrong scheme — you’ll redirect someone from https:// to http:// and spend an hour confused about why.

In WildFly, add this to the undertow config to make it trust these headers:

1
<http-listener name="default" ... proxy-address-forwarding="true"/>

Adding SSL

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
server {
    listen 80;
    server_name myapp.example.com;
    return 301 https://$host$request_uri;
}

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

    ssl_certificate     /etc/ssl/certs/myapp.crt;
    ssl_certificate_key /etc/ssl/private/myapp.key;

    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers HIGH:!aNULL:!MD5;
    ssl_prefer_server_ciphers on;

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

SSL terminates at NGINX. Your backend only ever sees plain HTTP on localhost. No need to configure certificates in your Java app.

Java apps and timeouts

Java applications can be slow to respond on cold start — JIT warmup, lazy initialization, that first database connection pool being established. NGINX’s default proxy timeout is 60 seconds, which is usually fine, but if you have a heavy initialization or a report endpoint that runs a complex query:

1
2
3
proxy_connect_timeout 10s;
proxy_read_timeout    120s;
proxy_send_timeout    120s;

I’ve been bitten by the default timeout on a report that took 90 seconds to generate. Endpoint worked fine in testing (warm JVM, small dataset). In production it timed out because NGINX gave up before the query finished. Not a fun thing to debug when users are complaining.

Checking logs

1
2
$ tail -f /var/log/nginx/access.log
$ tail -f /var/log/nginx/error.log

If something isn’t working and you’re not sure why, the error log is usually very direct about it. Most common thing I see: connect() failed (111: Connection refused) while connecting to upstream — which means NGINX is running but your Java app is not.

This post is licensed under CC BY 4.0 by the author.