Blog
3 min read

Nginx Reverse Proxy Configuration: A Production-Ready Example

A complete Nginx reverse proxy config for a Node.js or Python app, explained line by line: proxy_pass, forwarding headers, WebSockets, timeouts, upload limits, HTTPS redirect, gzip, static files, multiple apps by domain, and how to test and reload safely.

A reverse proxy sits in front of your app: it accepts connections on ports 80 and 443, handles HTTPS, and forwards requests to your app on an internal port. Nginx is the most widely used one. (Reverse proxies explained, What is Nginx?)

Here's a complete config, then what each part does.

The config

/etc/nginx/sites-available/myapp:

upstream myapp {
    server 127.0.0.1:3000;
    keepalive 32;
}

# Redirect HTTP to HTTPS
server {
    listen 80;
    listen [::]:80;
    server_name example.com www.example.com;
    return 301 https://example.com$request_uri;
}

# Redirect www to the bare domain
server {
    listen 443 ssl;
    listen [::]:443 ssl;
    http2 on;
    server_name www.example.com;
    ssl_certificate     /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
    return 301 https://example.com$request_uri;
}

server {
    listen 443 ssl;
    listen [::]:443 ssl;
    http2 on;
    server_name example.com;

    ssl_certificate     /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

    client_max_body_size 20M;

    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
    add_header X-Content-Type-Options nosniff always;

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

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

        # WebSockets
        proxy_set_header Upgrade    $http_upgrade;
        proxy_set_header Connection $connection_upgrade;

        proxy_read_timeout 60s;
    }
}

The $connection_upgrade variable needs this in the http block (e.g. /etc/nginx/conf.d/upgrade.conf):

map $http_upgrade $connection_upgrade {
    default upgrade;
    ''      '';
}

What each part does

upstream and proxy_pass

upstream names where your app runs; proxy_pass http://myapp; sends requests there. keepalive reuses connections to your app instead of opening a new one per request (it needs proxy_http_version 1.1, and an empty Connection header for normal requests — which the map above provides). List several server lines in upstream to load-balance. (What is a load balancer?)

Forwarded headers

Without them, your app thinks every request comes from 127.0.0.1 over plain HTTP:

  • Host — the domain the visitor used
  • X-Forwarded-For / X-Real-IP — the visitor's real IP (needed for rate limiting and logs)
  • X-Forwarded-Proto — https, so the app builds correct URLs and sets secure cookies

Tell your app to trust them: app.set('trust proxy', 1) in Express, ProxyFix in Flask, SECURE_PROXY_SSL_HEADER in Django. (Deploy an Express app)

WebSockets

The Upgrade and Connection headers let WebSocket connections pass through. Without them, real-time features fail. Long-lived sockets may also need a longer proxy_read_timeout. (WebSockets explained)

Timeouts

proxy_read_timeout (default 60s) is how long Nginx waits for your app before returning 504. Raise it only for specific slow routes. (504 Gateway Timeout)

client_max_body_size

Default is 1 MB — uploads larger than that get 413. (413 Request Entity Too Large)

Security headers

always makes Nginx add them to error responses too. Note that add_header in a location block replaces all headers from the server block — a common surprise. (HTTP security headers, What is HSTS?)

Serving static files directly

Let Nginx serve built assets without touching your app:

location /assets/ {
    root /srv/myapp/dist;
    expires 1y;
    add_header Cache-Control "public, immutable";
}

(HTTP caching headers)

Several apps on one server

One server block per domain, each with its own proxy_pass to a different port: app.example.com → 3000, api.example.com → 4000.

Enable, test, reload

sudo ln -s /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/
sudo nginx -t                    # always test first
sudo systemctl reload nginx      # no downtime

nginx -t catches syntax errors before they take your site down.

HTTPS certificates

Get them with Certbot, which can edit this config for you and renews automatically. (Certbot and Let's Encrypt)

Troubleshooting

  • 502 — app not running or wrong port in upstream. (502 Bad Gateway)
  • Redirect loop — app also redirects to HTTPS but doesn't see X-Forwarded-Proto, or Cloudflare Flexible SSL. (ERR_TOO_MANY_REDIRECTS)
  • Logs: /var/log/nginx/error.log and access.log.

EasySpawn puts a configured reverse proxy with automatic HTTPS in front of every app on your server — forwarded headers, WebSockets and sensible limits included. See how it works or join the waitlist.

Related: What Is Nginx? · Certbot and Let's Encrypt · What Is Caddy? · Reverse Proxies Explained

Keep reading