Nginx is the most widely deployed web server and reverse proxy, powering over 30% of all websites. Its event-driven architecture handles thousands of concurrent connections with minimal memory. This guide covers server blocks, reverse proxy setup, SSL/TLS termination, load balancing, caching, and gzip compression — the building blocks of production Nginx configurations. For a side-by-side with the alternative, see Nginx vs Apache, or jump to the Nginx cheat sheet for one-line snippets.
Tuning baseline: the defaults Nginx ships with are conservative. Set worker_processes auto; (one per CPU core), worker_connections 1024; in the events block, and use epoll; on Linux. Total max connections ≈ worker_processes × worker_connections.
How Do Server Blocks Work?
Server blocks (virtual hosts) let Nginx serve multiple domains from a single instance. Nginx matches incoming requests by server_name and listen directives. The first server block acts as the default if no match is found.
# /etc/nginx/sites-available/example.com
server {
listen 80;
listen [::]:80;
server_name example.com www.example.com;
root /var/www/example.com/html;
index index.html;
# Logging
access_log /var/log/nginx/example.com.access.log;
error_log /var/log/nginx/example.com.error.log;
location / {
try_files $uri $uri/ =404;
}
# Static assets with cache headers
location ~* \.(css|js|png|jpg|jpeg|gif|ico|svg|woff2)$ {
expires 30d;
add_header Cache-Control "public, immutable";
}
# Deny access to hidden files
location ~ /\. {
deny all;
return 404;
}
}# Symlink to sites-enabled
sudo ln -s /etc/nginx/sites-available/example.com /etc/nginx/sites-enabled/
# Test configuration syntax
sudo nginx -t
# Reload without downtime
sudo nginx -s reloadHow Do You Configure a Reverse Proxy?
A reverse proxy forwards client requests to backend application servers (Node.js, Python, Go, etc.) and returns the response. Nginx handles TLS termination, static files, and connection management while the backend focuses on application logic.
server {
listen 80;
server_name api.example.com;
location / {
proxy_pass http://127.0.0.1:3000;
# Forward client information 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;
# WebSocket support
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
# Timeouts
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
# Buffering (disable for streaming/SSE)
proxy_buffering off;
}
}Tip: Always set proxy_set_header Host $host — without it, the backend receives the upstream address instead of the original hostname, which breaks virtual hosting and cookie domains.
How Do You Set Up SSL/TLS?
SSL/TLS termination at Nginx encrypts traffic between clients and the server. Use Let's Encrypt with Certbot for free, automated certificates. Always redirect HTTP to HTTPS. For background on the handshake, ciphers, and the TLS 1.3 vs 1.2 differences, see HTTPS & TLS explained.
# Redirect HTTP → HTTPS
server {
listen 80;
listen [::]:80;
server_name example.com www.example.com;
return 301 https://$host$request_uri;
}
# HTTPS server
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name example.com www.example.com;
# Certificate files (managed by Certbot)
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
# Modern TLS settings
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;
# HSTS — tell browsers to always use HTTPS
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
# OCSP stapling — faster TLS handshakes
ssl_stapling on;
ssl_stapling_verify on;
ssl_trusted_certificate /etc/letsencrypt/live/example.com/chain.pem;
# Session cache for TLS resumption
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
ssl_session_tickets off;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
}
}# Install Certbot
sudo apt install certbot python3-certbot-nginx
# Obtain and auto-configure SSL
sudo certbot --nginx -d example.com -d www.example.com
# Auto-renewal is set up via systemd timer
sudo systemctl status certbot.timerHow do you enable HTTP/3 (QUIC)?
Nginx has shipped HTTP/3 / QUIC support since 1.25.0 (May 2023), built against OpenSSL 3.5+, BoringSSL, LibreSSL, or QuicTLS. QUIC runs over UDP/443, so add a listen 443 quic directive alongside the existing TLS listener and advertise the upgrade with an Alt-Svc header so browsers know to switch protocols on the next request.
server {
# Existing TLS 1.3 over TCP
listen 443 ssl;
listen [::]:443 ssl;
# QUIC / HTTP/3 over UDP
listen 443 quic reuseport;
listen [::]:443 quic reuseport;
http2 on;
http3 on;
server_name example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
ssl_protocols TLSv1.3; # QUIC requires TLS 1.3
# Tell clients HTTP/3 is available on the same port
add_header Alt-Svc 'h3=":443"; ma=86400' always;
}Open UDP/443 in the firewall — most outages here are missing UDP rules, not Nginx config. Verify with curl --http3 -I https://example.com (curl 7.88+ with --with-openssl-quic) or nginx -V to confirm --with-http_v3_module is compiled in.
How Do You Configure Load Balancing?
Nginx distributes traffic across multiple backend servers using an upstream block. It supports round-robin (default), least connections, IP hash, and weighted distribution.
# Round-robin (default) — equal distribution
upstream api_servers {
server 10.0.1.10:3000;
server 10.0.1.11:3000;
server 10.0.1.12:3000;
}
# Least connections — sends to the server with fewest active connections
upstream api_least {
least_conn;
server 10.0.1.10:3000;
server 10.0.1.11:3000;
}
# IP hash — same client always hits the same server (sticky sessions)
upstream api_sticky {
ip_hash;
server 10.0.1.10:3000;
server 10.0.1.11:3000;
}
# Weighted — send more traffic to powerful servers
upstream api_weighted {
server 10.0.1.10:3000 weight=5; # gets 5x traffic
server 10.0.1.11:3000 weight=1;
server 10.0.1.12:3000 backup; # only when others are down
}
# Health checks and failure detection
upstream api_resilient {
server 10.0.1.10:3000 max_fails=3 fail_timeout=30s;
server 10.0.1.11:3000 max_fails=3 fail_timeout=30s;
}
server {
listen 80;
location / {
proxy_pass http://api_servers;
}
}How do you get the real client IP behind a CDN or load balancer?
Behind Cloudflare, CloudFront, an ALB, or any reverse-proxying CDN, $remote_addr is the edge IP, not the user. Logs, rate limits, and geolocation all break. Fix it with the ngx_http_realip_module: list the trusted upstream ranges and pick the header to read.
# Trust Cloudflare's published IP ranges (https://www.cloudflare.com/ips/)
set_real_ip_from 173.245.48.0/20;
set_real_ip_from 103.21.244.0/22;
set_real_ip_from 103.22.200.0/22;
set_real_ip_from 103.31.4.0/22;
set_real_ip_from 141.101.64.0/18;
set_real_ip_from 108.162.192.0/18;
set_real_ip_from 190.93.240.0/20;
set_real_ip_from 188.114.96.0/20;
set_real_ip_from 197.234.240.0/22;
set_real_ip_from 198.41.128.0/17;
set_real_ip_from 162.158.0.0/15;
set_real_ip_from 104.16.0.0/13;
set_real_ip_from 104.24.0.0/14;
set_real_ip_from 172.64.0.0/13;
set_real_ip_from 131.0.72.0/22;
set_real_ip_from 2400:cb00::/32;
set_real_ip_from 2606:4700::/32;
set_real_ip_from 2803:f800::/32;
set_real_ip_from 2405:b500::/32;
set_real_ip_from 2405:8100::/32;
set_real_ip_from 2a06:98c0::/29;
set_real_ip_from 2c0f:f248::/32;
# Pick the header. Cloudflare → CF-Connecting-IP. AWS ALB → X-Forwarded-For.
real_ip_header CF-Connecting-IP;
real_ip_recursive on;Without this, every request looks like it came from a handful of CDN IPs — your rate limiter will throttle Cloudflare, not the abuser. Refresh the CIDR list when the CDN updates its ranges (Cloudflare publishes a JSON feed at /ips-v4 and /ips-v6).
How Do You Enable Caching and Gzip?
Nginx can cache proxy responses to reduce backend load and compress responses to reduce bandwidth. Both significantly improve response times.
# Define cache zone in http block (nginx.conf)
proxy_cache_path /var/cache/nginx levels=1:2
keys_zone=app_cache:10m
max_size=1g
inactive=60m
use_temp_path=off;
server {
location /api/ {
proxy_pass http://api_servers;
proxy_cache app_cache;
# Cache successful responses for 10 minutes
proxy_cache_valid 200 10m;
proxy_cache_valid 404 1m;
# Cache key
proxy_cache_key "$scheme$request_method$host$request_uri";
# Add header to show cache status (HIT/MISS/BYPASS)
add_header X-Cache-Status $upstream_cache_status;
# Bypass cache for authenticated requests
proxy_cache_bypass $http_authorization;
proxy_no_cache $http_authorization;
}
}# Enable in http block (nginx.conf)
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 5; # 1-9, 5 is a good balance
gzip_min_length 256; # Don't compress tiny responses
gzip_types
text/plain
text/css
text/javascript
application/javascript
application/json
application/xml
image/svg+xml
application/wasm;
# Serve pre-compressed files if available (.gz)
gzip_static on;What are common Nginx configuration patterns?
Frequently used configuration snippets for real-world deployments — covering rate limiting, SPA fallback, error pages, and security headers (which pair with CORS configuration for cross-origin APIs).
# Rate limiting — 10 requests/second per IP
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s;
server {
location /api/ {
limit_req zone=api_limit burst=20 nodelay;
proxy_pass http://api_servers;
}
}
# Single-page application (SPA) — fallback to index.html
location / {
root /var/www/app;
try_files $uri $uri/ /index.html;
}
# Custom error pages
error_page 404 /404.html;
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /var/www/errors;
internal;
}
# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
# File upload size limit
client_max_body_size 50m;References
- Nginx Documentation — official open-source Nginx docs covering every directive and module
- ngx_http_core_module Reference — exhaustive list of core HTTP directives and embedded variables like
$remote_addr - Mozilla SSL Configuration Generator — copy-pasteable TLS blocks for Modern, Intermediate, and Old client compatibility profiles
- Let's Encrypt Documentation — free automated certificate authority used by Certbot for the SSL setup above
- Nginx QUIC and HTTP/3 — official build flags, listener config, and troubleshooting for HTTP/3 (Nginx 1.25+)
- h5bp/server-configs-nginx — battle-tested boilerplate config snippets (security, performance, MIME types) maintained by the HTML5 Boilerplate team
Key Takeaways
- • Server blocks route requests by
server_name— always test withnginx -tbefore reloading - • Reverse proxy requires
proxy_set_header Host $hostfor correct backend behavior - • Use TLS 1.2+ with Let's Encrypt and always redirect HTTP to HTTPS
- • Load balance with upstream blocks — use
least_connfor uneven request durations - • Enable gzip at compression level 5 for text-based content types
- • Use
proxy_cacheto reduce backend load on cacheable endpoints - • Rate limiting with
limit_req_zoneprotects against abuse
Bookmark the Nginx Cheat Sheet for one-line directives, variables, and reload commands you can copy in seconds.