Nginx Configuration Patterns for Web Applications

Nginx powers a huge portion of the web. Understanding its configuration patterns is essential for deploying modern applications. Basic Structure 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 # /etc/nginx/nginx.conf user www-data; worker_processes auto; pid /run/nginx.pid; events { worker_connections 1024; } http { include /etc/nginx/mime.types; default_type application/octet-stream; # Logging access_log /var/log/nginx/access.log; error_log /var/log/nginx/error.log; # Performance sendfile on; keepalive_timeout 65; # Include site configs include /etc/nginx/conf.d/*.conf; include /etc/nginx/sites-enabled/*; } Static File Server 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 server { listen 80; server_name example.com; root /var/www/html; index index.html; location / { try_files $uri $uri/ =404; } # Cache static assets location ~* \.(css|js|png|jpg|jpeg|gif|ico|svg|woff2?)$ { expires 30d; add_header Cache-Control "public, immutable"; } } Reverse Proxy Basic Proxy 1 2 3 4 5 6 7 8 9 10 11 12 13 server { listen 80; server_name api.example.com; location / { proxy_pass http://localhost:3000; 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; } } WebSocket Support 1 2 3 4 5 6 7 8 location /ws { proxy_pass http://localhost:3000; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_set_header Host $host; proxy_read_timeout 86400; } Upstream (Multiple Backends) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 upstream backend { server 127.0.0.1:3001; server 127.0.0.1:3002; server 127.0.0.1:3003; keepalive 32; } server { location / { proxy_pass http://backend; proxy_http_version 1.1; proxy_set_header Connection ""; } } Load Balancing 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 upstream backend { # Round-robin (default) server app1:3000; server app2:3000; server app3:3000; } upstream backend_weighted { server app1:3000 weight=3; server app2:3000 weight=2; server app3:3000 weight=1; } upstream backend_ip_hash { ip_hash; # Sticky sessions by IP server app1:3000; server app2:3000; } upstream backend_least_conn { least_conn; # Send to least busy server app1:3000; server app2:3000; } Health Checks 1 2 3 4 5 upstream backend { server app1:3000 max_fails=3 fail_timeout=30s; server app2:3000 max_fails=3 fail_timeout=30s; server app3:3000 backup; # Only used when others fail } SSL/TLS Configuration 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 server { listen 443 ssl http2; server_name example.com; ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem; # Modern SSL settings ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256; ssl_prefer_server_ciphers off; # HSTS add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; # OCSP Stapling ssl_stapling on; ssl_stapling_verify on; resolver 8.8.8.8 8.8.4.4 valid=300s; } # Redirect HTTP to HTTPS server { listen 80; server_name example.com; return 301 https://$server_name$request_uri; } Security Headers 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 server { # Prevent clickjacking add_header X-Frame-Options "SAMEORIGIN" always; # Prevent MIME sniffing add_header X-Content-Type-Options "nosniff" always; # XSS protection add_header X-XSS-Protection "1; mode=block" always; # Referrer policy add_header Referrer-Policy "strict-origin-when-cross-origin" always; # Content Security Policy add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline';" always; } Rate Limiting 1 2 3 4 5 6 7 8 9 # Define rate limit zone limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s; server { location /api/ { limit_req zone=api burst=20 nodelay; proxy_pass http://backend; } } Connection Limiting 1 2 3 4 5 6 7 8 limit_conn_zone $binary_remote_addr zone=addr:10m; server { location /downloads/ { limit_conn addr 5; # 5 connections per IP limit_rate 100k; # 100KB/s per connection } } Caching Proxy Cache 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=cache:10m max_size=1g inactive=60m; server { location / { proxy_cache cache; proxy_cache_valid 200 1h; proxy_cache_valid 404 1m; proxy_cache_use_stale error timeout updating; proxy_cache_background_update on; add_header X-Cache-Status $upstream_cache_status; proxy_pass http://backend; } } Cache Bypass 1 2 3 4 5 location / { proxy_cache cache; proxy_cache_bypass $http_cache_control; proxy_no_cache $arg_nocache; } Gzip Compression 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 http { gzip on; gzip_vary on; gzip_min_length 1024; gzip_proxied any; gzip_comp_level 6; gzip_types text/plain text/css text/xml text/javascript application/json application/javascript application/xml application/rss+xml image/svg+xml; } Location Matching 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 server { # Exact match location = /favicon.ico { log_not_found off; access_log off; } # Prefix match (case-sensitive) location /api/ { proxy_pass http://backend; } # Regex match (case-sensitive) location ~ \.php$ { fastcgi_pass unix:/var/run/php/php-fpm.sock; } # Regex match (case-insensitive) location ~* \.(jpg|jpeg|png|gif)$ { expires 30d; } # Prefix match (highest priority after exact) location ^~ /static/ { root /var/www; } } Priority: = > ^~ > ~ / ~* > prefix ...

February 28, 2026 Â· 6 min Â· 1082 words Â· Rob Washington

Nginx Configuration Patterns for Modern Web Apps

Nginx is everywhere—reverse proxy, load balancer, static file server, SSL terminator. Here are the configuration patterns that work in production. Base Configuration 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 # /etc/nginx/nginx.conf user nginx; worker_processes auto; error_log /var/log/nginx/error.log warn; pid /run/nginx.pid; events { worker_connections 1024; use epoll; multi_accept on; } http { include /etc/nginx/mime.types; default_type application/octet-stream; # Logging log_format main '$remote_addr - $remote_user [$time_local] "$request" ' '$status $body_bytes_sent "$http_referer" ' '"$http_user_agent" "$http_x_forwarded_for" ' 'rt=$request_time uct=$upstream_connect_time ' 'uht=$upstream_header_time urt=$upstream_response_time'; access_log /var/log/nginx/access.log main; # Performance sendfile on; tcp_nopush on; tcp_nodelay on; keepalive_timeout 65; types_hash_max_size 2048; # Compression gzip on; gzip_vary on; gzip_proxied any; gzip_comp_level 6; gzip_types text/plain text/css text/xml application/json application/javascript application/xml+rss application/atom+xml image/svg+xml; # Security server_tokens off; include /etc/nginx/conf.d/*.conf; } Reverse Proxy Basic Proxy 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 # /etc/nginx/conf.d/app.conf upstream backend { server 127.0.0.1:3000; keepalive 32; } server { listen 80; server_name example.com; location / { proxy_pass http://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; proxy_set_header Connection ""; proxy_connect_timeout 60s; proxy_send_timeout 60s; proxy_read_timeout 60s; } } Load Balancing 1 2 3 4 5 6 7 8 9 upstream backend { least_conn; # or: round_robin, ip_hash, hash $request_uri server backend1.local:3000 weight=3; server backend2.local:3000 weight=2; server backend3.local:3000 backup; keepalive 32; } Health Checks (Commercial) 1 2 3 4 5 6 7 # nginx plus only upstream backend { server backend1:3000; server backend2:3000; health_check interval=5s fails=3 passes=2; } SSL/TLS Configuration Modern SSL Setup 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 server { listen 443 ssl http2; server_name example.com; ssl_certificate /etc/nginx/ssl/fullchain.pem; ssl_certificate_key /etc/nginx/ssl/privkey.pem; # Modern configuration 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; ssl_prefer_server_ciphers off; # OCSP Stapling ssl_stapling on; ssl_stapling_verify on; ssl_trusted_certificate /etc/nginx/ssl/chain.pem; resolver 8.8.8.8 8.8.4.4 valid=300s; resolver_timeout 5s; # Session caching ssl_session_timeout 1d; ssl_session_cache shared:SSL:50m; ssl_session_tickets off; # ... rest of config } # Redirect HTTP to HTTPS server { listen 80; server_name example.com; return 301 https://$server_name$request_uri; } Let’s Encrypt with Certbot 1 2 3 4 5 6 7 8 9 10 11 12 server { listen 80; server_name example.com; location /.well-known/acme-challenge/ { root /var/www/certbot; } location / { return 301 https://$server_name$request_uri; } } Security Headers 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 server { # ... SSL config ... # Security headers 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; add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline';" always; # HSTS (be careful - hard to undo) add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; # Hide server version server_tokens off; } Rate Limiting 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 # Define rate limit zones limit_req_zone $binary_remote_addr zone=general:10m rate=10r/s; limit_req_zone $binary_remote_addr zone=login:10m rate=1r/s; limit_conn_zone $binary_remote_addr zone=addr:10m; server { # General rate limit limit_req zone=general burst=20 nodelay; limit_conn addr 10; location /api/login { limit_req zone=login burst=5 nodelay; proxy_pass http://backend; } location /api/ { limit_req zone=general burst=50 nodelay; proxy_pass http://backend; } } Rate Limit Response 1 2 3 4 5 6 7 8 9 limit_req_status 429; limit_conn_status 429; error_page 429 /429.json; location = /429.json { internal; default_type application/json; return 429 '{"error": "Too many requests"}'; } Caching Proxy Cache 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 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_cache app_cache; proxy_cache_valid 200 10m; proxy_cache_valid 404 1m; proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504; proxy_cache_lock on; add_header X-Cache-Status $upstream_cache_status; proxy_pass http://backend; } } Static File Caching 1 2 3 4 5 6 7 8 9 10 11 location /static/ { alias /var/www/static/; expires 1y; add_header Cache-Control "public, immutable"; access_log off; } location ~* \.(jpg|jpeg|png|gif|ico|css|js|woff2)$ { expires 30d; add_header Cache-Control "public"; } WebSocket Support 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 map $http_upgrade $connection_upgrade { default upgrade; '' close; } server { location /ws/ { proxy_pass http://backend; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $connection_upgrade; proxy_set_header Host $host; proxy_read_timeout 86400; } } API Gateway Patterns Path-Based Routing 1 2 3 4 5 6 7 8 9 10 11 12 13 server { location /api/users { proxy_pass http://users-service; } location /api/orders { proxy_pass http://orders-service; } location /api/inventory { proxy_pass http://inventory-service; } } API Versioning 1 2 3 4 5 6 7 8 9 10 11 12 location /api/v1/ { proxy_pass http://api-v1/; } location /api/v2/ { proxy_pass http://api-v2/; } # Default to latest location /api/ { proxy_pass http://api-v2/; } Request/Response Modification 1 2 3 4 5 6 7 8 9 10 11 location /api/ { # Add headers to upstream request proxy_set_header X-Request-ID $request_id; proxy_set_header X-Forwarded-Host $host; # Modify response headers proxy_hide_header X-Powered-By; add_header X-Request-ID $request_id; proxy_pass http://backend; } Static Site Hosting 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 server { listen 80; server_name example.com; root /var/www/html; index index.html; # SPA routing - try file, then directory, then fall back to index location / { try_files $uri $uri/ /index.html; } # Cache static assets location /assets/ { expires 1y; add_header Cache-Control "public, immutable"; } # Deny access to hidden files location ~ /\. { deny all; } # Custom error pages error_page 404 /404.html; error_page 500 502 503 504 /50x.html; } Debugging Debug Logging 1 2 3 4 5 6 7 error_log /var/log/nginx/error.log debug; # Or per-location location /api/ { error_log /var/log/nginx/api-debug.log debug; # ... } Request Inspection 1 2 3 4 location /debug { default_type text/plain; return 200 "Request: $request\nHost: $host\nURI: $uri\nArgs: $args\nRemote: $remote_addr\n"; } Test Configuration 1 2 3 4 nginx -t # Test config syntax nginx -T # Test and dump full config nginx -s reload # Reload configuration nginx -s reopen # Reopen log files Complete Production Example 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 upstream api { least_conn; server api1:3000 weight=2; server api2:3000 weight=2; server api3:3000 backup; keepalive 32; } limit_req_zone $binary_remote_addr zone=api:10m rate=20r/s; proxy_cache_path /var/cache/nginx/api levels=1:2 keys_zone=api_cache:10m max_size=1g; server { listen 80; server_name example.com; return 301 https://$server_name$request_uri; } server { listen 443 ssl http2; server_name example.com; ssl_certificate /etc/nginx/ssl/fullchain.pem; ssl_certificate_key /etc/nginx/ssl/privkey.pem; ssl_protocols TLSv1.2 TLSv1.3; ssl_session_cache shared:SSL:10m; # Security add_header X-Frame-Options "SAMEORIGIN" always; add_header X-Content-Type-Options "nosniff" always; add_header Strict-Transport-Security "max-age=31536000" always; # Static files location /static/ { alias /var/www/static/; expires 1y; add_header Cache-Control "public, immutable"; } # API location /api/ { limit_req zone=api burst=50 nodelay; proxy_cache api_cache; proxy_cache_valid 200 1m; add_header X-Cache-Status $upstream_cache_status; proxy_pass http://api; 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 Connection ""; } # SPA location / { root /var/www/html; try_files $uri $uri/ /index.html; } } Nginx configuration is declarative and predictable once you understand the patterns. Start with a solid base, add features incrementally, and always test with nginx -t before reloading. The patterns above cover 95% of production use cases—adapt them to your needs. ...

February 26, 2026 Â· 7 min Â· 1284 words Â· Rob Washington

Nginx Configuration Patterns: From Basic Proxy to Production-Ready

Nginx is everywhere: reverse proxy, load balancer, static file server, SSL terminator. Its configuration syntax is powerful but has gotchas that catch everyone at least once. These patterns cover common use cases done right. Basic Reverse Proxy 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 upstream backend { server 127.0.0.1:3000; } server { listen 80; server_name example.com; location / { proxy_pass http://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; } } Always set those headers — your backend needs to know the real client IP and protocol. ...

February 23, 2026 Â· 5 min Â· 1043 words Â· Rob Washington

CORS Demystified: Why Your API Calls Get Blocked

You’ve built an API. It works perfectly in Postman. Then you call it from your frontend and get: A h i c a s c s e p s b r s e e e s t n e o n b t f l e o o t c n c k h e t d h a e t b y r ' e h C q t O u t R e p S s s t : p e / d l a i r p c e i y s . : o e u x N r a o c m e p ' . l A e c . c c e o s m s ' - C f o r n o t m r o o l r - i A g l i l n o w ' - h O t r t i p g s i : n / ' m h y e a a p d p e . r c o m ' CORS isn’t your API being broken. It’s your browser protecting users. Understanding why it exists makes fixing it straightforward. ...

February 23, 2026 Â· 7 min Â· 1478 words Â· Rob Washington