Skip to content

Nginx Configuration Essentials

120 companion flashcards · AI-assisted study content · Open the deck →

This deck walks you through the building blocks of nginx configuration, starting from what nginx actually stands for and how its process model works, all the way to specific directives that control performance and request handling. You'll get familiar with the main configuration file locations, the difference between similar commands like nginx -s stop and nginx -s quit, and the role of contexts such as http and server. The cards also cover commonly confused settings like alias versus root, and performance-oriented options such as sendfile, tcp_nopush, and tcp_nodelay.

The deck is a great fit if you're a system administrator, DevOps engineer, or web developer who needs to manage or troubleshoot an nginx-powered server. It's equally useful if you're preparing for a technical interview or certification, since the questions touch on both conceptual knowledge and practical commands you'd use day to day. If you're brand new to nginx, working through these cards will give you a solid mental map of how the configuration is organized before you dive into more advanced topics like reverse proxies or load balancing.

To get the most out of these flashcards, try to connect each concept to a real nginx configuration file on a test server. For example, after reviewing the worker_processes and worker_connections cards, open your nginx.conf and see how those values are set. Spacing your review sessions over several days works better than cramming, especially for the subtler distinctions like alias versus root or the difference between a graceful reload and a hard stop. Keep a terminal open alongside the deck so you can test syntax with nginx -t as soon as a card reminds you of the command.

Nginx Fundamentals and Process Model

Nginx — pronounced "engine x" — is a high-performance open-source HTTP server, reverse proxy, and IMAP/POP3 mail proxy originally written by Igor Sysoev and first released in 2004. It is built around a single master process that reads and validates the configuration, manages listening sockets, and spawns a configurable number of worker processes. By default the master creates one worker per CPU core, and every worker handles incoming connections using an event-driven, non-blocking loop. This architecture is what makes nginx able to serve tens of thousands of concurrent connections with very little memory compared to traditional process-per-request servers like Apache.

Because workers are long-lived, you can change the configuration or even upgrade the binary without dropping user connections. To do so, send SIGHUP to the master process (or run nginx -s reload): the master re-reads the configuration, starts new workers running the new settings, and gracefully drains the old workers once their existing requests finish. Two other shutdown signals are worth distinguishing. nginx -s stop sends SIGTERM and forces workers to exit immediately, closing any open connections. nginx -s quit sends SIGQUIT, which lets workers finish serving their current requests before exiting cleanly. Choosing between them is mostly about whether you can tolerate dropped sessions.

Before applying any change in production you can validate the syntax safely with nginx -t. This command parses the configuration files, includes, and referenced certificates without starting or reloading the server. The companion flag nginx -T goes one step further and dumps the fully expanded configuration to stdout, which is invaluable in CI/CD pipelines and when debugging complex include chains. On Linux the main configuration file lives at /etc/nginx/nginx.conf, with additional files typically loaded via include /etc/nginx/conf.d/*.conf; and per-site configs under /etc/nginx/sites-enabled/. Treat the configuration as a small, declarative program: write it, test it, then reload it.

Core Configuration Directives and Performance Knobs

At the top level the configuration file lives in the main context, and from there directives branch into several nested blocks. The most important ones are http {} for HTTP-wide settings, server {} for virtual hosts, location {} for URI-specific behaviour, upstream {} for backend pools, and events {} for connection-level tuning. The http and server blocks both accept performance-related directives that you will tune on any serious deployment.

Two directives define the scale of the worker pool. worker_processes auto; instructs nginx to spawn one worker per detected CPU core, which is the conventional starting point. Inside the events {} block, worker_connections sets the maximum number of simultaneous sockets each worker can keep open; a practical default is 1024, while high-traffic servers often push it to 4096 or 8192. The rough maximum number of concurrent clients the whole server can serve is approximately worker_processes × worker_connections, divided further by two when accounting for the fact that proxied connections consume one socket on the client side and one on the upstream side.

For static file delivery, sendfile on; enables the kernel's sendfile(2) zero-copy path, letting nginx push a file's contents from the page cache straight to the socket without copying through user space. Pair it with tcp_nopush on;, which sets TCP_CORK so response headers and the start of the body are coalesced into a single packet. tcp_nodelay on; does the opposite for keep-alive responses — it clears TCP_NODELAY so small packets go out immediately once the response is finished. Together, tcp_nopush on; for the first packet and tcp_nodelay on; for subsequent keep-alive traffic form the well-known "TCP_CORK / TCP_NODELAY trick" that maximises throughput without hurting latency on follow-up requests.

HTTP Routing: Servers, Locations, and Rewrites

The server {} block is nginx's virtual host. The server_name directive declares which hostnames the block responds to and supports exact names, leading wildcards (\*.example.com), trailing wildcards (www.example.\*), and regex names prefixed with ~. When a request arrives, nginx first picks the listen socket and then chooses the server with the best matching server_name. If no name matches, the block marked listen 80 default_server; (or listen 443 default_server; on TLS) handles the request — this is the conventional home for a catch-all redirect or a default site.

Inside a server, the location directive matches against the normalised request URI (after percent-decoding and stripping .. segments). Four kinds of match are available, in strict priority order: = exact match, ^~ preferential prefix, ~ and ~* regex (case-sensitive and case-insensitive, with the first defined winning), and finally the longest matching plain prefix. Understanding this order is essential: a location = /login always wins over a regex or prefix, which is exactly what you want for a high-traffic endpoint.

Once a location is selected, two directives determine where files come from. root appends the request URI to the configured path, so root /var/www; with a request for /img/x.png serves /var/www/img/x.png. alias replaces the matched location prefix with the alias path and requires the alias to end with a slash matching the location; with alias /var/www/; the same request is served as /var/www/img/x.png. The index directive defines the file nginx serves when a directory is requested (the default is index index.html;), and you can extend the list to index index.html index.htm index.php;. For paths that might not exist as files, try_files walks a list in order — try_files $uri $uri/ /index.php?$query_string; — and falls back to a final URI or named location when nothing matches. The special named location syntax (location @fallback { ... }) is reachable only through internal redirects, which makes it perfect for error handlers and SPA fallbacks.

For outright redirects or rejections, return sends an immediate HTTP response without contacting any upstream: return 301 https://example.com$request_uri; performs a permanent redirect, while return 302 ...; is the temporary variant. Use 301 only when the move is truly permanent, because browsers cache 301s aggressively and rollbacks become difficult. For more flexible URI manipulation, rewrite matches a regex against the URI and can capture groups, ending with a flag such as last, break, redirect, or permanent. The difference between last and break is subtle but important: last stops processing the current server or location and restarts matching against the rewritten URI, while break stops only the rewrite phase and continues serving the rewritten URI inside the same location. Used with try_files and return, these primitives cover almost every routing pattern you will encounter.

Reverse Proxying, Upstreams, and Backend Protocols

An upstream {} block defines a named group of backend servers that nginx will load-balance across. A typical example declares several server entries, optional weights, and a keepalive pool: upstream backend { server 10.0.0.1:8080; server 10.0.0.2:8080; keepalive 32; }. You then point a location at it with proxy_pass http://backend;. By default nginx uses round-robin; it also supports least_conn, ip_hash, and random out of the box. ip_hash hashes the client's IP to pick a backend, which keeps the same client on the same server — handy for stateful apps — but it cannot distinguish clients behind a shared NAT or proxy, and adding or removing a backend remaps most clients. For finer control, the server entries accept parameters such as weight=N to bias the share, max_fails and fail_timeout to mark a server down after consecutive errors, backup to use the server only when primaries are unavailable, and down to remove it from rotation during maintenance without deleting the line.

The proxy_pass directive has a subtle but critical behaviour tied to whether it ends with a slash. Without a trailing slash, the full original URI is forwarded to the upstream including the matched location prefix — so location /api/ { proxy_pass http://backend; } forwards /api/users as /api/users. With a trailing slash, the matched location prefix is replaced with /, and the same request becomes /users. Matching the trailing slash on the location to the slash on the upstream is therefore the usual recipe for clean URL translation. Beyond the URL, several headers must be set explicitly so the upstream sees the original client. proxy_set_header Host $host; preserves the virtual-host name; proxy_set_header X-Real-IP $remote_addr; and proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; forward the real client IP; and proxy_set_header X-Forwarded-Proto $scheme; tells the upstream whether the original request was HTTP or HTTPS.

Nginx speaks several backend protocols. proxy_pass handles plain HTTP and HTTPS upstreams. fastcgi_pass speaks the FastCGI protocol used by PHP-FPM and similar applications, and requires fastcgi_param mappings instead of proxy_set_header — the canonical PHP-FPM snippet binds fastcgi_pass to a Unix socket and supplies SCRIPT_FILENAME \(document_root\)fastcgi_script_name;. uwsgi_pass connects to a uWSGI application server, which is the standard way to serve Python WSGI apps. For microservices, grpc_pass routes gRPC traffic to a backend and requires HTTP/2 (TLS or cleartext h2c), which became stable in nginx 1.13.10. Regardless of protocol, several tunables control the connection: proxy_connect_timeout caps how long nginx waits to establish a TCP connection (default 60s), proxy_send_timeout caps how long it takes to send the request, and proxy_read_timeout caps the gap between successive reads from the upstream (default 60s) — the last of these is the one to bump for long-polling, Server-Sent Events, and WebSockets.

For body sizes, client_max_body_size defaults to 1m; raise it (for example to 50m) to allow large file uploads, otherwise nginx returns 413 Request Entity Too Large. client_body_buffer_size sets the in-memory buffer (default 8k), and anything larger spills to client_body_temp_path. Finally, proxy_buffering on; (the default) buffers the entire upstream response before forwarding to the client, which protects upstream from slow clients and enables proxy_cache. Setting it to off streams responses straight through with minimal buffering, which is the right choice for large media, Server-Sent Events, and WebSockets — combine it with add_header X-Accel-Buffering no; when the response is gated by an upstream you control.

Caching and Compression

Caching in nginx is configured in two steps: declare a cache zone on disk, then opt in from a location. The proxy_cache_path directive creates a managed cache directory tree with metadata stored in a shared memory zone — for example, proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=my_cache:10m max_size=10g inactive=60m use_temp_path=off;. In the location you then enable caching with proxy_cache my_cache; and define how long each response class lives via proxy_cache_valid 200 302 10m; and proxy_cache_valid 404 1m;. To see what is happening during debugging, add the X-Cache-Status header from $upstream_cache_status — values such as HIT, MISS, BYPASS, EXPIRED, STALE, UPDATING, and REVALIDATED tell you exactly why a given response came out of the cache or did not.

Two related directives let you carve holes in the cache. proxy_no_cache decides whether the response is saved at all, and proxy_cache_bypass decides whether to serve from cache if it is available or to fetch fresh from upstream. They are typically used together with cookie or query-string signals: proxy_cache_bypass $cookie_nocache $arg_nocache; proxy_no_cache $cookie_nocache $arg_nocache; ensures that logged-in users or requests tagged with ?nocache=1 skip the cache both ways. FastCGI caching follows the same pattern but uses fastcgi_cache_path, fastcgi_cache, fastcgi_cache_valid, and a fastcgi_cache_key. A typical WordPress key is fastcgi_cache_key "$scheme$request_method$host$request_uri", often extended with $cookie_logged_in so authenticated visitors bypass the cache automatically.

Compression is the other half of bandwidth reduction. gzip on; turns it on, gzip_types text/plain text/css application/json application/javascript text/xml; lists which MIME types to compress, and gzip_min_length 256; avoids wasting CPU on tiny responses. gzip_vary on; adds Vary: Accept-Encoding so intermediary caches store separate gzip and identity copies. When the response is already fingerprinted, you can precompress files at build time and serve them with gzip_static on;, which checks for file.js.gz alongside file.js. Brotli goes further but requires the nginx-module-brotli (or a fork that ships it built-in): enable it with brotli on; and a parallel brotli_types list, typically at brotli_comp_level 6;. Static-asset caching is handled by the expires directive: expires 30d; sets Cache-Control and Expires headers for general static files, while far-future caching for fingerprinted assets is usually written as expires 1y; combined with add_header Cache-Control "public, immutable";. For HTML you generally want the opposite — add_header Cache-Control "no-store, no-cache, must-revalidate"; with expires off;.

TLS, SSL, and Connection Hardening

TLS is mandatory for any production site. The minimum directives to enable it on a server block are listen 443 ssl http2;, ssl_certificate pointing at the PEM file, ssl_certificate_key at the private key, and ssl_protocols TLSv1.2 TLSv1.3;. The http2 keyword (or, in nginx versions before 1.25.1, the separate http2 on; directive) activates HTTP/2 over TLS, which is required for header compression and multiplexing. Older protocols — SSLv2, SSLv3, TLSv1.0, and TLSv1.1 — are all deprecated and should never appear in ssl_protocols; only TLSv1.2 and TLSv1.3 belong in a modern configuration.

Cipher selection and session reuse shape the actual handshake. A reasonable hardened cipher list is ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305;, paired with ssl_prefer_server_ciphers on; so the server's preference wins over the client's. ssl_dhparam is no longer required when you only allow ECDHE suites, but if you do use it, generate at least 2048 bits with openssl dhparam -out dhparam.pem 4096. To avoid the cost of repeated full handshakes, set ssl_session_cache shared:SSL:10m; — about 40 000 sessions per megabyte — and tune ssl_session_timeout (default 5m) to values like 1d for browsers or 4h for APIs.

OCSP stapling lets the server fetch and sign the certificate's revocation status itself, so clients do not need to contact the CA mid-handshake. Enable it with ssl_stapling on; ssl_stapling_verify on; resolver 8.8.8.8 1.1.1.1 valid=300s;. HSTS (HTTP Strict Transport Security) tells browsers to use HTTPS only for a given period: add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;. includeSubDomains extends the policy to every subdomain, so use it only after you have HTTPS on all of them; preload marks the site for inclusion in browsers' built-in HSTS preload list, which is hard to undo and requires a submission to hstspreload.org. The cleanest way to enforce HTTPS is a dedicated port-80 server that redirects every request: server { listen 80; server_name example.com www.example.com; return 301 https://$host$request_uri; }.

Certificates need to be renewed. Let's Encrypt issues 90-day certificates, and the standard tool is certbot: certbot --nginx -d example.com -d www.example.com edits the nginx configuration to serve the HTTP-01 challenge at /.well-known/acme-challenge/, obtains the certificate, and rewrites the HTTPS block. A daily cron with certbot renew --quiet --deploy-hook "systemctl reload nginx" keeps certificates fresh; after each renewal nginx is reloaded (or sent SIGHUP), which rotates workers and picks up the new certificate files without dropping connections — the same graceful pattern as any other configuration change.

Access Control, Rate Limiting, and Logging

Access control in nginx starts with the simple allow and deny directives, which act as an ACL matched against the client IP. A typical block permits an internal subnet and rejects everyone else: allow 10.0.0.0/8; deny all;. Denied requests receive a 403 Forbidden. For stronger authentication, HTTP Basic Auth uses auth_basic "Restricted"; alongside auth_basic_user_file /etc/nginx/.htpasswd;, with the password file generated by htpasswd -c /etc/nginx/.htpasswd alice. These primitives compose well with location matching, so you can lock down /admin while leaving the rest of the site open.

Rate limiting protects backends from abuse and is implemented as a two-step pattern. First declare a shared memory zone that counts requests keyed by some identifier, usually $binary_remote_addr: limit_req_zone $binary_remote_addr zone=one:10m rate=10r/s;. Then apply it inside a location with limit_req zone=one burst=20 nodelay;. burst is the queue size — how many requests may pile up — and nodelay tells nginx to serve queued requests immediately instead of spacing them out at the configured rate. A similar primitive, limit_conn, restricts concurrent connections per key (typically the client IP), which is the right tool for capping WebSocket or download-heavy clients. To protect a login endpoint specifically, combine a per-minute zone with a tight location match: limit_req_zone $binary_remote_addr zone=login:10m rate=5r/m; inside location = /login { limit_req zone=login burst=10 nodelay; ... }.

Logging gives you visibility into traffic and behaviour. The default access log format is "combined": $remote_addr - $remote_user [\(time_local] "\)request" $status $body_bytes_sent "\(http_referer" "\)http_user_agent". For more detail you can define a custom log_format that adds timing variables: $request_time measures total request processing time, while $upstream_response_time, $upstream_connect_time, and $upstream_header_time expose where latency is being spent — particularly useful when a backend is the bottleneck. To reduce syscall overhead on busy servers, open_log_file_cache max=1000 inactive=20s valid=1m min_uses=2; keeps file descriptors warm for frequently written log files. The error_log directive, default level error, captures runtime warnings and errors; raise it to info or debug when troubleshooting. Because debug is extremely noisy, pair it with debug_connection 1.2.3.4; to restrict verbose output to a single client IP, which lets you debug one request end-to-end without flooding your disks.

Advanced Routing, Variables, and Security Headers

Some patterns require more than straightforward location matching. For single-page applications using HTML5 history-mode routing, every request should fall through to the application's bootstrap file: location / { try_files $uri $uri/ /index.html; }. The canonical Vue or React snippet then adds a parallel location for fingerprinted assets with long-lived caching: location ~* \.(?:js|css|woff2?|png|jpg|svg)$ { expires 1y; add_header Cache-Control "public, immutable"; }. WebSockets are another special case: the proxy needs HTTP/1.1 with the Upgrade handshake preserved, which is achieved by proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; and a generous proxy_read_timeout 86400; so idle sockets are not closed at the default 60-second mark.

For more flexible conditionals, the if directive is powerful but famously tricky: the official wiki warns that if is "evil" when used outside a location block or when checking request headers. The safe pattern is to test only on built-in variables such as $http_x_custom, or to lift the logic out of if altogether using map and geo. The map directive builds a new variable by matching an input against patterns and is evaluated once per request: map $http_accept_language $lang { default en; ~^fr fr; }. The geo directive does the same thing keyed on the client IP, ideal for geolocation and coarse-grained allow/deny lists. When upstream hostnames must be resolved dynamically (for example, when targeting Consul- or Kubernetes-style service discovery), declare a resolver directive — resolver 1.1.1.1 8.8.8.8 valid=300s; — because nginx does not inherit the system resolver otherwise.

Two related features handle request-handling logic that should never be exposed to clients. A location marked internal; can be reached only via internal redirects (X-Accel-Redirect headers from upstream, error_page handlers, or rewrite ... last), and direct client requests return 404. The X-Accel-Redirect pattern is the canonical way to delegate access-controlled downloads to nginx: the upstream decides whether the user is authorised and returns X-Accel-Redirect: /protected/file.pdf, which nginx then serves from an internal location that enforces its own checks. For streaming large responses such as video or Server-Sent Events, disable buffering explicitly with proxy_buffering off; proxy_cache off; add_header X-Accel-Buffering no; chunked_transfer_encoding on; so nginx does not hold the whole response in memory before forwarding.

Security headers and minor hardening round out a production configuration. The standard set is X-Frame-Options (clickjacking protection, for example SAMEORIGIN or DENY), X-Content-Type-Options: nosniff (stops MIME sniffing), a Referrer-Policy such as strict-origin-when-cross-origin, a Content-Security-Policy appropriate to your application, and a Permissions-Policy. Always pass the always parameter so the headers are added to error responses too. Hidden files should never be reachable: location ~ /\. { deny all; return 404; } blocks /.git, /.env, and similar paths. To strip identifying headers from upstream responses, use proxy_hide_header X-Powered-By;, and to hide the nginx version itself use server_tokens off;. The headers-more module adds more_clear_headers, but it is not part of open-source nginx — the equivalent in vanilla nginx is proxy_hide_header per header name.

Frequently asked questions

What does nginx stand for?

engine x — a high-performance open-source HTTP server, reverse proxy, and IMAP/POP3 mail proxy originally written by Igor Sysoev and first released in 2004.

What does <code>tcp_nodelay</code> do?

Disables Nagle's algorithm (TCP_NODELAY) so small packets are sent immediately. With keepalive_timeout it enables the TCP_CORK / TCP_NODELAY trick for efficient keep-alive responses.

What is the classic Nginx + PHP-FPM configuration snippet?

location ~ \.php$ { fastcgi_pass unix:/run/php/php8.2-fpm.sock; fastcgi_index index.php; include fastcgi_params; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; }

What is <code>proxy_connect_timeout</code>?

Time allowed to establish a TCP connection to the upstream. Default is 60s. Example: proxy_connect_timeout 5s;

How do you set far-future caching for fingerprinted assets?

location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff2?)$ { expires 1y; add_header Cache-Control "public, immutable"; }

What is OCSP stapling and how do you enable it?

Tells nginx to fetch OCSP responses from the CA and staple them into the TLS handshake. Enable with ssl_stapling on; ssl_stapling_verify on; resolver 8.8.8.8 1.1.1.1 valid=300s;

What is <code>open_log_file_cache</code> used for?

Caches file descriptors and metadata for frequently written log files to reduce syscall overhead. Example: open_log_file_cache max=1000 inactive=20s valid=1m min_uses=2;

What is the purpose of <code>proxy_set_header Connection "";</code>?

Clears the Connection header sent to the upstream. Required when activating upstream keep-alive so the upstream does not see Connection: close.

How do you bypass the cache for a specific cookie or request?

proxy_cache_bypass $cookie_nocache $arg_nocache; proxy_no_cache $cookie_nocache $arg_nocache; This causes authenticated or tagged requests to skip the cache.

What is <code>resolver</code> used for?

Names the DNS server(s) nginx uses to resolve upstream hostnames (e.g. resolver 1.1.1.1 8.8.8.8 valid=300s;). Required for variables like proxy_pass http://my.service.consul:$server_port;.

Drill this topic

120 flashcards on Nginx Configuration Essentials — free, no signup needed to start.

Study Nginx Configuration Essentials flashcards

LearnWiki pages are generated with AI assistance from LearnCoachAssist's reviewed study catalog and may contain errors — verify anything critical against your course materials.