HomeProjectsBlogContact

Nginx Reverse Proxy Explained: Config Examples That Just Work

What a reverse proxy does, why almost every production app sits behind one, and copy-ready Nginx configs for Node.js apps, WebSockets, static files, upload limits and load balancing.

Nginx Reverse Proxy Explained: Config Examples That Just Work

Look at almost any production web app and you'll find something sitting in front of it: Nginx, a load balancer, a CDN. That something is a reverse proxy. Understanding what it does explains a lot of mysterious behaviour, like 502 errors, wrong client IPs and WebSockets that refuse to connect.

What a reverse proxy is

A reverse proxy receives requests from the internet and forwards them to one or more servers behind it, then passes the responses back. The client only ever talks to the proxy.

Browser  ->  Nginx (port 443)  ->  Your app (127.0.0.1:3000)

Why put something in front of your app?

  • HTTPS termination: Nginx handles certificates and encryption, so your app doesn't have to.
  • Static files: serves images, CSS and JavaScript very efficiently, freeing your app for real work.
  • Security and limits: request size limits, timeouts and rate limiting before traffic reaches your code.
  • Multiple apps on one server: route by domain or path to different backends.
  • Load balancing: spread traffic across several app instances.
  • Buffering: protects your app from slow clients holding connections open.

The basic config

server {
    listen 80;
    server_name example.com;

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

Those headers matter. Without them, your app thinks every request comes from 127.0.0.1 over plain HTTP. Then client IPs in logs are wrong, rate limiting by IP limits everyone at once (rate limiting), and redirect logic may loop because the app doesn't know the request was HTTPS.

Tell your framework to trust the proxy so it reads these headers. In Express, for example: app.set('trust proxy', 1).

HTTPS

After installing a certificate (Certbot can edit this config for you), you'll have a listen 443 ssl block and a redirect from port 80:

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

(HTTPS explained.)

WebSockets

WebSockets need the HTTP upgrade headers passed through, or connections fail:

location /socket/ {
    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_read_timeout 1h;   # keep long-lived connections open
}

(WebSockets vs SSE vs polling.)

Serving static files directly

location /assets/ {
    alias /var/www/app/public/assets/;
    expires 1y;
    add_header Cache-Control "public, immutable";
}

Hashed asset files can be cached for a year. (Caching strategies.)

Upload size limits

Nginx's default request body limit is small (1MB). Uploads larger than that fail with 413 before your app sees them:

client_max_body_size 20M;

Set it to the largest upload you actually allow. (File upload guide.)

Compression

gzip on;
gzip_types text/plain text/css application/json application/javascript image/svg+xml;
gzip_min_length 1024;

Multiple apps on one server

server {
    server_name api.example.com;
    location / { proxy_pass http://127.0.0.1:4000; }
}

server {
    server_name example.com;
    location / { proxy_pass http://127.0.0.1:3000; }
}

Load balancing

upstream app_servers {
    least_conn;
    server 127.0.0.1:3000;
    server 127.0.0.1:3001;
}

server {
    location / {
        proxy_pass http://app_servers;
    }
}

Run two app instances, and you can restart them one at a time for zero-downtime deploys. Keep the app stateless (sessions in a database or Redis, not memory) so any instance can serve any request. (Scaling a web app.)

Troubleshooting

Symptom Likely cause
502 Bad Gateway App isn't running, crashed, or is on a different port
504 Gateway Timeout App too slow; check slow queries or external calls
413 Request Entity Too Large client_max_body_size too low
WebSocket fails to connect Missing Upgrade/Connection headers
Redirect loop on HTTPS App doesn't trust X-Forwarded-Proto
All users share one IP in logs Missing forwarded headers or trust proxy setting

Always validate config before reloading:

sudo nginx -t && sudo systemctl reload nginx

And check the error log when something's wrong: sudo tail -f /var/log/nginx/error.log. (Status codes explained.)

FAQ

What is a reverse proxy?

A server that receives client requests and forwards them to backend servers, returning their responses. It commonly handles HTTPS, static files, load balancing and request limits.

What is the difference between a forward proxy and a reverse proxy?

A forward proxy acts on behalf of clients, for example a company proxy for browsing. A reverse proxy acts on behalf of servers, sitting in front of them to receive incoming traffic.

Why does Nginx return 502 Bad Gateway?

Nginx couldn't get a valid response from the app behind it, usually because the app isn't running, crashed, or is listening on a different address or port than proxy_pass points to.

Keep reading

More from the workbench

All articles →