The Nginx Configuration File: Common Directives and Typical Scenarios
Every Nginx capability — static sites, reverse proxying, load balancing, caching, rate limiting — is organized through a single configuration file. Knowing what the common directives do and where their pitfalls lie is the prerequisite for mastering Nginx. This post walks through “file structure → high-frequency directives → typical scenarios”, with key behaviors verified in containers.
Jump to your goal: for overall behavior read “The Configuration File Structure”, for concurrency read worker_connections, for reverse proxying read “Reverse Proxy”, for HTTPS read “SSL/TLS”, for multi-server distribution read “Load Balancing”, for speed read “Caching”, for hardening read “Security Hardening”.
The Configuration File Structure
The main configuration file usually lives at /etc/nginx/nginx.conf or /usr/local/nginx/conf/nginx.conf. Structurally it splits into the global block, the events block, and the http block:
1 | # Global block |
The global block governs overall behavior such as processes and logging, the events block governs connection handling, and the http block governs all HTTP-related configuration — in day-to-day work the http block is what you edit most.
The standard routine for a config change is a fixed pipeline: edit with vim nginx.conf → validate syntax with nginx -t → apply gracefully with nginx -s reload → verify behavior with curl or a browser. Only after all four steps is a change complete. Skipping the middle two steps and restarting directly produces a noticeably higher incident rate — every one of the syntax errors that nginx -t catches would otherwise make the reload fail and leave old and new configurations coexisting.
High-Frequency Directives
user
Defines the user that worker processes run as:
1 | user www; |
The master starts as root (it must bind the privileged ports 80/443), then drops workers down to this low-privilege user to do the work — even if a worker is fully compromised, the attacker only gets an ordinary user.
worker_processes and worker_connections
The two parameters that set the concurrency ceiling:
1 | worker_processes auto; # global block: number of workers, auto = number of CPU cores |
The service’s overall max concurrency ≈ number of workers × worker_connections. worker_processes auto lets Nginx probe the core count itself; worker_connections can be raised, but only if system resources keep up — the file descriptor limit (ulimit -n) and memory must both have headroom, otherwise you are just moving pressure from Nginx onto the operating system. Also note that connections are not requests: a browser typically keeps 2-6 connections open per site and reuses them; under keep-alive those connections hang around continuously, so capacity should be estimated by peak concurrent connections, not requests per second.
error_log and access_log
1 | error_log /var/log/nginx/error.log warn; |
Troubleshooting starts with error_log; traffic analysis runs on access_log. log_format defines the log fields, and the level is usually set to warn so the log does not balloon.
include
1 | include /etc/nginx/conf.d/*.conf; |
Split per-site configurations into separate files to keep the main config clean. Note that a single syntax error fails the entire reload — nginx -t also checks the included files.
Typical Scenarios
Reverse Proxy
Forward requests to a backend application while passing the original request information along:
1 | server { |
Those proxy_set_header lines cannot be skipped — without them, every client address the backend sees is Nginx itself (127.0.0.1 or an internal IP), and both logs and application-layer IP extraction become garbage. Confirmed by a live test (the backend echoes back the request headers it received via return):

Reading the image: there is no version number after Server: nginx (server_tokens off in effect; the default is Server: nginx/1.31.5); Content-Encoding: gzip shows compression is on; the last line is the backend service echoing the request headers it received — all four variables, Host, X-Real-IP, X-Forwarded-For, and X-Forwarded-Proto, made it through to the backend, so the passthrough chain works.
SSL/TLS and Forced Redirect
1 | server { |
The certificate and key paths must be correct, the Nginx process must have read permission, and the certificate chain must be complete — deploy only the leaf certificate and some clients will refuse to connect outright; concatenating the intermediate certificate into the crt file (cat domain.crt intermediate.crt > fullchain.crt) eliminates most of the “browser opens fine, command line reports a certificate error” troubleshooting. Two sources for certificates: public sites use Let’s Encrypt’s certbot to issue and renew, internal sites build their own CA or use internal certificates directly. The HSTS header forces browsers onto HTTPS for a year — once enabled it is hard to roll back quickly, so confirm HTTPS is stable before turning it on in production.
Load Balancing
1 | upstream backend { |
upstream defines the backend pool. The default is round-robin; use weight when backend performance is uneven. For session stickiness use ip_hash (same IP pinned to the same server) or least_conn (send to whichever has the fewest current connections), chosen by workload characteristics. proxy_next_upstream automatically re-routes requests that error or time out to the next server — simple, effective failover.
Caching
1 | proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=my_cache:10m |
proxy_cache_path must be declared first (cache storage path + key zone), and proxy_cache enables it inside a location. The X-Cache-Status response header tells you directly whether each request was a HIT (served from cache) or a MISS (fetched from origin) — when tuning the cache, look at it first. Live test hitting the same request twice (the backend responded with Cache-Control: public, max-age=60, which is cacheable):

The first was a MISS — the cache zone was still empty, the request penetrated to the backend, and the response was stored in the cache zone as it returned; the second was a HIT — the response came straight out of the cache zone and the backend never saw the request. When debugging “the data is stale but the backend is clearly fine” in production, the first step is exactly this header: HIT means the reader got precisely the copy stored in the cache.
Security Hardening
1 | server_tokens off; # keep the version number out of response headers |
Hiding the version number takes one line (visible in the test above); rate and connection limits per source IP are the basic defense against CC/DDoS attacks. Set the parameters loose rather than tight, then tighten after watching the logs.
Cheat Sheet
| Directive | Purpose | Common Values |
|---|---|---|
worker_processes |
number of worker processes | auto |
worker_connections |
max connections per worker | 1024~65535 |
include |
pull in sub-configs | conf.d/*.conf |
proxy_pass |
reverse proxy forwarding | http://backend |
proxy_set_header |
pass request headers to the backend | Host/X-Real-IP/X-Forwarded-* |
return 301 |
forced redirect | https://$host$request_uri |
server_tokens off |
hide the version number | fixed usage |
limit_req |
limit request rate | zone=one burst=5 |
proxy_cache |
enable response caching | paired with proxy_cache_path |
Caveats
- Validate syntax with
nginx -tbefore reloading a changed config;included files are checked too. aliaspaths must end with/; omit it and you get 404s.- A reverse proxy must carry the
proxy_set_headerfamily, or everything the backend receives about the client is wrong. - HSTS is hard to roll back quickly once enabled; confirm HTTPS is stable before going live.
- Keep rate and connection limit parameters loose rather than tight; tighten gradually while watching the logs.
This article was rewritten from the author’s CSDN blog posts originally published between 2020 and 2024.