Web Servers & Reverse Proxies

Nginx Fundamentals

22 min Lesson 2 of 28

Nginx Fundamentals

Nginx (pronounced "engine-x") is the web server of choice at nearly every major internet company — serving as the front door for Netflix, Cloudflare, Dropbox, GitHub, and most of the world's highest-traffic sites. Understanding Nginx deeply, from installation to its internal configuration model, is a prerequisite for every production DevOps role. This lesson covers the full mental model: how to get Nginx running, how its configuration file is structured, and the two concepts you will configure hundreds of times in your career — server blocks and location blocks.

Installing Nginx

On modern Ubuntu/Debian systems, the recommended installation path is the official Nginx stable or mainline package from the Nginx APT repository rather than the distribution-default package. The distribution package often lags months behind in security and feature patches.

# --- Ubuntu 22.04 / 24.04 --- # 1. Add the official Nginx APT repository sudo apt install -y curl gnupg2 ca-certificates lsb-release ubuntu-keyring curl https://nginx.org/keys/nginx_signing.key | gpg --dearmor \ | sudo tee /usr/share/keyrings/nginx-archive-keyring.gpg >/dev/null echo "deb [signed-by=/usr/share/keyrings/nginx-archive-keyring.gpg] \ http://nginx.org/packages/ubuntu $(lsb_release -cs) nginx" \ | sudo tee /etc/apt/sources.list.d/nginx.list # 2. Pin the official repo over the distro package echo -e "Package: *\nPin: origin nginx.org\nPin-Priority: 900\n" \ | sudo tee /etc/apt/preferences.d/99nginx # 3. Install sudo apt update && sudo apt install -y nginx # 4. Enable and start sudo systemctl enable --now nginx # 5. Verify nginx -v # nginx/1.26.x (stable) or 1.27.x (mainline) systemctl status nginx curl -I http://localhost # HTTP/1.1 200 OK
Pro practice: Always use the official Nginx repository in production. The Ubuntu/Debian package is typically 6-12 months stale — it may be missing critical security patches (CVEs) and newer module APIs. Pin it as shown so an apt upgrade never accidentally downgrades back to the distro package.

The nginx.conf Anatomy

Nginx's entire behavior is driven by a single structured configuration language. The file at /etc/nginx/nginx.conf is the root. Understanding its layered context model is the most important conceptual step.

Configuration is organized into contexts (blocks delimited by { }), and directives in a context apply to that context and all contexts nested within it. There are four you will use constantly:

  • main — the global context (no surrounding braces). Controls worker processes, PID file, error log, and module loading. Applies to the entire process.
  • events — tuning for the connection-processing model. Key directive: worker_connections.
  • http — everything related to HTTP/HTTPS. Contains server blocks and sets HTTP-wide defaults (MIME types, logging, compression, timeouts, upstream pools).
  • server — a virtual host (equivalent to Apache's VirtualHost). Lives inside http. You will have one per domain (or per port).
  • location — matches a URL path pattern inside a server block. This is where most per-route logic lives.
Nginx configuration context hierarchy main context (nginx.conf) worker_processes auto; error_log /var/log/nginx/error.log warn; pid /run/nginx.pid; events { } worker_connections 1024; use epoll; http { } access_log ...; gzip on; include mime.types; sendfile on; server { } — example.com listen 443 ssl; server_name example.com; location /api proxy_pass http://app:3000 location / root /var/www; try_files server { } — api.example.com listen 443 ssl; server_name api.example.com; location /v1 proxy_pass http://api:8080 location /health return 200 OK; access_log off;
Nginx configuration context hierarchy: main wraps events and http; http contains server blocks (virtual hosts); each server contains location blocks that match URL paths.

The canonical starting nginx.conf that top companies use in production is much leaner than the default Nginx ships with. Start with this skeleton:

# /etc/nginx/nginx.conf — production baseline user nginx; worker_processes auto; # One worker per CPU core error_log /var/log/nginx/error.log warn; pid /run/nginx.pid; events { worker_connections 1024; # Max concurrent connections per worker use epoll; # Linux kernel event model (fastest on Linux) multi_accept on; # Accept as many new connections as possible per wake } 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; # Use kernel sendfile() — zero-copy for static files tcp_nopush on; # Batch TCP packets (works with sendfile) tcp_nodelay on; # Disable Nagle for keep-alive connections keepalive_timeout 65; gzip on; gzip_types text/plain text/css application/json application/javascript text/xml application/xml image/svg+xml; include /etc/nginx/conf.d/*.conf; # Per-site configs live here }
Key idea: worker_processes auto makes Nginx spawn exactly one worker per logical CPU core. Each worker is single-threaded and handles thousands of connections asynchronously using the epoll (Linux) or kqueue (macOS/BSD) event loop. This is fundamentally different from Apache's thread-per-connection model and is the reason Nginx uses a fraction of the memory under high concurrency.

Server Blocks — Virtual Hosting

A server block is Nginx's unit of virtual hosting. Each block answers requests for a specific combination of IP address, port, and Host header (the server_name). When a request arrives, Nginx selects the correct server block through a specific matching algorithm:

  1. Match the listen address and port exactly.
  2. Among matching blocks, find one whose server_name matches the Host header exactly.
  3. If no exact match, check for a leading wildcard (*.example.com), then a trailing wildcard.
  4. If still no match, use regex server names in order of definition.
  5. Fall back to the default_server on that port.

In production you will always explicitly mark one server block as default_server on port 80 and 443, and that block should return 444 (Nginx closes the connection without a response) for unrecognized hostnames — this prevents scanners from fingerprinting your backend by hitting you by IP.

# /etc/nginx/conf.d/default.conf — catch-all, rejects IP-direct requests server { listen 80 default_server; listen [::]:80 default_server; server_name _; # Matches anything not claimed by another block return 444; # Silent close — no HTTP response sent } # /etc/nginx/conf.d/example.com.conf — real virtual host server { listen 80; listen [::]:80; server_name example.com www.example.com; # Redirect all HTTP to HTTPS (single 301, no redirect loop risk) return 301 https://example.com$request_uri; } server { listen 443 ssl; listen [::]:443 ssl; http2 on; # Enable HTTP/2 (Nginx 1.25.1+) server_name example.com www.example.com; ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem; root /var/www/example.com/public; index index.html index.php; # All location logic goes here include /etc/nginx/snippets/security-headers.conf; }
Production pitfall: Never omit the default_server catch-all block. Without it, the first server block defined on a port becomes the implicit default. This means a scanner hitting your server by IP address will get a response from your first virtual host — potentially leaking its domain name, TLS certificate, or backend technology stack. Always define an explicit return 444 catch-all.

Location Blocks — URL Routing

Inside a server block, location blocks route requests to different handlers based on the URL path. Nginx uses a specific precedence algorithm to select the winning location — getting this wrong is one of the most common production configuration bugs.

Location modifiers (in order of precedence):

  • = /exact — exact string match; highest priority, stops search immediately.
  • ^~ /prefix — prefix match that wins over any regex; if matched, stops regex search.
  • ~ pattern — case-sensitive regex (first match wins among regexes).
  • ~* pattern — case-insensitive regex.
  • /prefix — longest prefix match (no modifier); lowest priority, used only if no regex matches.
# /etc/nginx/conf.d/example.com.conf — location block examples server { listen 443 ssl; server_name example.com; root /var/www/example.com/public; # 1. Exact match — fastest, stops all further matching location = /favicon.ico { access_log off; log_not_found off; expires 30d; } # 2. Prefix match wins over regex — static assets bypass PHP location ^~ /assets/ { expires 1y; add_header Cache-Control "public, immutable"; access_log off; } # 3. Case-insensitive regex — PHP files location ~* \.php$ { fastcgi_pass unix:/run/php/php8.3-fpm.sock; fastcgi_index index.php; include fastcgi_params; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; } # 4. Longest prefix fallback — catch-all for SPA or PHP front-controller location / { try_files $uri $uri/ /index.php?$query_string; } # 5. API proxy — prefix match, delegates to upstream app location /api/ { proxy_pass http://127.0.0.1:3000/; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; 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; } }
Key idea — try_files: The directive try_files $uri $uri/ /index.php?$query_string is the foundation of every Laravel, Symfony, or single-page app deployment. Nginx first checks if the URI maps to a real file, then a real directory, and only falls through to the PHP front-controller when neither exists. This means static files are served by Nginx's C code directly — PHP never wakes up for a .js or .css request.

Testing and Reloading Configuration

Always test configuration before applying it. A syntax error in a live nginx.conf will cause nginx -s reload to refuse the change — but an error introduced while the process is down will prevent startup entirely.

# Test config syntax (dry-run, does NOT apply) sudo nginx -t # Expected: nginx: configuration file /etc/nginx/nginx.conf syntax is ok # nginx: configuration file /etc/nginx/nginx.conf test is successful # Test and dump the full merged config (useful for debugging include files) sudo nginx -T | grep -A5 "server_name" # Reload without dropping connections (sends SIGHUP to master process) sudo nginx -s reload # OR via systemd (preferred — integrates with service supervision) sudo systemctl reload nginx # Full restart (drops active connections — use only if reload is insufficient) sudo systemctl restart nginx # Print the compiled-in defaults and module list nginx -V 2>&1 | tr -- '-' '\n' | head -20
Pro practice: In CI/CD pipelines that deploy Nginx config changes, always run nginx -t as a pre-flight check before the pipeline does anything destructive. A failed config test should abort the deploy. At companies like Cloudflare, Nginx config changes go through the same code-review and automated validation pipeline as application code — a bad config that reaches production is a P0 incident.

How Nginx Selects a Server Block and Location: A Request Walk-Through

Tracing a single request through the Nginx decision tree builds the mental model you need to debug production issues quickly. Given a request for https://example.com/api/users?page=2:

  1. The TCP connection arrives on port 443. Nginx looks at all server blocks with listen 443.
  2. The TLS handshake happens; SNI provides the hostname example.com.
  3. Nginx scans server_name directives: exact match wins over wildcard.
  4. Inside the selected server block, Nginx evaluates all location blocks. The path is /api/users. There is no exact match (= /api/users), no ^~ prefix, and no matching regex. The longest prefix match is /api/.
  5. The /api/ location proxies to the upstream application server.
  6. Nginx sets proxy headers (X-Forwarded-For, X-Real-IP, Host) and forwards the request over an internal TCP or Unix socket connection.
  7. The upstream response returns; Nginx streams it to the client and logs the request.
Common failure mode — trailing slash in proxy_pass: proxy_pass http://backend:3000/ (with trailing slash) strips the location prefix from the URI before forwarding. proxy_pass http://backend:3000 (without) preserves it. A request to /api/users with location /api/ goes to http://backend:3000/users (trailing slash) or http://backend:3000/api/users (no slash). This mismatch causes 404s that are very hard to spot without reading the upstream access logs.

Key Variables and Built-In Directives

Nginx exposes dozens of variables in location context. The ones you will use constantly:

  • $uri — the current normalized request URI (without query string).
  • $args / $query_string — the query string portion.
  • $request_uri — the full original URI including query string (not normalized).
  • $remote_addr — client IP (but this is the load balancer IP behind a proxy; use $http_x_forwarded_for after trust validation).
  • $host — value of the Host header, or server name if missing.
  • $schemehttp or https.
  • $document_root — the root directive value for the current location.

By the end of this lesson you should be able to install Nginx from the official repository, read and write a well-structured nginx.conf, create virtual hosts with appropriate server blocks, and route requests through location blocks using the correct modifier for each use case. The next lesson builds on this to cover serving static files, PHP-FPM integration, and application backends in detail.