Portal | Level: L1: Foundations | Topics: Nginx & Web Servers, Load Balancing, TLS & PKI | Domain: DevOps & Tooling
Nginx & Web Servers - Primer¶
Why This Matters¶
Nginx sits in front of almost everything. It handles HTTP, terminates TLS, proxies to backends, serves static files, rate limits traffic, and does it all with a tiny memory footprint. Whether you are running a three-tier web app or a Kubernetes ingress, understanding Nginx is not optional.
The problem is that Nginx configuration looks deceptively simple but has deep subtleties — location matching order, the infamous proxy_pass trailing slash, and the "if is evil" trap. This primer walks you through how Nginx actually processes requests, so you can configure it with confidence instead of trial and error.
Architecture: How Nginx Works¶
Nginx uses an event-driven, non-blocking architecture:
┌─────────────────┐
Clients ──────▶│ Master Process │ (reads config, manages workers)
└────────┬────────┘
│ fork
┌──────────────┼──────────────┐
▼ ▼ ▼
┌─────────┐ ┌─────────┐ ┌─────────┐
│ Worker 1 │ │ Worker 2 │ │ Worker N │
│ (event │ │ (event │ │ (event │
│ loop) │ │ loop) │ │ loop) │
└─────────┘ └─────────┘ └─────────┘
Each worker handles thousands of connections using epoll/kqueue. No thread-per-connection overhead. This is why Nginx can handle 10K+ concurrent connections on modest hardware.
Who made it: Nginx was created by Igor Sysoev in 2004 to solve the C10K problem (handling 10,000 concurrent connections) for Rambler, Russia's second-largest website. The name is pronounced "engine-X." F5 Networks acquired Nginx Inc. in 2019 for $670 million.
Key Config: Worker Tuning¶
# /etc/nginx/nginx.conf
worker_processes auto; # one per CPU core (auto detects)
worker_rlimit_nofile 65535; # max open files per worker
events {
worker_connections 4096; # max connections per worker
multi_accept on; # accept all pending connections at once
use epoll; # Linux: use epoll (default on Linux)
}
Total max connections = worker_processes x worker_connections.
Directive Hierarchy¶
Nginx config is hierarchical. Directives inherit from parent contexts:
main (global)
├── events { }
├── http { }
│ ├── server { } # virtual host
│ │ ├── location / { } # URI matching
│ │ └── location /api { }
│ └── server { } # another virtual host
└── stream { } # TCP/UDP proxy (non-HTTP)
A directive set in http {} applies to all server {} blocks unless overridden. A directive in server {} applies to all location {} blocks unless overridden.
http {
gzip on; # applies to all servers
server {
gzip off; # overrides for this server only
location /api {
gzip on; # overrides again for this location
}
}
}
Server Blocks (Virtual Hosts)¶
server {
listen 80;
server_name example.com www.example.com;
root /var/www/example;
location / {
try_files $uri $uri/ =404;
}
}
server {
listen 80;
server_name api.example.com;
location / {
proxy_pass http://backend;
}
}
How Nginx Selects a Server Block¶
- Match the
listendirective (IP:port) - Match
server_nameagainst theHostheader - If no match, use the
default_server
server {
listen 80 default_server; # catches unmatched requests
server_name _; # convention for "match nothing"
return 444; # close connection (Nginx special)
}
Location Matching¶
This is where most Nginx confusion lives. Nginx evaluates locations in a specific order:
| Modifier | Type | Priority | Example |
|---|---|---|---|
= |
Exact match | 1 (highest) | location = /health |
^~ |
Prefix (no regex) | 2 | location ^~ /static/ |
~ |
Regex (case-sensitive) | 3 | location ~ \.php$ |
~* |
Regex (case-insensitive) | 3 | location ~* \.(jpg|png)$ |
| (none) | Prefix | 4 (lowest) | location / |
The Matching Algorithm¶
- Nginx first finds the longest matching prefix location
- If that prefix has
^~, stop — use it - If that prefix is
=(exact), stop — use it - Otherwise, evaluate all regex locations in config order; first regex match wins
- If no regex matches, use the longest prefix from step 1
# This is evaluated in this order:
location = /exact { } # only matches /exact (not /exact/)
location ^~ /static/ { } # prefix, skips regex check
location ~ \.php$ { } # regex, checked in config order
location ~* \.(jpg|gif)$ { } # case-insensitive regex
location /general/ { } # prefix, lowest priority
location / { } # catch-all prefix
Reverse Proxy with proxy_pass¶
Basic Proxy¶
server {
listen 80;
server_name app.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;
}
}
The Trailing Slash Matters¶
# NO trailing slash: location path is APPENDED to backend URL
location /app/ {
proxy_pass http://backend;
# Request: /app/page -> Backend sees: /app/page
}
# WITH trailing slash: location path is REPLACED
location /app/ {
proxy_pass http://backend/;
# Request: /app/page -> Backend sees: /page
}
# With a path: location is replaced with the path
location /app/ {
proxy_pass http://backend/v2/;
# Request: /app/page -> Backend sees: /v2/page
}
This is a top-5 Nginx misconfiguration. Get it wrong and your backend receives mangled paths.
Remember: Trailing slash mnemonic: No slash = No change (path passes through), Slash = Substitute (location prefix is replaced). The trailing
/onproxy_passactivates URI rewriting.
Upstream Blocks (Load Balancing)¶
upstream backend {
least_conn; # load balancing method
server 10.0.0.1:8080 weight=3;
server 10.0.0.2:8080;
server 10.0.0.3:8080 backup; # only used when others are down
keepalive 32; # persistent connections to backends
}
server {
location / {
proxy_pass http://backend;
proxy_http_version 1.1;
proxy_set_header Connection ""; # required for keepalive
}
}
Load Balancing Methods¶
round_robin default, rotate through servers
least_conn send to server with fewest active connections
ip_hash sticky sessions based on client IP
hash $key consistent hashing on arbitrary key
random random with optional two-choice algorithm
Health Checks (Open Source)¶
Nginx OSS does passive health checks only:
upstream backend {
server 10.0.0.1:8080 max_fails=3 fail_timeout=30s;
server 10.0.0.2:8080 max_fails=3 fail_timeout=30s;
}
After 3 failures in 30 seconds, the server is marked down for 30 seconds.
SSL/TLS Termination¶
server {
listen 443 ssl http2;
server_name secure.example.com;
ssl_certificate /etc/letsencrypt/live/secure.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/secure.example.com/privkey.pem;
# Modern TLS config
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
ssl_prefer_server_ciphers off;
# OCSP stapling
ssl_stapling on;
ssl_stapling_verify on;
# Session caching
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
ssl_session_tickets off;
# HSTS
add_header Strict-Transport-Security "max-age=63072000" always;
}
# Redirect HTTP to HTTPS
server {
listen 80;
server_name secure.example.com;
return 301 https://$host$request_uri;
}
Caching¶
Proxy Cache¶
http {
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_cache my_cache;
proxy_cache_valid 200 302 10m;
proxy_cache_valid 404 1m;
proxy_cache_use_stale error timeout updating http_500 http_502;
add_header X-Cache-Status $upstream_cache_status;
proxy_pass http://backend;
}
}
}
Static File Caching Headers¶
location ~* \.(css|js|jpg|png|gif|ico|woff2)$ {
expires 30d;
add_header Cache-Control "public, immutable";
access_log off;
}
Rate Limiting¶
http {
# Define rate limit zone: 10 requests/second per IP
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
server {
location /api/ {
limit_req zone=api burst=20 nodelay;
# burst=20: allow 20 request burst
# nodelay: don't delay, just allow burst then reject
limit_req_status 429;
proxy_pass http://backend;
}
}
}
Common Patterns¶
try_files for SPAs¶
Custom Error Pages¶
WebSocket Proxy¶
location /ws/ {
proxy_pass http://backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 86400;
}
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;
Testing and Reloading¶
# Test config syntax BEFORE reloading
nginx -t
# Reload (graceful — no downtime)
nginx -s reload
# or
systemctl reload nginx
# Restart (drops connections — avoid in production)
systemctl restart nginx
# Show compiled modules and config path
nginx -V
Always nginx -t before nginx -s reload. Always. Make it muscle memory.
Under the hood:
nginx -s reloadsends SIGHUP to the master process. The master forks new worker processes with the new config, then gracefully shuts down old workers after they finish serving active requests. This is why reload is zero-downtime but restart (systemctl restart nginx) drops connections — restart does a hard stop/start.Debug clue: When Nginx returns a 502 Bad Gateway, it means Nginx is running fine but cannot connect to the upstream backend. Check: is the backend process running? Is it listening on the right port? Is there a firewall rule blocking the connection? The answer is almost always in the Nginx error log at
/var/log/nginx/error.log.
Wiki Navigation¶
Prerequisites¶
- Networking Deep Dive (Topic Pack, L1)
Related Content¶
- API Gateways & Ingress (Topic Pack, L2) — Load Balancing
- Case Study: BMC Clock Skew Cert Failure (Case Study, L2) — TLS & PKI
- Case Study: DNS Looks Broken — TLS Expired, Fix Is Cert-Manager (Case Study, L2) — TLS & PKI
- Case Study: Deployment Stuck — ImagePull Auth Failure, Vault Secret Rotation (Case Study, L2) — TLS & PKI
- Case Study: SSL Cert Chain Incomplete (Case Study, L1) — TLS & PKI
- Case Study: User Auth Failing — OIDC Cert Expired, Cloud KMS Rotation (Case Study, L2) — TLS & PKI
- Deep Dive: TLS Handshake (deep_dive, L2) — TLS & PKI
- HAProxy & Nginx for Ops (Topic Pack, L2) — Load Balancing
- HTTP Protocol (Topic Pack, L0) — TLS & PKI
- Interview: Certificate Expired (Scenario, L2) — TLS & PKI
Pages that link here¶
- Anti-Primer: Nginx Web Servers
- BMC Clock Skew - Certificate Failure
- HAProxy & Nginx for Ops
- HTTP Protocol
- Master Curriculum: 40 Weeks
- Nginx & Web Servers
- Production Readiness Review: Answer Key
- Production Readiness Review: Study Plans
- Runbook: Load Balancer Health Check Failure
- Scenario: TLS Certificate Expired
- Symptoms: Deployment Stuck, ImagePull Auth Failure, Fix Is Vault Secret Rotation
- Symptoms: User Auth Failing, OIDC Cert Expired, Fix Is Cloud KMS Rotation
- TLS Handshake Deep Dive
- TLS Works From Some Clients But Fails From Others