name: nginx description: "[Applies to: **/*] This guide defines the definitive NGINX configuration best practices for our team, focusing on modularity, security, performance, and maintainability." source: "cursor_mdc"
nginx Best Practices
NGINX is our go-to for web serving, reverse proxying, and load balancing. This guide outlines the mandatory best practices for NGINX configurations, ensuring readability, security, and optimal performance across all our deployments.
Critical Guidelines:
1. Code Organization and Structure
Always break down your NGINX configuration into small, focused, and reusable include files. This significantly improves maintainability and reduces cognitive load.
✅ GOOD: Modular Configuration
# /etc/nginx/nginx.conf
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
# Global HTTP settings
include /etc/nginx/conf.d/http_common.conf;
# Upstream server definitions
include /etc/nginx/conf.d/upstreams/*.conf;
# SSL/TLS profiles
include /etc/nginx/conf.d/ssl_profiles/*.conf;
# Virtual hosts
include /etc/nginx/sites-enabled/*.conf;
}
❌ BAD: Monolithic Configuration
# /etc/nginx/nginx.conf (too long, hard to navigate)
# ... all server blocks, upstream definitions, SSL settings in one file ...
server {
listen 80;
server_name example.com;
# ... hundreds of lines ...
}
upstream backend_app {
# ... more lines ...
}
# ... SSL config ...
2. Consistent Formatting and Comments
Use two-space indentation. Add descriptive comments for every directive or block that isn't immediately obvious.
✅ GOOD
# /etc/nginx/sites-enabled/api.conf
server {
listen 80;
listen 443 ssl http2; # Enforce HTTPS and HTTP/2
server_name api.example.com;
# Include standard SSL settings
include /etc/nginx/conf.d/ssl_profiles/default_ssl.conf;
location / {
proxy_pass http://api_backend; # Proxy to the API upstream
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_redirect off;
}
}
❌ BAD
server {
listen 80;
listen 443 ssl http2;
server_name api.example.com;
include /etc/nginx/conf.d/ssl_profiles/default_ssl.conf;
location / {
proxy_pass http://api_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_redirect off;
}
} # No comments, inconsistent indentation
3. Security Hardening
Always enable modern TLS, HTTP/2, strong cipher suites, and essential security headers.
✅ GOOD: Secure SSL Profile (/etc/nginx/conf.d/ssl_profiles/default_ssl.conf)
ssl_protocols TLSv1.2 TLSv1.3; # Only modern, secure protocols
ssl_prefer_server_ciphers on;
ssl_ciphers "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384";
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1h;
ssl_session_tickets off;
ssl_dhparam /etc/nginx/ssl/dhparam.pem; # Generate with `openssl dhparam -out dhparam.pem 2048`
# Essential security headers
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "DENY" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "no-referrer-when-downgrade" always;
❌ BAD: Weak SSL/Missing Headers
ssl_protocols TLSv1 TLSv1.1 TLSv1.2; # Outdated protocols
ssl_ciphers HIGH:!aNULL:!MD5; # Too broad, potentially weak
# No security headers
4. Load Balancing and Upstream Configuration
Define upstream groups with explicit weight, max_fails, and fail_timeout. Use proxy_protocol and RealIP when behind another proxy/load balancer.
✅ GOOD: Robust Upstream (/etc/nginx/conf.d/upstreams/api_backend.conf)
upstream api_backend {
# Round-robin load balancing by default
server 10.0.0.1:8080 weight=5 max_fails=3 fail_timeout=10s;
server 10.0.0.2:8080 weight=5 max_fails=3 fail_timeout=10s;
# NGINX Plus: Enable health checks
# health_check interval=5s passes=1 fails=3;
}
# In http block or server block that accepts PROXY protocol
http {
# ...
server {
listen 80 proxy_protocol;
listen 443 ssl proxy_protocol;
# ...
# Set the real client IP from the PROXY protocol header
set_real_ip_from 192.168.1.0/24; # IP range of your upstream load balancer
real_ip_header proxy_protocol;
# ...
}
}
❌ BAD: Basic Upstream / Missing RealIP
upstream api_backend {
server 10.0.0.1:8080; # No weight, fail_timeout, max_fails
server 10.0.0.2:8080;
}
# No proxy_protocol or RealIP configuration, logs show proxy IP
5. Performance Optimization
Leverage caching, compression, and resource limits to ensure high-throughput and low-latency.
✅ GOOD: Caching, Compression, and Limits
http {
# ...
# Caching for static assets or API responses
proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=my_cache:10m inactive=60m max_size=1g;
server {
# ...
location /static/ {
root /var/www/html;
expires 30d;
add_header Cache-Control "public, immutable";
}
location /api/ {
proxy_cache my_cache;
proxy_cache_valid 200 302 10m;
proxy_cache_valid 404 1m;
proxy_cache_revalidate on;
proxy_cache_min_uses 1;
proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
proxy_cache_background_update on;
proxy_cache_lock on;
proxy_cache_lock_timeout 5s;
proxy_cache_lock_age 5s;
proxy_cache_bypass $http_pragma; # Bypass cache for specific requests
add_header X-Cache-Status $upstream_cache_status;
proxy_pass http://api_backend;
}
# Enable Gzip compression for text-based content
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_buffers 16 8k;
gzip_http_version 1.1;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
# Rate limiting (per IP, 10 requests per second burstable by 20)
limit_req_zone $binary_remote_addr zone=mylimit:10m rate=10r/s;
limit_req zone=mylimit burst=20 nodelay;
# Connection limits (10 connections per IP)
limit_conn_zone $binary_remote_addr zone=conn_limit:10m;
limit_conn conn_limit 10;
# Max request body size (10MB)
client_max_body_size 10M;
}
}
❌ BAD: No Caching, No Compression, No Limits
# No proxy_cache_path defined
server {
# ...
location /static/ {
root /var/www/html;
# No expires, no Cache-Control
}
# No gzip directives
# No rate limiting or connection limits
# No client_max_body_size, vulnerable to large body attacks
}
6. Common Pitfalls and Gotchas
a. location Block Order: Specific (exact, regex) locations must come before general (prefix) ones.
❌ BAD
location / { # This catches everything first
proxy_pass http://default_backend;
}
location = /specific { # This will never be hit
return 200 "Specific page";
}
✅ GOOD
location = /specific { # Exact match first
return 200 "Specific page";
}
location ~ \.php$ { # Regex matches next
fastcgi_pass unix:/var/run/php/php-fpm.sock;
# ...
}
location / { # General prefix match last
proxy_pass http://default_backend;
}
b. Missing resolver for Upstream Hostnames: If your upstream servers are defined by hostnames (not IPs), NGINX needs a DNS resolver.
❌ BAD
upstream dynamic_backend {
server app.internal.svc.cluster.local:8080; # Will fail to resolve without resolver
}
✅ GOOD
http {
resolver 10.0.0.10 valid=30s; # Use your internal DNS server, cache for 30s
resolver_timeout 5s;
upstream dynamic_backend {
server app.internal.svc.cluster.local:8080;
}
}
7. Testing Approaches
Always validate your NGINX configuration before reloading or restarting the service.
✅ GOOD: Configuration Syntax Check
sudo nginx -t
This command checks the syntax of your configuration files and attempts to open files referenced by include directives. If successful, it will output:
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
❌ BAD: Deploying without nginx -t
Never deploy or reload NGINX without first running nginx -t. A single syntax error can bring down your entire web service.
For more advanced testing, implement integration tests that hit your NGINX endpoints and verify expected behavior (e.g., correct routing, headers, content). Consider using NGINX Amplify for continuous configuration analysis and performance monitoring.