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 usedX-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";
}
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.logandaccess.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
Writing a systemd Service File for Your App (With Hardening)
A production-ready systemd unit for a Node.js or Python app, explained: Type, User, WorkingDirectory, EnvironmentFile, Restart and backoff, graceful stop, resource limits, logging, and the sandboxing options (ProtectSystem, NoNewPrivileges, PrivateTmp) that limit damage if the app is compromised.
How to Deploy a FastAPI App to Production
Running uvicorn main:app --reload is for development. A production FastAPI deploy needs worker processes, a process manager or container, a reverse proxy with HTTPS, proper settings, migrations, and health checks. Step-by-step options for a VPS and Docker.