Webhook Security: Protecting Your Endpoints from the Wild West

Webhooks are HTTP endpoints that receive data from external services. Anyone who discovers your webhook URL can send requests to it. That’s a problem. Here’s how to secure them properly. The Threat Model Your webhook endpoint faces several threats: Spoofing: Attacker sends fake payloads pretending to be Stripe/GitHub/etc. Replay attacks: Attacker captures a legitimate request and resends it Tampering: Attacker intercepts and modifies payloads in transit Enumeration: Attacker discovers your webhook URLs through guessing Denial of service: Attacker floods your endpoint with requests Signature Verification Most webhook providers sign their payloads. Always verify: ...

March 1, 2026 Â· 5 min Â· 968 words Â· Rob Washington

Rate Limiting Strategies That Protect Without Frustrating

Rate limiting is the bouncer at your API’s door. Too strict, and legitimate users get frustrated. Too loose, and one bad actor can take down your service. Here’s how to find the balance. Why Rate Limit? Without limits, a single client can: Exhaust your database connections Burn through your third-party API quotas Inflate your cloud bill Deny service to everyone else Rate limiting isn’t about being restrictive—it’s about being fair. ...

March 1, 2026 Â· 5 min Â· 1047 words Â· Rob Washington

SSL/TLS Certificates: From Let's Encrypt to Production

HTTPS is table stakes. Here’s how to set up certificates properly and avoid the 3am “certificate expired” panic. Let’s Encrypt with Certbot Standalone Mode (No Web Server) 1 2 3 4 5 6 7 8 9 # Install sudo apt install certbot # Get certificate (stops any service on port 80) sudo certbot certonly --standalone -d example.com -d www.example.com # Certificates stored in: # /etc/letsencrypt/live/example.com/fullchain.pem # /etc/letsencrypt/live/example.com/privkey.pem Webroot Mode (Server Running) 1 2 # Certbot verifies via http://example.com/.well-known/acme-challenge/ sudo certbot certonly --webroot -w /var/www/html -d example.com Nginx Plugin 1 sudo certbot --nginx -d example.com -d www.example.com Certbot modifies nginx config automatically. ...

February 28, 2026 Â· 4 min Â· 779 words Â· Rob Washington

SOPS: Git-Friendly Secrets Management

The eternal problem: you need secrets in your repo for deployment, but you can’t commit plaintext credentials. SOPS solves this elegantly by encrypting only the values while leaving keys readable. Why SOPS? Traditional approaches: Environment variables: Work, but no version control Vault: Great, but complex for small teams AWS Secrets Manager: Vendor lock-in, API calls at runtime .env files in .gitignore: Hope nobody commits them SOPS encrypts secrets in place. You commit encrypted files. CI/CD decrypts at deploy time. Full audit trail in git. ...

February 28, 2026 Â· 4 min Â· 681 words Â· Rob Washington

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

Environment Files Done Right: Patterns for .env Management

Environment files are deceptively simple. A few KEY=value pairs, what could go wrong? Quite a bit, actually. Here’s how to manage them without shooting yourself in the foot. The Basic Rules 1 2 3 4 # .env DATABASE_URL=postgres://localhost:5432/myapp API_KEY=sk_test_abc123 DEBUG=true Rule 1: Never commit secrets to git. 1 2 3 4 5 6 # .gitignore .env .env.local .env.*.local *.pem *.key Rule 2: Always commit an example file. 1 2 3 4 # .env.example (committed to repo) DATABASE_URL=postgres://localhost:5432/myapp API_KEY=your_api_key_here DEBUG=true New developers copy .env.example to .env and fill in their values. The example documents what’s needed without exposing real credentials. ...

February 26, 2026 Â· 6 min Â· 1073 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

Nginx Configuration: From Basics to Production Hardening

Nginx powers a significant portion of the internet. These configurations will help you move from default installs to production-ready setups. 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 26 27 28 29 30 31 32 # /etc/nginx/nginx.conf user www-data; worker_processes auto; pid /run/nginx.pid; events { worker_connections 1024; multi_accept on; } 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; tcp_nopush on; tcp_nodelay on; keepalive_timeout 65; # Gzip gzip on; gzip_types text/plain text/css application/json application/javascript; # Virtual hosts include /etc/nginx/conf.d/*.conf; include /etc/nginx/sites-enabled/*; } Basic Server Block 1 2 3 4 5 6 7 8 9 10 11 12 13 # /etc/nginx/sites-available/example.com server { listen 80; listen [::]:80; server_name example.com www.example.com; root /var/www/example.com; index index.html; location / { try_files $uri $uri/ =404; } } 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 28 29 30 31 32 33 34 server { listen 443 ssl http2; listen [::]:443 ssl http2; server_name example.com; # Certificates ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem; # Modern SSL configuration ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256; ssl_prefer_server_ciphers off; # SSL session caching ssl_session_cache shared:SSL:10m; ssl_session_timeout 1d; ssl_session_tickets off; # OCSP stapling ssl_stapling on; ssl_stapling_verify on; resolver 8.8.8.8 8.8.4.4 valid=300s; root /var/www/example.com; } # Redirect HTTP to HTTPS server { listen 80; listen [::]:80; server_name example.com www.example.com; return 301 https://$server_name$request_uri; } 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; } Proxy with Buffering Control 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 location /api { proxy_pass http://backend; # Disable buffering for streaming proxy_buffering off; # Or tune buffer sizes proxy_buffer_size 4k; proxy_buffers 8 4k; proxy_busy_buffers_size 8k; # Timeouts proxy_connect_timeout 60s; proxy_send_timeout 60s; proxy_read_timeout 60s; } 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 25 26 27 28 29 30 31 32 33 34 35 upstream backend { # Round robin (default) server 10.0.0.1:3000; server 10.0.0.2:3000; server 10.0.0.3:3000; # Weighted server 10.0.0.1:3000 weight=3; server 10.0.0.2:3000 weight=1; # Backup server server 10.0.0.4:3000 backup; # Health checks server 10.0.0.1:3000 max_fails=3 fail_timeout=30s; } # Alternative algorithms upstream backend_least_conn { least_conn; server 10.0.0.1:3000; server 10.0.0.2:3000; } upstream backend_ip_hash { ip_hash; # Session persistence server 10.0.0.1:3000; server 10.0.0.2:3000; } server { location / { proxy_pass http://backend; } } Caching Proxy Cache 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 http { # Define cache zone proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=my_cache:10m max_size=1g inactive=60m use_temp_path=off; server { location / { proxy_pass http://backend; proxy_cache my_cache; proxy_cache_valid 200 60m; proxy_cache_valid 404 1m; proxy_cache_use_stale error timeout updating; # Cache key proxy_cache_key "$scheme$request_method$host$request_uri"; # Add header to show cache status add_header X-Cache-Status $upstream_cache_status; } # Bypass cache for specific requests location /api { proxy_pass http://backend; proxy_cache my_cache; proxy_cache_bypass $http_authorization; proxy_no_cache $http_authorization; } } } Static File Caching 1 2 3 4 5 location ~* \.(jpg|jpeg|png|gif|ico|css|js|woff2)$ { expires 30d; add_header Cache-Control "public, immutable"; access_log off; } Rate Limiting 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 http { # Define rate limit zones limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s; limit_req_zone $binary_remote_addr zone=login_limit:10m rate=1r/s; server { # Apply to API endpoints location /api/ { limit_req zone=api_limit burst=20 nodelay; proxy_pass http://backend; } # Stricter limit for login location /login { limit_req zone=login_limit burst=5; proxy_pass http://backend; } } } Connection Limiting 1 2 3 4 5 6 7 http { limit_conn_zone $binary_remote_addr zone=conn_limit:10m; server { limit_conn conn_limit 10; # Max 10 connections per IP } } Security Headers 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 server { # 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';" always; # HSTS (only enable after confirming SSL works) add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; # Hide nginx version server_tokens off; # Prevent access to hidden files location ~ /\. { deny all; } } Access Control IP-based 1 2 3 4 5 6 7 location /admin { allow 10.0.0.0/8; allow 192.168.1.0/24; deny all; proxy_pass http://backend; } Basic Authentication 1 2 3 4 5 6 location /admin { auth_basic "Admin Area"; auth_basic_user_file /etc/nginx/.htpasswd; proxy_pass http://backend; } Create password file: ...

February 25, 2026 Â· 6 min Â· 1232 words Â· Rob Washington

Environment Variables Done Right: 12-Factor Config in Practice

The third factor of the 12-Factor App methodology states: “Store config in the environment.” Simple advice that’s surprisingly easy to get wrong. The Core Principle Configuration that varies between environments (dev, staging, production) should come from environment variables, not code. This includes: Database connection strings API keys and secrets Feature flags Service URLs Port numbers Log levels What stays in code: application logic, default behaviors, anything that doesn’t change between deploys. ...

February 25, 2026 Â· 6 min Â· 1182 words Â· Rob Washington