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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# Global block
user www;
worker_processes 1;

error_log /var/log/nginx/error.log;
pid /run/nginx.pid;

# events block
events {
worker_connections 1024;
}

# http block
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;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;

include /etc/nginx/conf.d/*.conf;
}

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
2
3
4
worker_processes auto;        # global block: number of workers, auto = number of CPU cores
events {
worker_connections 1024; # events block: max connections per worker
}

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
2
error_log /var/log/nginx/error.log warn;
access_log /var/log/nginx/access.log main;

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
2
3
4
5
6
7
8
9
10
11
12
server {
listen 80;
server_name myapp.example.com;

location / {
proxy_pass http://localhost:5000;
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;
}
}

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):

Reverse proxy passthrough test

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
2
3
4
5
6
7
8
9
10
11
12
13
server {
listen 443 ssl;
server_name example.com;
ssl_certificate /etc/nginx/ssl/example.com.crt;
ssl_certificate_key /etc/nginx/ssl/example.com.key;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
}

server {
listen 80;
server_name example.com;
return 301 https://$host$request_uri; # inside the port-80 server block
}

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
2
3
4
5
6
7
8
9
10
11
upstream backend {
server backend1.example.com;
server backend2.example.com weight=2;
}

server {
location / {
proxy_pass http://backend;
proxy_next_upstream error timeout http_502 http_503 http_504;
}
}

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
2
3
4
5
6
7
8
proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=my_cache:10m
max_size=1g inactive=60m use_temp_path=off;

location / {
proxy_cache my_cache;
proxy_pass http://backend;
add_header X-Cache-Status $upstream_cache_status;
}

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):

Cache hit test

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
2
3
4
5
server_tokens off;    # keep the version number out of response headers

limit_req_zone $binary_remote_addr zone=one:10m rate=1r/s;
limit_conn_zone $binary_remote_addr zone=addr:10m;
# inside a location: limit_req zone=one burst=5; limit_conn addr 10;

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 -t before reloading a changed config; included files are checked too.
  • alias paths must end with /; omit it and you get 404s.
  • A reverse proxy must carry the proxy_set_header family, 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.