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

Writing Systemd Service Files That Actually Work

Systemd services look simple until they don’t start, restart unexpectedly, or fail silently. Here’s how to write service files that work reliably in production. Basic Structure 1 2 3 4 5 6 7 8 9 10 11 12 # /etc/systemd/system/myapp.service [Unit] Description=My Application After=network.target [Service] Type=simple ExecStart=/usr/bin/myapp Restart=always [Install] WantedBy=multi-user.target Three sections, three purposes: [Unit] - What is this, what does it depend on [Service] - How to run it [Install] - When to start it Service Types Type=simple (Default) 1 2 3 [Service] Type=simple ExecStart=/usr/bin/myapp Systemd considers the service started immediately when ExecStart runs. Use when your process stays in foreground. ...

February 26, 2026 Â· 6 min Â· 1098 words Â· Rob Washington

Terraform State Backends: Choosing and Configuring Remote State

Local Terraform state works for learning. Production requires remote state—for team collaboration, state locking, and not losing your infrastructure when your laptop dies. Here’s how to set it up properly. Why Remote State? Local state (terraform.tfstate) has problems: No collaboration - Team members overwrite each other’s changes No locking - Concurrent applies corrupt state No backup - Laptop dies, state is gone, orphaned resources everywhere Secrets in plain text - State contains sensitive data Remote backends solve all of these. ...

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

Docker Compose for Production: Beyond the Tutorial

Docker Compose tutorials show you docker compose up. Production requires health checks, resource limits, proper logging, restart policies, and deployment strategies. Here’s how to bridge that gap. Base Configuration 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 # docker-compose.yml version: "3.8" services: app: image: myapp:${VERSION:-latest} build: context: . dockerfile: Dockerfile restart: unless-stopped environment: - NODE_ENV=production env_file: - .env ports: - "3000:3000" This is a starting point. Let’s make it production-ready. ...

February 26, 2026 Â· 6 min Â· 1214 words Â· Rob Washington

Ansible Playbook Patterns That Scale

Ansible is easy to start and hard to master. A simple playbook works great for 5 servers. The same playbook becomes unmaintainable at 50. Here are the patterns that keep Ansible codebases sane as they grow. Project Structure Start with a structure that scales: a ├ ├ │ │ │ │ │ │ │ │ │ ├ │ │ │ ├ │ │ │ └ n ─ ─ ─ ─ ─ s ─ ─ ─ ─ ─ i b a i ├ │ │ │ │ └ p ├ ├ └ r ├ ├ └ g └ l n n ─ ─ l ─ ─ ─ o ─ ─ ─ r ─ e s v ─ ─ a ─ ─ ─ l ─ ─ ─ o ─ / i e y e u b n p ├ └ s ├ └ b s w d s c n p p a ├ └ l t r ─ ─ t ─ ─ o i e a / o g o _ l ─ ─ e o o ─ ─ a ─ ─ o t b t m i s v l ─ ─ . r d g k e s a m n t a / c y u h g ├ └ i h g └ s . e b o x g r v v f / c o r ─ ─ n o r ─ y r a n r s a a g t s o ─ ─ g s o ─ m v s e / r u i t u / t u l e e s s l o s p a w s p a r s / . t n . _ l e . _ l s . y . y v l b y v l . y m y m a . s m a . y m l m l r y e l r y m l l s m r s m l / l v / l e r s . y m l The key insight: separate inventory per environment. Never mix production and staging in the same inventory file. ...

February 26, 2026 Â· 8 min Â· 1636 words Â· Rob Washington

Container Orchestration Patterns for AI Workloads

Running AI workloads in containers presents unique challenges that traditional web application patterns don’t address. GPU scheduling, model caching, and bursty inference traffic all require thoughtful architecture. Here’s what actually works in production. The GPU Scheduling Problem Standard Kubernetes scheduling assumes CPU and memory are your primary constraints. When you add GPUs to the mix, everything changes. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 apiVersion: v1 kind: Pod metadata: name: inference-server spec: containers: - name: model image: my-registry/llm-server:v1.2 resources: limits: nvidia.com/gpu: 1 memory: "32Gi" requests: nvidia.com/gpu: 1 memory: "24Gi" The naive approach—one GPU per pod—works until you realize GPUs cost $2-4/hour and sit idle between requests. MIG (Multi-Instance GPU) and time-slicing help, but they introduce complexity: ...

February 26, 2026 Â· 4 min Â· 850 words Â· Rob Washington

SSH Tunnels Demystified: Local, Remote, and Dynamic Forwarding

SSH tunnels are one of those tools that seem magical until you understand the three basic patterns. Once you do, you’ll use them constantly. The Three Types Local forwarding (-L): Access a remote service as if it were local Remote forwarding (-R): Expose a local service to a remote network Dynamic forwarding (-D): Create a SOCKS proxy through the SSH connection Let’s break down each one. Local Forwarding: Reach Remote Services Locally Scenario: You need to access a database that’s only available from a server you can SSH into. ...

February 26, 2026 Â· 6 min Â· 1236 words Â· Rob Washington

Distributed Tracing: The Missing Piece of Your Observability Stack

When a request fails in a distributed system, the question isn’t if something went wrong—it’s where. Logs tell you what happened. Metrics tell you how often. But tracing tells you the story. The Problem with Logs and Metrics Alone You’ve got 15 microservices. A user reports slow checkout. You check the logs—thousands of entries. You check the metrics—latency is up, but which service? You’re playing detective without a map. This is where distributed tracing shines. It connects the dots across service boundaries, showing you the exact path a request takes and where time is spent. ...

February 16, 2026 Â· 5 min Â· 930 words Â· Rob Washington