A reverse proxy receives requests from clients and forwards them to one or more backend applications. It’s how most production apps are served: the app (Node.js, Python, Go, Java, PHP-FPM) listens on a local port, and Nginx handles the public side — TLS, compression, static files, caching and load balancing.
Basic setup
apt install -y nginx # Debian/Ubuntu
# dnf install -y nginx && systemctl enable --now nginx # AlmaLinux/Rocky
Assume your app listens on 127.0.0.1:3000. Create /etc/nginx/sites-available/example.com (on RHEL-family systems, use /etc/nginx/conf.d/example.com.conf):
server {
listen 80;
server_name example.com www.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;
}
}
ln -s /etc/nginx/sites-available/example.com /etc/nginx/sites-enabled/
nginx -t && systemctl reload nginx
On AlmaLinux/Rocky with SELinux enforcing, allow Nginx to connect to backends: setsebool -P httpd_can_network_connect 1.
Add HTTPS
apt install -y certbot python3-certbot-nginx
certbot --nginx -d example.com -d www.example.com --redirect
Serve static files directly
Let Nginx serve assets and only proxy dynamic requests:
location /static/ {
alias /var/www/example/static/;
expires 30d;
access_log off;
}
location / {
proxy_pass http://127.0.0.1:3000;
# headers as above
}
Route by path
location /api/ {
proxy_pass http://127.0.0.1:4000/; # trailing slash strips /api/ from the path
}
location / {
proxy_pass http://127.0.0.1:3000;
}
The trailing slash matters: proxy_pass http://backend/; replaces the matched location prefix; proxy_pass http://backend; passes the original path unchanged. This is the most common reverse-proxy mistake.
Load balancing
upstream app {
least_conn;
server 127.0.0.1:3000;
server 127.0.0.1:3001;
server 10.10.0.2:3000 backup;
keepalive 32;
}
server {
# …
location / {
proxy_pass http://app;
proxy_http_version 1.1;
proxy_set_header Connection "";
}
}
Methods: round-robin (default), least_conn, ip_hash (sticky by client IP). Nginx marks failed servers as unavailable automatically (max_fails, fail_timeout).
Caching responses
proxy_cache_path /var/cache/nginx/app levels=1:2 keys_zone=app:50m max_size=1g inactive=60m;
location / {
proxy_cache app;
proxy_cache_valid 200 10m;
proxy_cache_use_stale error timeout updating http_500 http_502 http_503;
add_header X-Cache $upstream_cache_status;
proxy_pass http://app;
}
proxy_cache_use_stale keeps serving cached pages if the backend is briefly down.
Timeouts and limits
proxy_connect_timeout 5s;
proxy_read_timeout 60s; # raise for long requests or WebSockets
client_max_body_size 20m; # uploads
WebSockets
Add the Upgrade and Connection headers — see secure WebSocket server.
Common errors
| Error | Meaning |
|---|---|
| 502 Bad Gateway | Backend not running / wrong port / SELinux blocking |
| 504 Gateway Timeout | Backend too slow — raise proxy_read_timeout or fix the app |
| 413 Request Entity Too Large | Raise client_max_body_size |
App generates http:// links |
Pass X-Forwarded-Proto and enable proxy trust in the app |
Check /var/log/nginx/error.log first — it names the exact cause.
Summary
Keep apps on localhost, let Nginx handle TLS, static files and routing, and pass the forwarding headers. Related: reverse proxy for multiple Docker containers, VPS as a reverse-proxy gateway and hosting multiple websites.