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 for proxy_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:

  1. Static root/var/www/site/public holds HTML guides; nginx serves /guides/ and / directly.
  2. API proxylocation /api/settle/ forwards to 127.0.0.1:3847 with forwarded headers and 30s read timeout.
  3. Games path — separate upstream on port 3849 for game WebSocket + REST.
  4. TLS — single cert for the apex domain; HTTP redirects to HTTPS.
  5. Logging — custom log_format including $request_time and $upstream_response_time for latency triage.
  6. 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 /api and /api/.
  • Wrong server_name catch-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_size blocks 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 auto matches CPU cores; worker_connections sized for peak concurrency.
  • TLS 1.2+ only; cert auto-renewal tested (Certbot timer or equivalent).
  • HTTP to HTTPS redirect on port 80.
  • proxy_set_header trio: 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 warn in prod.
  • Config under version control; nginx -t in 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 httpserverlocation; 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