Every time I set up a new VPS I end up googling "nginx reverse proxy config" and copying some random gist from 2019. Then I spend an hour wondering why WebSocket connections drop, or why SSL headers don't pass through, or why the damn thing returns 502 for no reason.

No more. Here's the config I actually use, with the bits that always trip me up.

Server terminal with configuration
The terminal where I spend way too much time ( trust me )

The Minimal Reverse Proxy

Here's the bare minimum. One app, one port, no SSL:

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

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

That's it. Four proxy headers and a pass directive. Most tutorials add 20 lines of nonsense you don't need.

Server rack in data center
Where it all lives ( somewhere like this )

Adding SSL With Certbot

Run certbot and it generates the SSL config for you. Don't write it by hand:

sudo certbot --nginx -d myapp.example.com

Done. Certbot modifies your config, sets up redirect from 80 to 443, and auto-renews. If you're writing SSL config blocks by hand in 2026, you're wasting time.

WebSocket Support

If your app uses WebSockets ( most do these days ), add this inside the location block:

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

The two lines that matter are proxy_http_version 1.1 and the Upgrade/Connection headers. Forget these and your Socket.io / ws connections will silently fail. No error, no log, just frustration.

Multiple Apps on One Server

One server, three apps. Each gets its own server block:

# /etc/nginx/sites-available/app1
server {
    listen 80;
    server_name app1.example.com;
    location / { proxy_pass http://127.0.0.1:3000; }
}

# /etc/nginx/sites-available/app2
server {
    listen 80;
    server_name app2.example.com;
    location / { proxy_pass http://127.0.0.1:4000; }
}

# /etc/nginx/sites-available/api
server {
    listen 80;
    server_name api.example.com;
    location / { proxy_pass http://127.0.0.1:5000; }
}

Symlink them into sites-enabled, run nginx -t, reload. Each app thinks it's alone on the server. Nginx handles the routing.

Data center with server racks
Running three apps on one box keeps costs down

The 502 Problem

502 Bad Gateway means nginx can't reach your upstream app. Every single time it's one of these:

1. Your app isn't running ( check with systemctl status )

2. Wrong port in proxy_pass ( check with ss -tlnp )

3. Your app binds to localhost but nginx proxies to 127.0.0.1 and they're different on IPv6 ( yes, really )

Number 3 took me two hours to debug once. Two hours for an IPv6 quirk. Learn from my pain.

Caching Static Assets

If your app serves static files, let nginx handle them instead:

location /static/ {
    alias /var/www/static/;
    expires 30d;
    add_header Cache-Control "public, immutable";
}

location /assets/ {
    alias /var/www/assets/;
    expires 7d;
    add_header Cache-Control "public";
}

Nginx serves static files way faster than Node or Python ever will. Offload that work.

The Config I Actually Use

Here's the full template I copy to every new server. Swap the domain and port, run certbot, done:

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

    location / {
        proxy_pass http://127.0.0.1:3000;
        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_read_timeout 86400s;
        proxy_send_timeout 86400s;
    }

    location /static/ {
        alias /var/www/static/;
        expires 30d;
        add_header Cache-Control "public, immutable";
    }
}

The 86400s timeouts are for WebSocket connections. Without them, long-running sockets get killed after 60 seconds. Ask me how I know.

Stop overthinking nginx configs. Start with the minimal version, add SSL via certbot, add WebSocket headers if you need them. That's 95% of use cases. The other 5%? That's what the docs are for.