Server Blocks, Reverse Proxy, SSL/HTTPS, Load Balancing, Caching, Security Headers, Performance Tuning — web server mastery.
# ── Ubuntu / Debian ──
sudo apt update
sudo apt install nginx
sudo systemctl start nginx
sudo systemctl enable nginx
# ── CentOS / RHEL ──
sudo yum install epel-release
sudo yum install nginx
sudo systemctl start nginx
sudo systemctl enable nginx
# ── macOS (Homebrew) ──
brew install nginx
brew services start nginx
# ── Docker ──
docker run -d --name nginx -p 80:80 nginx:alpine
# ── From Source (latest) ──
./configure --prefix=/etc/nginx \
--with-http_ssl_module \
--with-http_v2_module \
--with-http_realip_module \
--with-http_gzip_static_module \
--with-stream \
--with-stream_ssl_module
make && sudo make install# ── Control Commands ──
nginx # start nginx
nginx -s stop # fast shutdown
nginx -s quit # graceful shutdown
nginx -s reload # reload config (no downtime)
nginx -s reopen # reopen log files
nginx -t # test configuration syntax
nginx -T # test and dump full config
# ── Systemd ──
sudo systemctl start nginx
sudo systemctl stop nginx
sudo systemctl restart nginx
sudo systemctl reload nginx # graceful config reload
sudo systemctl status nginx
# ── Debug ──
nginx -V # show compiled modules & paths
nginx -t -c /path/to/nginx.conf # test specific config
tail -f /var/log/nginx/access.log
tail -f /var/log/nginx/error.log| Path | Description |
|---|---|
| /etc/nginx/ | Config root directory |
| /etc/nginx/nginx.conf | Main configuration file |
| /etc/nginx/conf.d/ | Server block configs (*.conf) |
| /etc/nginx/sites-available/ | Available sites |
| /etc/nginx/sites-enabled/ | Enabled sites (symlinks) |
| /var/www/html/ | Default document root |
| /var/log/nginx/access.log | Access log |
| /var/log/nginx/error.log | Error log |
| Directive | Context | Description |
|---|---|---|
| worker_processes | main | Number of worker processes (auto) |
| worker_connections | events | Max connections per worker |
| error_log | main/http/server | Error log path and level |
| access_log | http/server/location | Access log path and format |
| include | any | Include another config file |
| sendfile | http/server/location | Zero-copy file transfers |
| tcp_nopush | http/server | Optimized TCP packet sending |
| keepalive_timeout | http/server | Keepalive connection timeout |
# ── Minimal Production nginx.conf ──
worker_processes auto;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
events {'{'}
worker_connections 1024;
multi_accept on;
use epoll;
{'}'}
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" '
'rt=$request_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;
server_tokens off;
# Gzip
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 4;
gzip_min_length 256;
gzip_types text/plain text/css application/json
application/javascript text/xml application/xml;
# Include server blocks
include /etc/nginx/conf.d/*.conf;
include /etc/nginx/sites-enabled/*;
{'}'}# ── Basic Server Block ──
server {'{'}
listen 80;
listen [::]:80;
server_name example.com www.example.com;
root /var/www/example/public;
index index.html index.htm;
location / {'{'}
try_files $uri $uri/ =404;
{'}'}
# Custom error pages
error_page 404 /404.html;
error_page 500 502 503 504 /50x.html;
location = /50x.html {'{'}
root /usr/share/nginx/html;
{'}'}
access_log /var/log/nginx/example.access.log;
error_log /var/log/nginx/example.error.log;
{'}'}
# ── Default server (catch-all) ──
server {'{'}
listen 80 default_server;
server_name _; # matches any hostname not caught by others
return 444; # drop connection (no response)
{'}'}
# ── Redirect HTTP to HTTPS ──
server {'{'}
listen 80;
listen [::]:80;
server_name example.com www.example.com;
return 301 https://$host$request_uri;
{'}'}
# ── Redirect www to non-www ──
server {'{'}
listen 80;
server_name www.example.com;
return 301 https://example.com$request_uri;
{'}'}| Pattern | Match Type | Example | Priority |
|---|---|---|---|
| = /path | Exact match | = /api/health | Highest (stops search) |
| ^~ /prefix | Prefix match (stops regex) | ^~ /images/ | High (no regex check) |
| ~ /regex | Case-sensitive regex | ~ \.php$ | Medium (first match wins) |
| ~* /regex | Case-insensitive regex | ~* \.\w+$ | Medium |
| /prefix | Longest prefix match | /api/v1/ | Lowest (fallback) |
| @named | Named location | @fallback | Internal redirects only |
# ── Location Block Examples ──
# Static files
location /static/ {'{'}
alias /var/www/static/;
expires 30d;
add_header Cache-Control "public, immutable";
{'}'}
# PHP (FastCGI)
location ~ \.php$ {'{'}
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/run/php/php-fpm.sock;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
{'}'}
# Block specific paths
location ~* /\.(env|git|svn|htaccess|htpasswd) {'{'}
deny all;
return 404;
{'}'}
# Named location for fallback
location @fallback {'{'}
proxy_pass http://backend;
{'}'}
# Try files with fallback
location / {'{'}
try_files $uri $uri/ /index.html?q=$uri&$args;
{'}'}
# Return JSON
location /health {'{'}
default_type application/json;
return 200 '{"status":"ok"}';
{'}'}
# Internal only (no direct access)
location /internal/ {'{'}
internal;
alias /var/www/internal-data/;
{'}'}=), then prefix (longest match wins), then if a prefix has ^~, it stops. Otherwise, regex patterns are checked in order of appearance in the config file. If no regex matches, the longest prefix is used.# ── Basic Reverse Proxy ──
server {'{'}
listen 80;
server_name api.example.com;
location / {'{'}
proxy_pass http://127.0.0.1:3000;
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;
{'}'}
{'}'}
# ── Path-based Routing ──
server {'{'}
listen 80;
server_name example.com;
# API to Node.js backend
location /api/ {'{'}
proxy_pass http://127.0.0.1:3000/; # trailing slash strips /api prefix
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 proxy
location /ws/ {'{'}
proxy_pass http://127.0.0.1:3001/;
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;
{'}'}
# Frontend (Next.js/Nuxt)
location / {'{'}
proxy_pass http://127.0.0.1:4000;
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;
{'}'}
{'}'}
# ── Upstream (Load Balancing) ──
upstream backend {'{'}
server 127.0.0.1:3000 weight=3;
server 127.0.0.1:3001 weight=2;
server 127.0.0.1:3002 backup;
keepalive 32;
{'}'}
server {'{'}
listen 80;
server_name app.example.com;
location / {'{'}
proxy_pass http://backend;
proxy_http_version 1.1;
proxy_set_header Connection "";
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_connect_timeout 5s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
proxy_buffers 8 16k;
proxy_buffer_size 32k;
{'}'}
{'}'}| Header | Purpose |
|---|---|
| Host | Original hostname requested by client |
| X-Real-IP | Client IP address |
| X-Forwarded-For | Chain of proxy IPs |
| X-Forwarded-Proto | Original protocol (http/https) |
| X-Forwarded-Host | Original Host header |
| X-Forwarded-Port | Original server port |
| Upgrade | WebSocket upgrade header |
| Connection | Connection upgrade for WebSocket |
| Directive | Default | Purpose |
|---|---|---|
| proxy_connect_timeout | 60s | Connect to upstream timeout |
| proxy_send_timeout | 60s | Send request to upstream timeout |
| proxy_read_timeout | 60s | Read response from upstream timeout |
| proxy_buffer_size | 4k/8k | First part of response buffer |
| proxy_buffers | 8 4k/8k | Number and size of buffers |
| proxy_busy_buffers_size | 8k/16k | Busy buffer size |
proxy_pass http://backend; (no slash) preserves the URI including /api/.proxy_pass http://backend/; (with trailing slash) strips the matched location prefix. This is the most common Nginx proxy misconfiguration.# ── Let's Encrypt with Certbot ──
sudo apt install certbot python3-certbot-nginx
sudo certbot --nginx -d example.com -d www.example.com
sudo certbot renew --dry-run # test renewal
sudo certbot renew # actual renewal
sudo systemctl status certbot.timer # auto-renewal
# ── Manual SSL Certificates ──
sudo openssl req -x509 -nodes -days 365 \
-newkey rsa:2048 \
-keyout /etc/ssl/private/nginx-selfsigned.key \
-out /etc/ssl/certs/nginx-selfsigned.crt
# ── Generate Diffie-Hellman (stronger encryption) ──
sudo openssl dhparam -out /etc/nginx/dhparam.pem 2048# ── SSL Server Block ──
server {'{'}
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name example.com www.example.com;
# Certificate paths
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
# SSL Protocols & Ciphers (hardened)
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
:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305;
ssl_prefer_server_ciphers off;
# Session settings
ssl_session_timeout 1d;
ssl_session_cache shared:MozSSL:10m;
ssl_session_tickets off;
# OCSP Stapling
ssl_stapling on;
ssl_stapling_verify on;
ssl_trusted_certificate /etc/letsencrypt/live/example.com/chain.pem;
resolver 8.8.8.8 8.8.4.4 valid=300s;
resolver_timeout 5s;
# DH params (if generated)
ssl_dhparam /etc/nginx/dhparam.pem;
root /var/www/example/public;
index index.html;
location / {'{'}
try_files $uri $uri/ =404;
{'}'}
{'}'}
# ── Security Headers ──
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
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 Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'" always;ssl_verify offmakes your server vulnerable to MITM attacks. Always use trusted CAs (Let's Encrypt is free). Test your SSL config at SSL Labs.# ── Upstream: Load Balancing Methods ──
# 1. Round Robin (default)
upstream rr_backend {'{'}
server 10.0.1.1:3000;
server 10.0.1.2:3000;
server 10.0.1.3:3000;
{'}'}
# 2. Least Connections
upstream lc_backend {'{'}
least_conn;
server 10.0.1.1:3000;
server 10.0.1.2:3000;
server 10.0.1.3:3000;
{'}'}
# 3. IP Hash (session affinity)
upstream iph_backend {'{'}
ip_hash;
server 10.0.1.1:3000;
server 10.0.1.2:3000;
{'}'}
# 4. Generic Hash (hash any key)
upstream hash_backend {'{'}
hash $request_uri consistent; # consistent hashing
server 10.0.1.1:3000;
server 10.0.1.2:3000;
{'}'}
# 5. Weighted
upstream weighted_backend {'{'}
server 10.0.1.1:3000 weight=5; # 50% traffic
server 10.0.1.2:3000 weight=3; # 30% traffic
server 10.0.1.3:3000 weight=2; # 20% traffic
{'}'}
# ── Server Parameters ──
upstream full_backend {'{'}
server 10.0.1.1:3000 weight=3 max_fails=3 fail_timeout=30s;
server 10.0.1.2:3000 backup; # only used when others fail
server 10.0.1.3:3000 down; # permanently offline
server 10.0.1.4:3000 max_conns=100; # max connections
server 10.0.1.5:3000 resolve; # re-resolve DNS
keepalive 32; # keepalive connections
{'}'}
server {'{'}
listen 80;
server_name app.example.com;
location / {'{'}
proxy_pass http://full_backend;
proxy_http_version 1.1;
proxy_set_header Connection "";
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;
# Health check endpoint
proxy_next_upstream error timeout http_502 http_503 http_504;
proxy_next_upstream_tries 3;
proxy_next_upstream_timeout 10s;
{'}'}
{'}'}| Method | Directive | Best For |
|---|---|---|
| Round Robin | (default) | Homogeneous servers |
| Least Connections | least_conn | Long-running requests |
| IP Hash | ip_hash | Session persistence |
| Generic Hash | hash $key | URI-based routing |
| Random | random | Testing / edge cases |
| Parameter | Default | Description |
|---|---|---|
| weight | 1 | Server weight for balancing |
| max_fails | 1 | Failures before marking down |
| fail_timeout | 10s | Time server is considered down |
| backup | - | Only used when others are down |
| down | - | Permanently offline |
| max_conns | 0 (unlimited) | Max simultaneous connections |
| resolve | off | Re-resolve DNS periodically |
# ── Gzip Compression ──
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 4; # 1-9 (4 is good balance)
gzip_min_length 256;
gzip_buffers 16 8k;
gzip_http_version 1.1;
gzip_types
text/plain
text/css
text/xml
text/javascript
application/json
application/javascript
application/xml
application/rss+xml
application/atom+xml
image/svg+xml
font/opentype
font/ttf;
# ── Browser Cache (Static Files) ──
location ~* \.(jpg|jpeg|png|gif|ico|svg|webp|avif)$ {'{'}
expires 90d;
add_header Cache-Control "public, immutable";
access_log off;
{'}'}
location ~* \.(css|js)$ {'{'}
expires 1y;
add_header Cache-Control "public, immutable";
access_log off;
{'}'}
location ~* \.(woff|woff2|ttf|otf|eot)$ {'{'}
expires 1y;
add_header Cache-Control "public, immutable";
add_header Access-Control-Allow-Origin "*";
access_log off;
{'}'}
# ── Proxy Cache ──
proxy_cache_path /var/cache/nginx/api
levels=1:2
keys_zone=api_cache:10m
max_size=1g
inactive=60m
use_temp_path=off;
server {'{'}
listen 80;
server_name api.example.com;
location / {'{'}
proxy_cache api_cache;
proxy_cache_key $scheme$host$request_uri;
proxy_cache_valid 200 10m;
proxy_cache_valid 404 1m;
proxy_cache_valid any 1m;
proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
proxy_cache_lock on;
proxy_cache_background_update on;
proxy_cache_revalidate on;
add_header X-Cache-Status $upstream_cache_status;
proxy_pass http://backend;
proxy_set_header Host $host;
{'}'}
{'}'}
# ── Rate Limiting ──
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s;
server {'{'}
location /api/ {'{'}
limit_req zone=api_limit burst=20 nodelay;
limit_req_status 429;
proxy_pass http://backend;
{'}'}
{'}'}
# ── Connection Limiting ──
limit_conn_zone $binary_remote_addr zone=conn_limit:10m;
server {'{'}
limit_conn conn_limit 10;
limit_conn_status 429;
{'}'}| Directive | Description |
|---|---|
| public | Cacheable by any cache (CDN, browser) |
| private | Cacheable only by browser |
| no-cache | Must revalidate before using cached |
| no-store | Never cache |
| max-age=31536000 | Cache for 1 year (immutable) |
| must-revalidate | Must verify after expiry |
| immutable | Never revalidate (for content-hashed) |
| s-maxage=300 | Cache for 5min (CDN/shared only) |
| Value | Meaning |
|---|---|
| MISS | Not in cache, fetched from upstream |
| HIT | Served from cache |
| EXPIRED | Cache expired, revalidated |
| STALE | Served stale while revalidating |
| UPDATING | Cache is being updated |
| REVALIDATED | Revalidated and served from cache |
| BYPASS | Cache was bypassed (e.g., no-cache) |
# ── 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 Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
add_header Referrer-Policy strict-origin-when-cross-origin always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'; frame-ancestors 'self';" always;
# ── Block Sensitive Files ──
location ~ /\.(env|git|svn|htaccess|htpasswd|DS_Store)$ {'{'}
deny all;
return 404;
{'}'}
location ~ /\.(bak|config|sql|log|inc|old|orig)$ {'{'}
deny all;
return 404;
{'}'}
# ── Block Bad Bots ──
if ($http_user_agent ~* (SemrushBot|AhrefsBot|MJ12bot|DotBot|PetalBot)) {'{'}
return 403;
{'}'}
# ── Limit Request Size ──
client_max_body_size 10m;
# ── Hide Nginx Version ──
server_tokens off;
# ── Block by IP ──
deny 192.168.1.100;
deny 10.0.0.0/8;
allow all;
# ── Basic Auth ──
# Create password file: htpasswd -c /etc/nginx/.htpasswd user1
location /admin/ {'{'}
auth_basic "Admin Area";
auth_basic_user_file /etc/nginx/.htpasswd;
proxy_pass http://backend;
{'}'}
# ── IP Whitelist for API ──
location /internal-api/ {'{'}
allow 10.0.0.0/8;
allow 172.16.0.0/12;
deny all;
proxy_pass http://backend;
{'}'}
# ── Request Rate Limiting ──
limit_req_zone $binary_remote_addr zone=general:10m rate=30r/s;
limit_req_zone $binary_remote_addr zone=login:10m rate=5r/m;
limit_req_zone $binary_remote_addr zone=api:10m rate=100r/s;
server {'{'}
# General site limit
limit_req zone=general burst=50 nodelay;
# Login endpoint (stricter)
location /auth/login {'{'}
limit_req zone=login burst=5 nodelay;
limit_req_status 429;
proxy_pass http://backend;
{'}'}
# API endpoint
location /api/ {'{'}
limit_req zone=api burst=200 nodelay;
limit_req_status 429;
proxy_pass http://backend;
{'}'}
{'}'}location ~ /\\.(env|git) block is essential. Also use server_tokens off to hide version info, and always set client_max_body_sizeto prevent large file upload attacks.# ── Worker Tuning ──
worker_processes auto; # = CPU cores
worker_rlimit_nofile 100000; # max open files per worker
events {'{'}
worker_connections 4096; # connections per worker
multi_accept on; # accept all connections at once
use epoll; # Linux: epoll (best), BSD: kqueue
{'}'}
# ── TCP Optimization ──
sendfile on; # zero-copy file transfer
tcp_nopush on; # send headers and data in one packet
tcp_nodelay on; # disable Nagle's algorithm (for small data)
keepalive_timeout 65; # keepalive timeout
keepalive_requests 1000; # max requests per keepalive connection
# ── Buffer Tuning ──
client_body_buffer_size 16k;
client_header_buffer_size 1k;
client_max_body_size 10m;
large_client_header_buffers 4 8k;
# ── File Caching ──
open_file_cache max=200000 inactive=20s;
open_file_cache_valid 30s;
open_file_cache_min_uses 2;
open_file_cache_errors on;
# ── SSL Session Caching ──
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
ssl_session_tickets off;
# ── Log Buffering ──
access_log /var/log/nginx/access.log main buffer=32k flush=5m;
error_log /var/log/nginx/error.log warn buffer=16k flush=5m;| Setting | Recommended | Why |
|---|---|---|
| worker_processes | auto | Match CPU cores |
| worker_connections | 4096+ | High concurrency |
| sendfile | on | Zero-copy transfers |
| gzip | on | Compress responses |
| open_file_cache | on | Cache file descriptors |
| keepalive | on | Reuse connections |
| server_tokens | off | Security + smaller headers |
| access_log buffer | 32k+ | Reduce disk I/O |
# ── Rewrite Rules ──
# Permanent redirect
rewrite ^/old/(.*)$ /new/$1 permanent;
# Remove trailing slash
rewrite ^(.*)/$ $1 permanent;
# Remove .html extension
rewrite ^(/.*)\.html$ $1 permanent;
# Force lowercase
if ($host != $host_simplified) {'{'}
return 301 https://$host_simplified$request_uri;
{'}'}
# Block user agents
if ($http_user_agent ~* (bot|crawler)) {'{'}
return 403;
{'}'}
# Custom 502 page
error_page 502 /502.html;
location = /502.html {'{'}
root /var/www/html/errors;
internal;
{'}'}location /api/ { proxy_pass http://backend; } — the original URI /api/users is passed to the backend as-is.location /api/ { proxy_pass http://backend/; } — the matched /api/ prefix is stripped, so /api/users becomes /users. This is the most common Nginx misconfiguration. Always test with nginx -t and check the upstream receives the correct path.
Nginx evaluates locations in this order: (1) = exact match (immediate use if matched), (2) ^~ longest prefix (stops regex search if matched), (3) regex ~ / ~* in config order (first match wins), (4) longest prefix without ^~ (used only if no regex matched). Named locations @name are only used for internal redirects.
Define an upstream block with multiple server directives. Methods: round-robin (default), least_conn,ip_hash, and hash. Use weight for weighted distribution, backup for failover,max_fails and fail_timeout for health checking. Use proxy_next_upstream to retry failed requests on other servers.
Each worker process handles connections independently. worker_processes auto sets the number equal to CPU cores, which is optimal since Nginx is event-driven and non-blocking. More workers than cores wastes CPU context-switching. Each worker can handle thousands of connections via epoll/kqueue event notification.
Set proxy_http_version 1.1; and add Upgrade and Connection headers. Increase proxy_read_timeout (default 60s is too short for persistent WebSocket connections — use 86400s for 24h). Also set proxy_set_header Connection "upgrade" to properly upgrade the connection.
if (avoid it — use map and return instead), explain the event-driven architecture, and discuss real-world scenarios: SSL termination, caching strategies, rate limiting, and WebSocket proxying.