Nginx Configuration Guide for Web Developers 2026: Reverse Proxy, SSL & Performance
Master Nginx configuration in 2026. Complete guide covering reverse proxy setup, SSL/TLS with Let's Encrypt, gzip compression, caching, rate limiting, load balancing, and common gotchas.
Nginx powers roughly 34% of all web servers on the internet. Itβs the workhorse behind reverse proxies, load balancers, static file servers, and API gateways for applications of every scale. Despite its ubiquity, its configuration syntax trips up developers constantly β a misplaced semicolon or a wrong location block order can silently break an entire site.
This guide walks through every configuration pattern youβll actually need as a web developer in 2026, with complete, production-tested config blocks for each.
Nginx Block Structure
Before diving into specific features, understanding Nginxβs block hierarchy is essential:
# /etc/nginx/nginx.conf β top-level structure
# Global context (worker processes, user, pid)
worker_processes auto;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
events {
# Connection handling
worker_connections 1024;
use epoll; # Linux only β fastest event model
multi_accept on;
}
http {
# HTTP-level settings (MIME types, logging, gzip, etc.)
include mime.types;
default_type application/octet-stream;
# Include all virtual hosts
include /etc/nginx/conf.d/*.conf;
include /etc/nginx/sites-enabled/*;
}
Each site lives in a separate server {} block:
server {
listen 80;
server_name example.com www.example.com;
root /var/www/example.com;
location / {
# Request handling
}
location /api/ {
# API-specific handling
}
}
Block precedence rules (critical to understand):
server {}β matches by IP/port/hostnamelocation {}β matches by URL path (exact=> prefix^~> regex~> prefix/)
Reverse Proxy Setup
The most common Nginx use case is proxying requests to an upstream application server (Node.js, Python, Go, etc.):
# /etc/nginx/conf.d/myapp.conf
upstream app_backend {
server 127.0.0.1:3000;
keepalive 32; # Persistent connections to upstream
}
server {
listen 80;
server_name app.example.com;
# Redirect HTTP to HTTPS
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
server_name app.example.com;
# SSL config (see next section)
ssl_certificate /etc/letsencrypt/live/app.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/app.example.com/privkey.pem;
# Proxy to upstream
location / {
proxy_pass http://app_backend;
# Essential proxy headers
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade'; # WebSocket support
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
proxy_connect_timeout 30s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
# Buffer settings
proxy_buffering on;
proxy_buffer_size 4k;
proxy_buffers 8 4k;
}
}
Common gotcha: Without proxy_set_header Host $host, your app receives localhost as the host instead of the real domain name, which breaks URL generation in frameworks like Rails, Django, and Next.js.
SSL/TLS with Letβs Encrypt
Letβs Encrypt provides free, auto-renewing SSL certificates. Hereβs the complete setup with security best practices:
Step 1 β Install Certbot and obtain a certificate:
# Ubuntu/Debian
apt install certbot python3-certbot-nginx
# Obtain certificate
certbot --nginx -d example.com -d www.example.com
# Test auto-renewal
certbot renew --dry-run
Step 2 β Nginx SSL configuration with strong security settings:
server {
listen 443 ssl http2;
server_name example.com www.example.com;
# Certificate paths (managed by Certbot)
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
# Modern TLS β disable old, vulnerable protocols
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers off;
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;
# Session resumption (performance)
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
ssl_session_tickets off;
# OCSP Stapling β faster certificate validation
ssl_stapling on;
ssl_stapling_verify on;
ssl_trusted_certificate /etc/letsencrypt/live/example.com/chain.pem;
resolver 8.8.8.8 8.8.4.4 valid=300s;
# Security headers
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
add_header X-Content-Type-Options nosniff;
add_header X-Frame-Options DENY;
add_header X-XSS-Protection "1; mode=block";
add_header Referrer-Policy "strict-origin-when-cross-origin";
}
HSTS note: The Strict-Transport-Security header tells browsers to always use HTTPS. The preload directive enables HSTS preloading β only add it when youβre 100% committed to HTTPS on your domain and all subdomains.
Gzip Compression
Gzip reduces transfer sizes by 60β80% for text-based assets. Add these settings to the http {} block in nginx.conf:
http {
# Enable gzip
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6; # 1-9, sweet spot is 5-6
gzip_min_length 1024; # Don't compress tiny files
gzip_buffers 16 8k;
gzip_http_version 1.1;
# Types to compress
gzip_types
text/plain
text/css
text/xml
text/javascript
application/json
application/javascript
application/xml
application/xml+rss
application/atom+xml
application/ld+json
image/svg+xml
font/truetype
font/opentype
application/vnd.ms-fontobject;
}
What not to compress: Images (JPEG, PNG, WebP), videos, and audio files are already compressed β gzipping them wastes CPU with negligible size benefit. The gzip_types list intentionally excludes these.
Caching Headers
Static assets should be aggressively cached. Dynamic API responses should not be cached by default:
server {
# Static assets β cache for 1 year (cache-busting via filename hashing)
location ~* \.(js|css|woff2|woff|ttf|eot)$ {
expires 1y;
add_header Cache-Control "public, immutable";
access_log off;
}
# Images β cache for 30 days
location ~* \.(jpg|jpeg|png|gif|ico|svg|webp|avif)$ {
expires 30d;
add_header Cache-Control "public, no-transform";
access_log off;
}
# HTML β short cache or no cache (let browser revalidate)
location ~* \.html$ {
expires -1;
add_header Cache-Control "no-cache, must-revalidate";
}
# API routes β no caching
location /api/ {
proxy_pass http://app_backend;
add_header Cache-Control "no-store";
}
}
Nginx proxy cache (server-side caching):
http {
# Define cache zone (1GB, metadata stored in /tmp/nginx_cache)
proxy_cache_path /tmp/nginx_cache
levels=1:2
keys_zone=CACHE:10m
max_size=1g
inactive=60m
use_temp_path=off;
}
server {
location / {
proxy_pass http://app_backend;
proxy_cache CACHE;
proxy_cache_valid 200 10m;
proxy_cache_valid 404 1m;
proxy_cache_use_stale error timeout updating;
proxy_cache_lock on;
# Show cache hit/miss in response headers (useful for debugging)
add_header X-Cache-Status $upstream_cache_status;
}
}
Rate Limiting
Rate limiting protects your application from abuse, scrapers, and DDoS:
http {
# Define rate limit zones
# $binary_remote_addr β per IP address
# 10m β 10MB memory for the zone (stores ~160k IPs)
# rate=10r/s β 10 requests per second
limit_req_zone $binary_remote_addr zone=general:10m rate=10r/s;
limit_req_zone $binary_remote_addr zone=api:10m rate=30r/s;
limit_req_zone $binary_remote_addr zone=login:10m rate=5r/m;
limit_req_status 429; # Return 429 Too Many Requests (not 503)
limit_conn_status 429;
}
server {
# Apply general rate limiting to all routes
limit_req zone=general burst=20 nodelay;
# Stricter limit on API
location /api/ {
limit_req zone=api burst=50 nodelay;
proxy_pass http://app_backend;
}
# Very strict on auth endpoints (prevent brute force)
location /api/auth/login {
limit_req zone=login burst=3;
proxy_pass http://app_backend;
}
}
burst explained: If the rate is 10r/s with burst=20, Nginx queues up to 20 excess requests and serves them. nodelay processes the burst immediately without artificial delay β useful for APIs, bad for login endpoints.
Load Balancing
When you have multiple application instances, Nginx distributes traffic across them:
upstream app_cluster {
# Default: round-robin
server 10.0.0.1:3000;
server 10.0.0.2:3000;
server 10.0.0.3:3000;
# Weighted round-robin (server 1 gets 3x traffic)
# server 10.0.0.1:3000 weight=3;
# server 10.0.0.2:3000 weight=1;
# Least connections β best for mixed workloads
# least_conn;
# IP hash β sticky sessions (same client always β same server)
# ip_hash;
# Health check (Nginx Plus only; OSS uses passive checks)
# check interval=3000 rise=2 fall=5 timeout=1000;
keepalive 32;
}
server {
location / {
proxy_pass http://app_cluster;
proxy_next_upstream error timeout http_500 http_502 http_503;
proxy_next_upstream_tries 3;
}
}
When to use which algorithm:
- Round-robin (default): Equal servers, stateless apps
- Least connections: Mixed request durations (API + file uploads)
- IP hash: Apps with server-side sessions (legacy apps)
- Weighted: Servers with different capacities
Common Gotchas
1. location block order matters
server {
# WRONG β this catches /api/health too, so it never reaches the specific rule below
location /api/ {
proxy_pass http://app_backend;
}
location = /api/health {
return 200 "OK";
}
}
server {
# CORRECT β exact match takes priority over prefix match
location = /api/health {
return 200 "OK";
}
location /api/ {
proxy_pass http://app_backend;
}
}
2. Trailing slash in proxy_pass
# These behave differently:
# Without trailing slash β passes /api/users as-is to upstream
proxy_pass http://backend;
# With trailing slash β strips /api prefix, passes /users to upstream
location /api/ {
proxy_pass http://backend/;
}
3. 413 Request Entity Too Large
Default max body size is 1MB. Increase it for file upload endpoints:
# In http {}, server {}, or location {} block
client_max_body_size 50M;
4. WebSocket connections drop
Without the Upgrade and Connection headers, WebSocket connections fail silently:
location /ws/ {
proxy_pass http://app_backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_read_timeout 86400; # Keep WS alive for up to 24h
}
5. Test config before reloading
Never reload Nginx without testing the config first:
nginx -t # Test configuration
nginx -t && nginx -s reload # Safe reload (atomic, zero downtime)
Complete Production Config Template
Hereβs a battle-tested template combining all the above patterns:
# /etc/nginx/conf.d/production.conf
upstream app {
server 127.0.0.1:3000;
keepalive 32;
}
limit_req_zone $binary_remote_addr zone=ratelimit:10m rate=20r/s;
server {
listen 80;
server_name example.com www.example.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
server_name example.com www.example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
add_header Strict-Transport-Security "max-age=63072000" always;
add_header X-Content-Type-Options nosniff;
gzip on;
gzip_types text/css application/javascript application/json;
client_max_body_size 25M;
location ~* \.(js|css|woff2|png|jpg|webp|svg)$ {
root /var/www/example.com;
expires 1y;
add_header Cache-Control "public, immutable";
access_log off;
}
location / {
limit_req zone=ratelimit burst=30 nodelay;
proxy_pass http://app;
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;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 60s;
}
}
Generate and Validate Configs Automatically
Writing Nginx config by hand is error-prone. Use DevPlaybookβs tooling to speed up the process:
- Nginx Config Generator β generate production-ready Nginx configs from a form interface. Select your use case (reverse proxy, static site, load balancer), fill in your domains and upstream ports, and get a complete config file ready to deploy.
The generator handles the common gotchas automatically and outputs configs that pass nginx -t on first try.
Summary
| Pattern | Key Setting | Common Mistake |
|---|---|---|
| Reverse proxy | proxy_set_header Host $host | Missing β breaks URL generation |
| SSL/TLS | ssl_protocols TLSv1.2 TLSv1.3 | Leaving TLSv1.0/1.1 enabled |
| Gzip | gzip_types explicit list | Compressing images (waste of CPU) |
| Caching | immutable for hashed assets | Short-caching immutable files |
| Rate limiting | burst + nodelay | No burst allowance β 429 on normal traffic |
| Load balancing | least_conn for mixed loads | Default round-robin for long requests |
| WebSockets | Upgrade + Connection headers | WS connections drop immediately |
Nginx configuration is mostly about understanding these patterns, their interaction order, and the subtle flag differences that change behavior completely. Once these building blocks are solid, you can confidently configure any web infrastructure scenario.
50 CLI Commands That Save Hours Every Week
The exact Git shortcuts, file search tricks, Docker commands, and productivity hacks that senior developers use daily. Compiled from 5+ years of shipping production code.
- Git mastery (10 cmds)
- File & search wizardry
- Docker & DevOps tricks
- Process & network tools
- Productivity boosters
- macOS & Linux install guide
No spam. 1 email/week max. Unsubscribe anytime.
Found this guide useful? Check out our free developer tools.
Affiliate disclosure: Some links below are affiliate links β we may earn a small commission at no extra cost to you. Learn more.
Recommended Tools & Resources
DigitalOcean
$200 credit for new users. Simple, affordable cloud hosting for developers.
GitHub Student Pack
Free access to 100+ developer tools. Perfect for students and new devs.
Vercel
Deploy frontend apps instantly. Free tier is generous for side projects.
DevPlaybook Products
Boilerplates, scripts & AI toolkits to 10x your dev workflow.