Skip to main content
Infrastructure·10 min read

Nginx Reverse Proxy Setup — Fixing 502 Bad Gateway and Upstream Timeouts

Configure Nginx as a reverse proxy for Node.js, Python, and Java backends. Covers load balancing, SSL termination, WebSocket proxying, and production hardening.

DT

DevOps Engineer & Technical Writer

The Problem

Your application server listens on port 3000. You need it accessible on port 80/443, with SSL termination, rate limiting, and the ability to route traffic to multiple backend services based on URL path. Running your application directly on port 80 means running as root, no SSL offloading, no buffering, and no graceful failover.

NGINX REVERSE PROXY WITH LOAD BALANCING CLIENTS Internet HTTPS requests port 443 Nginx Reverse Proxy SSL Termination (443) Load Balancing Rate Limiting Buffering + Headers UPSTREAM App1 :3000 ● healthy UPSTREAM App2 :3001 ● healthy UPSTREAM App3 :3002 ● slow proxy_next_upstream: automatic failover on error/timeout

Nginx as a reverse proxy solves all of these problems while adding a resilient front layer to your infrastructure.

Installing Nginx

# Ubuntu/Debian

sudo apt update && sudo apt install nginx -y

# RHEL/CentOS/Amazon Linux

sudo yum install nginx -y

# Verify installation

nginx -v

sudo systemctl start nginx

sudo systemctl enable nginx

Confirm Nginx is running:

curl -I http://localhost

# Should return HTTP/1.1 200 OK with Server: nginx

Basic Reverse Proxy Configuration

The simplest reverse proxy forwards all traffic to a backend application:

# /etc/nginx/sites-available/myapp

server {

listen 80;

server_name app.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;

}

}

Enable the site:

sudo ln -s /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/

sudo nginx -t # Test configuration syntax

sudo systemctl reload nginx

Why those proxy headers matter

  • Host — Your backend needs the original hostname, not 127.0.0.1
  • X-Real-IP — The actual client IP (otherwise backend sees Nginx's IP)
  • X-Forwarded-For — Chain of proxies the request passed through
  • X-Forwarded-Proto — Whether the client connected via HTTP or HTTPS

Without these headers, your application logs show all traffic coming from 127.0.0.1 and cannot determine if the original connection was secure.

SSL Termination with Let's Encrypt

In production, Nginx handles SSL so your backend doesn't have to:

# Install certbot

sudo apt install certbot python3-certbot-nginx -y

# Obtain certificate (Nginx plugin handles config automatically)

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

# Verify auto-renewal

sudo certbot renew --dry-run

After certbot runs, your config will look like:

server {

listen 443 ssl http2;

server_name app.example.com;

ssl_certificate /etc/letsencrypt/live/app.example.com/fullchain.pem;

ssl_certificate_key /etc/letsencrypt/live/app.example.com/privkey.pem;

ssl_protocols TLSv1.2 TLSv1.3;

ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;

ssl_prefer_server_ciphers off;

# HSTS — tell browsers to always use HTTPS

add_header Strict-Transport-Security "max-age=63072000" always;

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;

}

}

# Redirect HTTP to HTTPS

server {

listen 80;

server_name app.example.com;

return 301 https://$server_name$request_uri;

}

Load Balancing Multiple Backends

When running multiple instances of your application:

upstream app_backend {

# Round-robin by default

server 10.0.1.10:3000;

server 10.0.1.11:3000;

server 10.0.1.12:3000;

# Mark a server as backup (used only when others are down)

server 10.0.1.13:3000 backup;

# Health checks — remove server after 3 failures

server 10.0.1.10:3000 max_fails=3 fail_timeout=30s;

}

server {

listen 443 ssl http2;

server_name app.example.com;

location / {

proxy_pass http://app_backend;

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;

# Retry next upstream on failure

proxy_next_upstream error timeout http_502 http_503;

proxy_next_upstream_tries 2;

}

}

Load balancing algorithms

upstream app_backend {

# Least connections — sends to server with fewest active connections

least_conn;

server 10.0.1.10:3000;

server 10.0.1.11:3000;

}

upstream app_backend_sticky {

# IP hash — same client always hits same server (sticky sessions)

ip_hash;

server 10.0.1.10:3000;

server 10.0.1.11:3000;

}

upstream app_backend_weighted {

# Weighted — server with weight 3 gets 3x more traffic

server 10.0.1.10:3000 weight=3;

server 10.0.1.11:3000 weight=1;

}

Path-Based Routing to Different Services

Route traffic to different backends based on URL path — a common microservices pattern:

upstream api_service {

server 10.0.1.10:8080;

server 10.0.1.11:8080;

}

upstream frontend_service {

server 10.0.1.20:3000;

}

upstream auth_service {

server 10.0.1.30:4000;

}

server {

listen 443 ssl http2;

server_name app.example.com;

# API requests

location /api/ {

proxy_pass http://api_service/;

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;

}

# Authentication endpoints

location /auth/ {

proxy_pass http://auth_service/;

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;

}

# Everything else goes to frontend

location / {

proxy_pass http://frontend_service;

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;

}

}

Note the trailing slash in proxy_pass http://api_service/ — this strips the /api/ prefix before forwarding. Without it, the backend receives the full path including /api/.

WebSocket Proxying

WebSocket connections need special handling because they upgrade from HTTP:

location /ws/ {

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;

# Increase timeouts for long-lived connections

proxy_read_timeout 86400s;

proxy_send_timeout 86400s;

}

Without the Upgrade and Connection headers, the WebSocket handshake fails and clients get a 400 error.

Production Hardening

Buffering and timeouts

server {

# Buffer responses from backend

proxy_buffering on;

proxy_buffer_size 4k;

proxy_buffers 8 16k;

proxy_busy_buffers_size 32k;

# Timeouts — prevent hanging connections

proxy_connect_timeout 5s;

proxy_read_timeout 60s;

proxy_send_timeout 60s;

# Client body size — increase for file uploads

client_max_body_size 50m;

}

Rate limiting

# Define rate limit zone in http block

http {

limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s;

limit_req_zone $binary_remote_addr zone=login_limit:10m rate=1r/s;

}

server {

location /api/ {

limit_req zone=api_limit burst=20 nodelay;

proxy_pass http://api_service;

}

location /auth/login {

limit_req zone=login_limit burst=5;

proxy_pass http://auth_service;

}

}

Security headers

server {

add_header X-Frame-Options "SAMEORIGIN" always;

add_header X-Content-Type-Options "nosniff" always;

add_header X-XSS-Protection "1; mode=block" always;

add_header Referrer-Policy "strict-origin-when-cross-origin" always;

# Hide Nginx version

server_tokens off;

# Deny access to hidden files

location ~ /\. {

deny all;

return 404;

}

}

Testing and Debugging

# Test configuration syntax before reloading

sudo nginx -t

# Reload without dropping connections

sudo systemctl reload nginx

# Check error log for proxy issues

sudo tail -f /var/log/nginx/error.log

# Check access log for request patterns

sudo tail -f /var/log/nginx/access.log

# Test with curl — verbose shows headers

curl -vI https://app.example.com

# Test specific backend directly (bypass Nginx)

curl http://127.0.0.1:3000/health

Common error patterns in logs

# Backend not responding

connect() failed (111: Connection refused) while connecting to upstream

# Backend too slow

upstream timed out (110: Connection timed out) while reading response header

# Request body too large

client intended to send too large body

Common Mistakes

  • Missing proxy_set_header Host $host — Backend receives Host: 127.0.0.1 instead of the real domain. This breaks virtual hosting and redirect generation.
  • Trailing slash confusionproxy_pass http://backend/ (with slash) strips the matched location prefix. proxy_pass http://backend (no slash) passes the full URI.
  • Not increasing client_max_body_size — Default is 1MB. File uploads fail with 413 Request Entity Too Large.
  • Forgetting HTTP to HTTPS redirect — Users typing http:// get an Nginx default page instead of being redirected.
  • Using proxy_pass with variables without resolver — If you use variables in proxy_pass (e.g., proxy_pass http://$backend), you must set resolver 8.8.8.8 or Nginx cannot resolve the hostname.
  • Not testing with nginx -t before reload — A syntax error in any config file takes down all sites, not just the one you edited.
  • Quick Reference

    TaskDirective
    Basic proxy<code class="inline-code">proxy_pass http://127.0.0.1:3000</code>
    SSL termination<code class="inline-code">listen 443 ssl http2</code> + cert paths
    Load balance<code class="inline-code">upstream name { server ip:port; }</code>
    Sticky sessions<code class="inline-code">ip_hash</code> in upstream block
    WebSocket support<code class="inline-code">proxy_set_header Upgrade $http_upgrade</code>
    Rate limiting<code class="inline-code">limit_req_zone</code> + <code class="inline-code">limit_req zone=name</code>
    Max upload size<code class="inline-code">client_max_body_size 50m</code>
    Hide version<code class="inline-code">server_tokens off</code>
    Health check retry<code class="inline-code">proxy_next_upstream error timeout</code>
    Test config<code class="inline-code">sudo nginx -t</code>

    Summary

    Nginx as a reverse proxy gives you SSL termination, load balancing, path-based routing, rate limiting, and connection buffering — all without modifying your application code. Start with the basic proxy config, add SSL via certbot, then layer on load balancing and hardening as your traffic grows.

    ---

    Frequently Asked Questions

    What is a reverse proxy and why use NGINX for it?

    A reverse proxy sits in front of backend servers, forwarding client requests and returning responses. NGINX is popular for this because it handles thousands of concurrent connections efficiently, provides load balancing, SSL termination, caching, and request routing. It reduces backend load and provides a single entry point for security and routing rules.

    How do I configure NGINX as a reverse proxy?

    Create a server block with location / { proxy_pass http://backend:8080; } to forward all requests to your backend. Add essential headers: proxy_set_header Host $host;, proxy_set_header X-Real-IP $remote_addr;, and proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;. Place the configuration in /etc/nginx/conf.d/ and reload with nginx -s reload.

    Why is NGINX returning 502 Bad Gateway?

    502 means NGINX cannot reach the upstream backend server. Common causes: backend process isn't running, wrong port in proxy_pass, firewall blocking the connection, or backend taking too long to respond. Check backend health, verify the proxy_pass URL, review NGINX error logs (/var/log/nginx/error.log), and increase proxy_read_timeout if the backend is slow.

    How do I set up SSL/TLS with NGINX?

    Add listen 443 ssl;, ssl_certificate /path/to/cert.pem;, and ssl_certificate_key /path/to/key.pem; to your server block. Redirect HTTP to HTTPS with a separate server block on port 80 using return 301 https://$host$request_uri;. Use ssl_protocols TLSv1.2 TLSv1.3; and strong cipher suites. For free certificates, use certbot with Let's Encrypt.

    How do I load balance between multiple backends with NGINX?

    Define an upstream block: upstream backend { server 10.0.0.1:8080; server 10.0.0.2:8080; } and reference it with proxy_pass http://backend;. NGINX uses round-robin by default. Add weight=3 for weighted distribution, least_conn; for least-connections algorithm, or ip_hash; for session persistence based on client IP.

    ---