How to Configure Nginx Reverse Proxy: From Request Forwarding to Header Handling
This guide walks through the complete lifecycle of an Nginx reverse‑proxy request on Debian/Ubuntu, covering configuration inspection, server and location selection, URI rewriting, upstream pools, header forwarding, TLS termination, timeouts, buffering, WebSocket upgrades, retry logic, rollback procedures, and troubleshooting common error codes.
Preserve Current Configuration
When a 502 appears, first verify that Nginx is listening, the configuration is loaded, the upstream is reachable, or the application returned an error. Avoid an immediate reload because it may discard a still‑working old worker.
nginx -v
nginx -V 2>&1
systemctl status nginx --no-pager
ss -lntp | grep -E ':(80|443)\b'
nginx -T > /tmp/nginx-config-$(date +%Y%m%d%H%M%S).txtRequest Selection and Forwarding
The request first matches a listen address/port, then TLS SNI or HTTP Host selects a server. Inside the server block Nginx matches location directives: exact match, longest ordinary prefix, then regular expressions. The ^~ modifier stops further regex processing.
upstream app_backend {
server 127.0.0.1:8080;
keepalive 32;
}
server {
listen 80;
server_name example.com;
location / {
proxy_pass http://app_backend;
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;
}
} keepalivedefines idle upstream connections per worker, not a concurrency limit. proxy_set_header X-Forwarded-For appends the client address; the original header is not overwritten.
proxy_pass Trailing Slash and URI Rewrite
Without a trailing slash the original URI is passed unchanged. With a trailing slash the matched location prefix is stripped before forwarding.
# No trailing slash – URI stays the same
location /api/ {
proxy_pass http://app_backend;
}
# With trailing slash – prefix is removed
location /api/ {
proxy_pass http://app_backend/;
}In the second case a request for /api/users?id=1 reaches the upstream as /users?id=1. Verify the actual path with a test service before production.
Host and Forwarded Headers
$host: uses the request line or Host header, falls back to server_name. $http_host: raw Host header, may include a port. $proxy_host: name and port from proxy_pass, useful for internal routing.
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;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Port $server_port;If Nginx sits behind another load balancer, the client address seen by Nginx is the balancer’s IP. Use the real_ip module with trusted CIDR ranges to replace $remote_addr safely.
set_real_ip_from 10.20.0.0/16;
set_real_ip_from 192.0.2.10/32;
real_ip_header X-Forwarded-For;
real_ip_recursive on;Upstream Pool, Connection Reuse and Health Checks
Passive failure handling uses max_fails and fail_timeout together with proxy_next_upstream. These settings react only to connection errors, not to active health checks. A single‑server upstream behaves differently from a multi‑node pool.
upstream app_backend {
least_conn;
server 10.10.1.11:8080 max_fails=3 fail_timeout=10s;
server 10.10.1.12:8080 max_fails=3 fail_timeout=10s;
keepalive 64;
}For TLS upstreams, enable SNI and certificate verification:
location /secure-api/ {
proxy_pass https://secure_backend/;
proxy_ssl_server_name on;
proxy_ssl_name api.internal.example;
proxy_ssl_verify on;
proxy_ssl_trusted_certificate /etc/ssl/certs/ca-certificates.crt;
proxy_ssl_verify_depth 3;
}Timeouts, Buffering and Request Body
Set timeouts according to service‑level objectives. proxy_connect_timeout protects against unreachable upstreams; proxy_send_timeout and proxy_read_timeout bound idle periods between reads/writes. Buffering is enabled by default; disable it for streaming or Server‑Sent Events.
location /api/ {
proxy_pass http://app_backend/;
proxy_connect_timeout 3s;
proxy_send_timeout 30s;
proxy_read_timeout 30s;
send_timeout 30s;
client_max_body_size 10m;
}
location /events/ {
proxy_pass http://app_backend;
proxy_http_version 1.1;
proxy_buffering off;
proxy_cache off;
proxy_read_timeout 1h;
proxy_set_header Connection "";
}
location /upload/ {
client_max_body_size 2g;
proxy_request_buffering off;
proxy_pass http://upload_backend;
proxy_read_timeout 10m;
}When buffering is disabled, the upstream connection may be held open by a slow client; monitor worker limits and temporary file paths ( client_body_temp_path, proxy_temp_path).
WebSocket and HTTP Upgrade
HTTP Upgrade is not automatically passed. Use a map to set the proper Connection header.
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
server {
listen 80;
server_name example.com;
location /ws/ {
proxy_pass http://app_backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_read_timeout 60m;
}
}Validate the handshake with curl -i -H 'Connection: Upgrade' -H 'Upgrade: websocket' … and expect a 101 response.
Retry Logic and Idempotency
proxy_next_upstreamcan retry on connection errors, timeouts or specific HTTP codes. Enable it only for idempotent methods; otherwise a non‑idempotent POST may cause duplicate side effects.
location /api/ {
proxy_pass http://app_backend;
proxy_next_upstream error timeout http_502 http_503 http_504;
proxy_next_upstream_tries 2;
proxy_next_upstream_timeout 5s;
}TLS Termination and HTTP Redirect
Typical production setup uses port 80 for HTTP‑to‑HTTPS redirects and port 443 for TLS termination.
server {
listen 80;
server_name example.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
server_name example.com;
ssl_certificate /path/to/cert.pem;
ssl_certificate_key /path/to/key.pem;
ssl_protocols TLSv1.2 TLSv1.3;
location / {
proxy_pass http://app_backend;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto https;
}
}Check certificate name, validity, chain and key match before rollout using openssl x509 -in … and openssl pkey -in ….
Rollback‑able Configuration Changes
#!/usr/bin/env bash
set -euo pipefail
STAMP="$(date +%Y%m%d%H%M%S)"
BACKUP_DIR="/var/backups/nginx-${STAMP}"
install -d -m 0700 "${BACKUP_DIR}"
cp -a /etc/nginx "${BACKUP_DIR}/"
nginx -T > "${BACKUP_DIR}/nginx-T.txt" 2>&1
printf 'Backup saved to %s
' "${BACKUP_DIR}"Modify a single site file in sites-available, link it into sites-enabled, test with nginx -t, then reload. If validation fails, restore the changed file and reload again.
Diagnosing 502, 504, 499 and Redirect Loops
502 Bad Gateway : No usable upstream response. Search the error log for patterns such as connect() failed, upstream prematurely closed, no live upstreams, host not found. Verify upstream reachability directly from the Nginx host.
grep -E 'connect\(\) failed|upstream prematurely closed|no live upstreams|host not found' /var/log/nginx/error.log | tail -n 100504 Gateway Timeout : Connection or read timeout. Extract structured JSON logs to pinpoint the stage that timed out.
jq -r 'select(.status == 504) | [.time,.request_id,.request,.upstream_addr,.upstream_connect_time,.upstream_header_time,.upstream_response_time] | @tsv' /var/log/nginx/proxy-access.json | tail -n 50499 Client Closed Request : Client closed the connection before Nginx responded. Correlate $request_time with upstream timings and any front‑end load‑balancer logs.
awk '$9 == 499 {print}' /var/log/nginx/access.log | tail -n 50Redirect loops : Often caused by trusting X-Forwarded-Proto when TLS is terminated upstream. Inspect the redirect chain with curl.
curl -sS -o /dev/null -D - --max-redirs 0 http://example.com/
curl -sS -o /dev/null -D - --max-redirs 0 https://example.com/Automation Acceptance Script
The script below checks syntax, a local health endpoint, the external URL response code, and recent emerg / alert logs.
#!/usr/bin/env bash
set -euo pipefail
SITE_URL="https://example.com"
LOCAL_HEALTH="http://127.0.0.1/nginx-health"
nginx -t
systemctl is-active --quiet nginx
curl --fail --silent --show-error --max-time 5 "$LOCAL_HEALTH" > /dev/null
http_code=$(curl --silent --show-error --output /dev/null \
--write-out '%{http_code}' --max-time 10 "$SITE_URL")
case "$http_code" in
200|204|301|302) ;;
*) printf 'Unexpected HTTP status: %s
' "$http_code" >&2; exit 1 ;;
esac
if journalctl -u nginx --since '5 minutes ago' --no-pager | grep -qE '\[emerg\]|\[alert\]'; then
echo 'Recent nginx emerg/alert log found' >&2
exit 1
fi
printf 'Nginx validation passed; status=%s
' "$http_code"Key Review Questions for Reverse‑Proxy Configurations
Is the URI kept or stripped?
Which Host does the upstream expect?
What is the trusted source range for client IPs?
Are retries allowed and are the methods idempotent?
Do request/response bodies need buffering?
What are the maximum request size and the individual timeout values?
Is there a dedicated configuration for WebSocket/SSE?
Is upstream TLS verification enabled (SNI, certificate chain, depth)?
Do logs contain request IDs and segmented timing?
How are changes rolled out, validated and rolled back?
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
MaGe Linux Operations
Founded in 2009, MaGe Education is a top Chinese high‑end IT training brand. Its graduates earn 12K+ RMB salaries, and the school has trained tens of thousands of students. It offers high‑pay courses in Linux cloud operations, Python full‑stack, automation, data analysis, AI, and Go high‑concurrency architecture. Thanks to quality courses and a solid reputation, it has talent partnerships with numerous internet firms.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
