Guide
nginx fundamentals explained
nginx powers a large share of the world’s busiest sites — not because it is
the only web server, but because its event-driven, non-blocking
architecture handles tens of thousands of idle keep-alive connections on
modest hardware. On a typical VPS, nginx serves static HTML from disk, terminates
TLS, and forwards API
traffic to Node or Python processes bound to localhost. Understanding nginx at the
config-block level — http, server, location —
is foundational for anyone deploying web apps, APIs, or
microservices
behind a single public IP. This guide covers the process model, configuration
anatomy, static vs proxy modes, upstream pools and health semantics, compression and
edge caching, rate limiting, a multi-service VPS worked example, a server-choice
decision table, common pitfalls, and a production checklist.
What nginx is and how it scales
nginx (pronounced “engine-x”) is a high-performance HTTP server, reverse proxy, and load balancer. Unlike traditional process-per-connection servers (classic Apache prefork), nginx uses an event loop per worker process: each worker can manage many connections concurrently without spawning a thread per client.
Master and worker processes
A running nginx instance has one master process (runs as root,
reads config, binds privileged ports) and one or more worker
processes (handle actual I/O). The master never serves traffic; workers accept
connections, parse HTTP, and either serve files or proxy upstream. Reloading config
(nginx -s reload) spawns new workers with updated rules and gracefully
retires old ones — critical for zero-downtime deploys on a single box.
Three common roles
- Static file server — serves HTML, CSS, images directly from disk with efficient
sendfile. - Reverse proxy — forwards dynamic requests to app servers; see the dedicated reverse proxy guide.
- Load balancer — distributes traffic across an upstream pool using round-robin, least-conn, or IP hash; pairs with L7 load balancing concepts.
Most production installs combine all three: static assets from nginx, API routes
proxied to backends, multiple backend instances behind an upstream
block.
Configuration anatomy
nginx config is declarative and block-structured. Directives inherit downward; inner blocks override outer defaults. A minimal mental model:
# /etc/nginx/nginx.conf (simplified)
worker_processes auto;
events { worker_connections 1024; }
http {
include mime.types;
default_type application/octet-stream;
server {
listen 80;
server_name example.com;
root /var/www/html;
location /api/ {
proxy_pass http://127.0.0.1:3000;
}
location / {
try_files $uri $uri/ =404;
}
}
}
Key blocks and directives
events— connection limits per worker; tune with expected concurrency.http— global HTTP settings: gzip, logging format, upstream definitions.server— virtual host:listen,server_name, TLS certs.location— URI matching (prefix, regex, exact); where routing decisions happen.upstream— named backend pool forproxy_pass.
Location matching follows specificity rules: exact = beats prefix,
longest prefix wins among plain prefixes, regex is evaluated in declaration order
(a common source of subtle bugs). Use nginx -t before every reload —
a syntax error on reload can leave you without a working config if you were not
careful.
Serving static files efficiently
For static sites and SPAs, nginx reads files from root or
alias and streams them with minimal copying. Important directives:
root— appends the URI to the path (/var/www+/index.html).alias— replaces the matched location prefix (use carefully with trailing slashes).try_files— fall through candidates; essential for SPA client-side routing.expires/add_header Cache-Control— pair with HTTP caching strategy.
Static delivery is where nginx shines: no interpreter overhead, kernel
sendfile, and optional on-disk or in-memory caching via
proxy_cache even for proxied content. For a content site serving
thousands of guide pages, letting nginx handle TLS + static files while Node
handles only dynamic settlement APIs is the right split.
Reverse proxy and upstream pools
Dynamic requests use proxy_pass to forward to a backend URL. Headers
must be set explicitly or the upstream sees wrong hostnames and schemes:
location /api/ {
proxy_pass http://api_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;
}
upstream api_backend {
server 127.0.0.1:3847;
server 127.0.0.1:3848 backup;
keepalive 32;
}
Upstream semantics
- Round-robin (default) — distributes requests evenly.
- least_conn — sends to the worker with fewest active connections.
- ip_hash — sticky sessions by client IP (fragile behind NAT).
- backup — only used when primaries are down.
- max_fails / fail_timeout — passive health: mark server down after N errors.
For WebSocket upgrades, add proxy_set_header Upgrade $http_upgrade
and Connection "upgrade". Timeouts (proxy_read_timeout,
proxy_connect_timeout) prevent hung workers when backends stall — a
frequent cause of 502 Bad Gateway during deploys or GC pauses.
TLS termination at the edge
nginx terminates HTTPS on port 443, decrypts traffic, and forwards plain HTTP to
localhost backends — the standard pattern on a single VPS. Certbot integrates
cleanly: obtain Let’s Encrypt certs, nginx references
ssl_certificate and ssl_certificate_key, and HTTP-01
challenges are served from a .well-known location block.
Modern configs enable TLS 1.2+, strong cipher suites, OCSP stapling, and HSTS
once you are confident HTTPS works everywhere. Redirect port 80 to 443 with a
tiny server block. Never proxy sensitive cookies over unencrypted
loopback in multi-tenant environments; on a dedicated VPS, localhost HTTP to
backends is acceptable and simplifies cert rotation.
Compression, caching, and rate limiting
Gzip and brotli
Enable gzip on for text assets (HTML, CSS, JS, JSON). Brotli modules
(or pre-compressed static files) shave another 15–25% off transfer size. Do not
gzip tiny responses — CPU cost exceeds savings below ~1 KB.
Proxy cache
proxy_cache stores upstream responses on disk or in memory keyed by
URL (and optionally custom keys). Useful for cacheable GET endpoints and static
API responses. Set Cache-Control from upstream thoughtfully — nginx
respects validity headers unless you override. Complements CDN edge caching described in
CDN guides
for global distribution.
Rate limiting
limit_req_zone and limit_req implement token-bucket
throttling per IP or API key header — the first line of defense before traffic
hits your app. Pair with application-level limits from
API rate limiting
patterns for layered protection against abuse and credential stuffing.
Worked example: one VPS, static site plus API
Imagine deploying a content site with a Node settlement API — a layout common to platforms like solana.garden:
- Static root —
/var/www/site/publicholds HTML guides; nginx serves/guides/and/directly. - API proxy —
location /api/settle/forwards to127.0.0.1:3847with forwarded headers and 30s read timeout. - Games path — separate upstream on port 3849 for game WebSocket + REST.
- TLS — single cert for the apex domain; HTTP redirects to HTTPS.
- Logging — custom
log_formatincluding$request_timeand$upstream_response_timefor latency triage. - Deploy flow — rsync new static files, reload nginx; restart Node without touching nginx when only backend code changes.
This split keeps nginx doing what it is best at (I/O-bound static + TLS) while application logic stays in managed processes — aligned with twelve-factor port-binding and disposability principles.
Decision table: nginx vs alternatives
| Need | Prefer | Why |
|---|---|---|
| High-concurrency static + reverse proxy on Linux VPS | nginx | Mature, low memory, vast docs and module ecosystem |
| Automatic HTTPS with minimal config | Caddy | Built-in ACME; simpler config for small projects |
| Apache modules (.htaccess, mod_php legacy) | Apache httpd | Shared hosting compatibility; heavier per-connection model |
| Service mesh, gRPC, advanced L7 routing | Envoy | Dynamic xDS config, observability hooks, sidecar pattern |
| Kubernetes ingress at scale | nginx Ingress / Envoy Gateway | CRD-driven routing integrated with cluster DNS |
| TCP/UDP passthrough load balancing | HAProxy or nginx stream module | L4 without terminating HTTP |
nginx remains the default choice for single-server and small-cluster deployments where operational simplicity and proven performance matter more than dynamic service discovery.
Common pitfalls
- Trailing slash in
proxy_pass— changes whether URI paths are rewritten; test both/apiand/api/. - Wrong
server_namecatch-all — default server accidentally serves the wrong site on shared IPs. - Missing forwarded headers — apps generate http:// links or wrong redirects behind TLS.
- Regex location order — broader regex shadows specific routes; use
^~prefix to stop search. - Client body size — default 1 MB
client_max_body_sizeblocks uploads; tune per route. - No upstream keepalive — new TCP connection per request adds latency to APIs.
- Reload without
nginx -t— broken config can prevent workers from starting. - Logging only status codes — without
$upstream_response_time, you cannot tell nginx from backend slowness.
Production checklist
worker_processes automatches CPU cores;worker_connectionssized for peak concurrency.- TLS 1.2+ only; cert auto-renewal tested (Certbot timer or equivalent).
- HTTP to HTTPS redirect on port 80.
proxy_set_headertrio: Host, X-Forwarded-For, X-Forwarded-Proto on all proxied routes.- Upstream keepalive enabled; timeouts set per route sensitivity.
- Gzip enabled for text; brotli or pre-compression for high-traffic static.
- Rate limits on auth and expensive public endpoints.
- Structured access logs shipped to aggregation; error log level
warnin prod. - Config under version control;
nginx -tin deploy pipeline before reload. - Security headers (optional module or app):
X-Content-Type-Options, frame options, HSTS when stable.
Key takeaways
- nginx uses an event-driven worker model to serve many concurrent connections efficiently.
- Config nests
http→server→location; matching rules determine routing. - Combine static serving, TLS termination, and reverse proxy on one edge node for typical VPS deploys.
- Upstream blocks enable load distribution and passive failover across app instances.
- Compression, edge cache, and rate limits belong at nginx before traffic hits application code.
Related reading
- Reverse proxy explained — TLS termination, path routing, WebSocket upgrades, and 502 debugging
- Load balancing explained — L4 vs L7 algorithms, health checks, and sticky sessions
- TLS and HTTPS explained — certificates, handshakes, and modern cipher configuration
- HTTP caching explained — Cache-Control, ETags, and CDN integration at the edge