A reverse proxy sits between your clients and backend servers, forwarding requests and returning responses. Nginx is the most popular choice for this role in 2026 — it handles SSL termination, load balancing, caching, and WebSocket proxying with minimal configuration. This guide covers every production-ready pattern you need.
Quick Answers: nginx proxy_pass Trailing Slash
The trailing slash question is really a URI replacement question: proxy_pass with a URI rewrites the matching location prefix; proxy_pass without a URI forwards the original request URI.
What happens with proxy_pass http://backend?
No URI is specified, so Nginx forwards the full request URI. A request for /api/users reaches the upstream as /api/users.
What happens with proxy_pass http://backend/?
The slash is a URI of /. Nginx replaces the matching location prefix with /, so location /api/ plus /api/users reaches the upstream as /users.
What happens with proxy_pass http://backend/service/?
The /service/ URI replaces the matching location prefix. For location /api/, /api/users becomes /service/users upstream.
Can I use a URI part in regex or named locations?
Avoid it. When Nginx cannot determine the prefix to replace, specify proxy_pass without a URI and use rewrite rules or variables deliberately.
Request Mapping Examples
| location | proxy_pass | request | upstream URI |
|---|---|---|---|
| /api/ | http://backend | /api/users | /api/users |
| /api/ | http://backend/ | /api/users | /users |
| /docs/ | http://backend/manual/ | /docs/install.html | /manual/install.html |
# proxy_pass trailing slash behavior
# Rule: proxy_pass with a URI replaces the matching location prefix.
# proxy_pass without a URI passes the full original request URI.
server {
listen 80;
server_name example.com;
# Keep the /api/ prefix on the upstream request
# Request: /api/users?id=1
# Upstream: http://api_backend/api/users?id=1
location /api/ {
proxy_pass http://api_backend;
}
# Strip the /app/ prefix before forwarding
# Request: /app/dashboard
# Upstream: http://app_backend/dashboard
location /app/ {
proxy_pass http://app_backend/;
}
# Replace /docs/ with /manual/
# Request: /docs/install.html
# Upstream: http://docs_backend/manual/install.html
location /docs/ {
proxy_pass http://docs_backend/manual/;
}
# In regex or named locations, do not add a URI part to proxy_pass.
# Use rewrite or pass the full request URI explicitly instead.
location ~ ^/files/(.*)$ {
proxy_pass http://file_backend;
}
}Basic Reverse Proxy Setup
The simplest reverse proxy forwards all requests to a single backend server. The key headers ensure your backend sees the real client IP and protocol.
# Basic reverse proxy configuration
server {
listen 80;
server_name example.com www.example.com;
location / {
proxy_pass http://localhost:3000;
# Pass original request headers to the 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;
# Timeouts (seconds)
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
# Buffer settings
proxy_buffering on;
proxy_buffer_size 16k;
proxy_buffers 4 16k;
}
}SSL/TLS Termination
SSL termination at Nginx means your backend servers only handle HTTP internally, simplifying their configuration. Nginx handles the encryption overhead.
# Full SSL/TLS reverse proxy with HTTP redirect
server {
listen 80;
server_name example.com www.example.com;
# Permanent redirect all HTTP traffic to HTTPS
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name example.com www.example.com;
# SSL certificate (Let's Encrypt)
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
# Modern SSL settings (2026 recommended)
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305;
ssl_prefer_server_ciphers off;
# SSL session cache
ssl_session_timeout 1d;
ssl_session_cache shared:SSL:10m;
ssl_session_tickets off;
# HSTS: tell browsers to always use HTTPS (6 months)
add_header Strict-Transport-Security "max-age=15768000" always;
# OCSP stapling
ssl_stapling on;
ssl_stapling_verify on;
resolver 8.8.8.8 8.8.4.4 valid=300s;
location / {
proxy_pass http://localhost: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;
}
}Upstream Blocks and Load Balancing
The upstream block defines a pool of backend servers. Nginx supports round-robin, least-connections, and IP-hash load balancing algorithms.
# Load balancing across multiple backends
upstream app_servers {
# Default: round-robin
server 10.0.0.1:3000;
server 10.0.0.2:3000;
server 10.0.0.3:3000;
# Least connections algorithm
# least_conn;
# IP hash (sticky sessions)
# ip_hash;
# Weighted distribution
# server 10.0.0.1:3000 weight=3;
# server 10.0.0.2:3000 weight=1;
# Health checks (Nginx Plus only; use passive for open-source)
server 10.0.0.3:3000 max_fails=3 fail_timeout=30s;
# Keepalive connections to backends
keepalive 32;
}
server {
listen 443 ssl http2;
server_name example.com;
# ... SSL config omitted for brevity ...
location / {
proxy_pass http://app_servers;
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 Connection "";
proxy_http_version 1.1;
proxy_connect_timeout 5s;
proxy_read_timeout 60s;
}
}WebSocket Proxying
WebSocket connections require special handling because they upgrade from HTTP to a persistent TCP connection. The Upgrade and Connection headers must be forwarded.
# WebSocket proxy configuration
server {
listen 443 ssl http2;
server_name ws.example.com;
# ... SSL config ...
location /ws/ {
proxy_pass http://localhost:8080;
# WebSocket upgrade headers
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;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# Long timeout for persistent connections
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
}
# Regular HTTP traffic on the same server
location / {
proxy_pass http://localhost:3000;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}Proxy Caching
Nginx can cache backend responses to reduce load and improve latency for cacheable content.
# Caching with proxy_cache
proxy_cache_path /var/cache/nginx
levels=1:2
keys_zone=app_cache:10m
max_size=1g
inactive=60m
use_temp_path=off;
server {
listen 443 ssl http2;
server_name example.com;
# ... SSL config ...
location / {
proxy_pass http://localhost:3000;
proxy_set_header Host $host;
# Enable cache
proxy_cache app_cache;
proxy_cache_key "$scheme$request_method$host$request_uri";
proxy_cache_valid 200 302 10m;
proxy_cache_valid 404 1m;
# Return stale content while refreshing
proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
# Add cache status header for debugging
add_header X-Cache-Status $upstream_cache_status;
# Don't cache if Cookie or Authorization header present
proxy_cache_bypass $http_cookie $http_authorization;
proxy_no_cache $http_cookie $http_authorization;
}
# Bypass cache for API routes
location /api/ {
proxy_pass http://localhost:3000;
proxy_cache_bypass 1;
proxy_no_cache 1;
}
}Quick Reference: Key Directives
| Directive | Description |
|---|---|
| proxy_pass | Upstream backend URL |
| proxy_set_header | Modify/add request headers to backend |
| proxy_connect_timeout | Time to establish connection to backend |
| proxy_read_timeout | Time to read response from backend |
| proxy_send_timeout | Time to send request to backend |
| proxy_buffering | Buffer backend responses (on/off) |
| proxy_cache | Enable caching using a named cache zone |
| proxy_cache_valid | Cache duration per status code |
| proxy_http_version | HTTP version for backend (set 1.1 for keepalive) |
| upstream | Define a pool of backend servers |
| keepalive | Max idle keepalive connections to backends |
Production Best Practices
- Always test configuration before reloading: nginx -t. Never reload without testing in production.
- Use proxy_set_header X-Forwarded-Proto $scheme so your backend knows whether the original request was HTTP or HTTPS.
- Set appropriate timeouts. The default proxy_read_timeout is 60s — increase for long-running requests (file uploads, reports).
- Enable keepalive connections to backends using proxy_http_version 1.1 and keepalive in the upstream block for better performance.
- Monitor /var/log/nginx/error.log and access.log. Set up log rotation with logrotate.
Frequently Asked Questions
How do I pass the real client IP to my backend?
Add these headers: proxy_set_header X-Real-IP $remote_addr and proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for. Then in your backend, read X-Real-IP (for the direct client) or the first entry in X-Forwarded-For. If using multiple proxies, configure trusted proxy IPs.
How do I handle multiple backends with different paths?
Use multiple location blocks: location /api/ { proxy_pass http://api-server:8080; } and location /app/ { proxy_pass http://app-server:3000/; }. The proxy_pass URI matters: a target with no URI keeps the matched prefix, while a target with a URI replaces the matched location prefix.
What is the difference between proxy_pass with and without a trailing slash?
Without a URI, proxy_pass http://backend forwards the full request URI including the location path. With a URI, proxy_pass http://backend/ replaces the matched location prefix with /. Example: location /app/ with proxy_pass http://backend/ forwards /app/page as /page.
How do I debug proxy connection issues?
Check /var/log/nginx/error.log first. Common issues: (1) upstream connection refused — backend not running or wrong port; (2) upstream timed out — increase proxy_read_timeout; (3) 502 Bad Gateway — backend crashed or returned invalid response. Add "error_log /var/log/nginx/error.log debug;" temporarily for verbose logging.
How do I configure Nginx to use HTTP/2 for backends?
Nginx's proxy module only supports HTTP/1.1 for upstream connections (as of 2026). HTTP/2 is only for client-to-Nginx connections (listen 443 ssl http2). To get HTTP/2 end-to-end, use grpc_pass instead of proxy_pass for gRPC backends, or use Nginx Plus which has HTTP/2 upstream support.