Level: Beginner to intermediate Description: NGINX as the Swiss Army knife of web traffic and protocols — static file server, reverse proxy, load balancer, cache, TLS termination, and request rewriting.
Table of Contents
- Course Overview
- Installing NGINX and Serving Static Files
- Reverse Proxy
- Load Balancing
- Caching, Buffering, and Proxy Headers
- HTTPS: TLS Termination
- Request and Response Rewriting
- Architecture Diagrams
- Reference Tables
- Useful Commands
1. Course Overview
NGINX is often described as the Swiss Army knife of web traffic. In a single binary, it can take on multiple roles:
| Role | Description |
|---|---|
| Static file server | Serves HTML, CSS, images directly from disk |
| Reverse proxy | Forwards incoming requests to one or more back-ends |
| Load balancer | Distributes traffic across multiple service instances |
| Cache | Keeps a local copy of responses to reduce back-end load |
| TLS termination | Decrypts incoming HTTPS traffic, communicates with back-ends over plain HTTP |
| Request/Response rewriter | Modifies URLs, headers, and response bodies |
NGINX’s event-driven model (non-blocking event loop) allows it to handle tens of thousands of simultaneous connections with a very small memory footprint.
2. Installing NGINX and Serving Static Files
2.1 Official Docker Image
The fastest way to get started:
# Start NGINX on local port 8080 → container port 80
docker run -p 8080:80 nginx
# Verify
curl http://localhost:8080
# → "Welcome to nginx!"
# Stop with Ctrl+C
Advantages of the Docker approach:
- Starts in less than a second.
- Logs (access log + error log) are piped directly to
stdout/stderrof the container — no need to look for files. - Complete isolation, clean teardown with
docker compose down.
2.2 Installation on Ubuntu / WSL
# Dependencies
sudo apt-get update
sudo apt-get install -y curl gnupg2 ca-certificates lsb-release debian-archive-keyring
# Import the official NGINX signing key
curl https://nginx.org/keys/nginx_signing.key | gpg --dearmor \
| sudo tee /usr/share/keyrings/nginx-archive-keyring.gpg >/dev/null
# Add the official stable repository
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
sudo apt-get update
sudo apt-get install -y nginx
# Check version
nginx -v # 1.25.x
nginx -V # version + compiled modules
# Start
sudo nginx
# or
sudo service nginx start
sudo service nginx status
sudo service nginx stop
Tip: The official Ubuntu repository may contain an older version (1.18). For the latest version, always use the official
nginx.orgrepository.
2.3 Installation on macOS with Homebrew
brew install nginx
# Start (also registers the service at login)
brew services start nginx
# Test
curl http://localhost:8080 # default port 8080 on macOS
# Stop
brew services stop nginx
Configuration paths on macOS differ from Linux. Find the main file with
brew info nginx.
2.4 Architecture: master process and worker processes
# View NGINX processes in tree form
ps aux -f | grep nginx
root master process /usr/sbin/nginx -c /etc/nginx/nginx.conf
nginx worker process
nginx worker process
nginx worker process
...
- Master process: reads configuration, handles signals, maintains worker processes.
- Worker processes: actually process connections. By default,
worker_processes autocreates one worker per CPU core. - Event-driven, non-blocking model: each worker handles thousands of simultaneous connections via an event loop.
2.5 Anatomy of nginx.conf
cat /etc/nginx/nginx.conf
# or with syntax highlighting
bat /etc/nginx/nginx.conf
Typical structure:
# MAIN context (global)
worker_processes auto;
error_log /var/log/nginx/error.log notice;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
http {
# HTTP context — applies to all HTTP traffic
include /etc/nginx/mime.types;
default_type application/octet-stream;
access_log /var/log/nginx/access.log main;
sendfile on;
keepalive_timeout 65;
# Include additional config files
include /etc/nginx/conf.d/*.conf;
}
The file /etc/nginx/conf.d/default.conf contains the default server block:
server {
listen 80;
server_name localhost;
location / {
root /usr/share/nginx/html;
index index.html index.htm;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}
2.6 Simple directives vs blocks
| Type | Example | Description |
|---|---|---|
| Simple directive | worker_processes auto; | One line terminated by ; |
| Block directive | http { ... } | Groups other directives in { } |
| Context | main, http, server, location, stream, mail | Context determines where a directive applies |
Context hierarchy:
main
└── events
└── http
└── server
└── location
└── stream
└── server
└── mail
└── server
2.7 Serving static files
Minimal configuration example to serve a static site:
http {
include mime.types;
default_type application/octet-stream;
sendfile on;
server {
listen 80;
server_name localhost;
# Serve all files from /var/www/html
location / {
root /var/www/html;
index index.html index.htm;
}
# Custom 404 page
error_page 404 /404.html;
# Directory listing (disabled by default)
# autoindex on;
}
}
Key directives for static files:
| Directive | Example | Role |
|---|---|---|
root | root /var/www/html; | Root directory for files |
index | index index.html; | Default file |
error_page | error_page 404 /404.html; | Custom error page |
autoindex | autoindex on; | Enable directory listing |
try_files | try_files $uri $uri/ =404; | Look for file, then folder, else 404 |
2.8 Hot configuration reload
NGINX supports live reload without interrupting existing connections:
# From the host
sudo nginx -s reload
# In a Docker container
docker exec <container_name> nginx -s reload
# With systemd
sudo systemctl reload nginx
# Other signals
nginx -s stop # immediate stop
nginx -s quit # graceful stop (waits for connections to finish)
nginx -s reopen # reopen log files
Best practice: Always validate the config before reloading:
nginx -t # configuration test
3. Reverse Proxy
A reverse proxy receives client requests and forwards them to internal back-ends. Clients do not know the back-ends directly.
3.1 The proxy_pass directive
server {
listen 8080;
server_name localhost;
location / {
# Forward all requests to the Ghost back-end
proxy_pass http://ghost:2368;
}
}
proxy_passreplacesroot— we no longer serve a file, we forward.- The name
ghostis resolved by DNS (Docker Compose automatically creates DNS entries for each service). - The
locationpath is preserved in the transmitted request, unless a trailing slash is used inproxy_pass.
Complete example with headers:
server {
listen 80;
server_name localhost;
location / {
proxy_pass http://backend:3000;
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;
}
}
3.2 Multiple server blocks
Multiple server blocks allow routing traffic by port or hostname:
# Port 8080 → Ghost blog
server {
listen 8080;
location / {
proxy_pass http://ghost:2368;
}
}
# Port 8081 → "Hello" service
server {
listen 8081;
location / {
proxy_pass http://hello:5678;
}
}
# Port 8081, domain mailhog.com → MailHog UI
server {
listen 8081;
server_name mailhog.com;
location / {
proxy_pass http://email:8025;
}
}
3.3 The Host header and server block selection
When multiple server blocks listen on the same port, NGINX uses the Host header of the HTTP request to choose which one to apply:
# Test without modifying /etc/hosts
curl -H "Host: mailhog.com" http://localhost:8081/
# With verbose curl to see headers
curl -v http://localhost:8081/
Server block selection priority:
- Exact match of
server_name - Wildcard match (
*.example.com) - Regex match (
~^www\.(.+)\.com$) default_server(or first server block if absent)
3.4 Proxying TCP/UDP with the stream module
The stream module allows proxying non-HTTP protocols (SMTP, MySQL, Redis, etc.):
# Outside the http{} block
stream {
server {
listen 2525; # Entry port (custom SMTP)
proxy_pass email:1025; # MailHog SMTP back-end
}
# Example: MySQL proxy
server {
listen 3306;
proxy_pass db-server:3306;
}
}
Note: The
streamblock is at the same level ashttpin the configuration, not nested inside it.
Test with telnet to send an email via the SMTP proxy:
telnet localhost 2525
# → 220 mailhog.example ESMTP
HELO test
MAIL FROM:<alex@foo.com>
RCPT TO:<test@example.com>
DATA
Subject: Test via NGINX proxy
Hi from telnet!
.
QUIT
4. Load Balancing
4.1 The upstream directive
The upstream directive defines a group of back-ends to which NGINX distributes traffic:
http {
upstream echoz {
server echo1:8080;
server echo2:8080;
server echo3:8080;
}
server {
listen 80;
location / {
proxy_pass http://echoz; # References the upstream group
}
}
}
4.2 Load balancing algorithms
| Algorithm | Directive | Description |
|---|---|---|
| Round Robin | (default) | Cycle sequentially through servers |
| Weighted Round Robin | weight=N | Distribute proportionally by weights |
| Least Connections | least_conn; | Send to server with fewest active connections |
| IP Hash | ip_hash; | Map each client IP to a fixed server (sticky sessions) |
| Generic Hash | hash $request_uri; | Hash based on an NGINX variable |
| Random | random; | Random selection |
upstream backend_pool {
# Round robin (default) — no directive needed
# Least connections
least_conn;
server app1:3000;
server app2:3000;
server app3:3000;
}
upstream sticky_pool {
# IP Hash — sticky sessions
ip_hash;
server app1:3000;
server app2:3000;
}
4.3 Weights and backup servers
upstream weighted_pool {
server app1:3000; # weight = 1 (default)
server app2:3000 weight=5; # receives 5x more traffic
server app3:3000; # weight = 1
# app2 receives 5/7 of requests
}
upstream with_backup {
server primary1:3000;
server primary2:3000;
server standby:3000 backup; # only intervenes if primaries are down
}
4.4 Passive health checks
NGINX (open source) performs passive health checks: it monitors back-end responses and marks them unavailable on failure.
upstream resilient_pool {
server app1:3000 max_fails=3 fail_timeout=30s;
server app2:3000 max_fails=3 fail_timeout=30s;
server app3:3000 max_fails=3 fail_timeout=30s;
}
| Parameter | Default | Description |
|---|---|---|
max_fails | 1 | Number of consecutive failures before marking as down |
fail_timeout | 10s | Duration the server stays marked as down |
NGINX Plus offers active health checks that proactively test back-ends without waiting for actual errors.
4.5 Modifying headers during proxying
NGINX automatically modifies certain headers in proxy mode:
| Header | NGINX behavior |
|---|---|
Host | Replaced by $proxy_host (back-end name) |
Connection | Replaced by close |
X-Real-IP | Not added by default |
X-Forwarded-For | Not added by default |
To inspect headers on the server side:
# verbose curl
curl -v http://localhost/
# httpie (recommended tool, more readable)
http -v http://localhost/headers
# Compare direct vs proxied with icdiff
icdiff <(http localhost:5000/headers) <(http localhost:80/headers)
5. Caching, Buffering, and Proxy Headers
5.1 Adding custom headers: X-Real-IP
location / {
proxy_pass http://backend:3000;
# Forward the real client IP to the back-end
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $host;
}
5.2 Fixing a proxied WebSocket
By default, NGINX does not forward the upgrade headers required for WebSockets. Without this fix, the WebSocket fails on the client side (status “disconnected”):
location / {
proxy_pass http://app:3000;
proxy_http_version 1.1;
# Headers required for WebSocket upgrade
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
5.3 Buffering
By default, buffering is enabled: NGINX waits for the complete response from the back-end before transmitting it to the client.
location / {
proxy_pass http://backend:3000;
# Disable buffering (useful for streaming, SSE, long polling)
proxy_buffering off;
}
# Selective disabling for a specific endpoint
location /stream {
proxy_pass http://backend:3000;
proxy_buffering off; # No buffer for this endpoint
}
location / {
proxy_pass http://backend:3000;
# proxy_buffering on by default — with cache
}
When to disable buffering?
- Long polling / Server-Sent Events (SSE)
- Streaming large files
- Incremental real-time responses
When to keep buffering enabled?
- Caching (required)
- Slow clients (NGINX can receive the complete response and transmit it progressively)
5.4 Transparent caching
http {
# Define cache zone (outside server/location blocks)
proxy_cache_path /var/cache/nginx
levels=1:2
keys_zone=mycache:10m
max_size=1g
inactive=60m
use_temp_path=off;
server {
listen 80;
location / {
proxy_pass http://backend:3000;
# Enable cache for this location
proxy_cache mycache;
proxy_cache_valid 200 302 10m;
proxy_cache_valid 404 1m;
proxy_cache_min_uses 2; # Cache after 2 identical requests
proxy_cache_use_stale error timeout updating;
# Add a header to see cache status
add_header X-Cache-Status $upstream_cache_status;
}
}
}
proxy_cache_path parameters:
| Parameter | Description |
|---|---|
levels=1:2 | Cache subdirectory structure |
keys_zone=name:size | Zone name and hash table size in memory |
max_size | Maximum cache size on disk |
inactive | Purge entries not accessed after this delay |
5.5 Logging cache hits and misses
http {
# Custom log format for cache
log_format cache_log '$request $upstream_cache_status';
access_log /var/log/nginx/cache.log cache_log;
server {
# ...
}
}
$upstream_cache_status values:
| Value | Meaning |
|---|---|
HIT | Response served from cache |
MISS | Response not found in cache, back-end consulted |
EXPIRED | Entry expired, back-end re-consulted |
BYPASS | Cache bypassed (proxy_cache_bypass) |
REVALIDATED | Response revalidated with back-end |
UPDATING | Stale response being updated |
# Monitor the cache log in real time
tail -f /var/log/nginx/cache.log
6. HTTPS: TLS Termination
TLS termination centralizes SSL/TLS certificate management in NGINX. Back-ends communicate over plain HTTP, simplifying their configuration.
Client ←─── HTTPS ───→ NGINX ←─── HTTP ───→ Back-end(s)
(encrypted) (internal network)
6.1 Generating a test certificate with mkcert
# Install mkcert
# macOS
brew install mkcert
# Windows
winget install FiloSottile.mkcert
# or
choco install mkcert
# Linux
apt install mkcert
# Create a local CA and install it
mkcert -install
# Generate a certificate for multiple domains
mkcert localhost example.com example.net 127.0.0.1
# Check the CA directory
mkcert -CAROOT
This generates two files:
localhost+3.pem→ certificatelocalhost+3-key.pem→ private key
6.2 Configuring HTTPS on port 443
server {
listen 80;
listen 443 ssl;
server_name localhost example.com example.net;
# Certificate and private key
ssl_certificate /certs/cert.pem;
ssl_certificate_key /certs/key.pem;
# Recommended SSL parameters
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
location / {
proxy_pass http://backend:3000;
}
}
Mounting certificates in Docker Compose:
services:
nginx:
image: nginx:latest
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
- ./certs:/certs:ro # Certs accessible inside the container
6.3 Enabling HTTP/2
server {
listen 443 ssl;
# Enable HTTP/2 (modern syntax — do not use http2 parameter in listen)
http2 on;
ssl_certificate /certs/cert.pem;
ssl_certificate_key /certs/key.pem;
# ...
}
Note: The syntax
listen 443 ssl http2;is deprecated. Use thehttp2 on;directive instead.
SSL verification tools:
# certigo — check the certificate
certigo connect localhost:443
# sslyze — TLS security audit
sslyze localhost
# testssl.sh — complete audit with color coding (green = good, red = problem)
testssl localhost
6.4 Securing multiple domains with server_name
# Server 1: echo service
server {
listen 443 ssl;
http2 on;
server_name example.org;
ssl_certificate /certs/cert.pem;
ssl_certificate_key /certs/key.pem;
location / {
proxy_pass http://echo:8080;
}
}
# Server 2: MailHog UI
server {
listen 443 ssl;
http2 on;
server_name example.com;
ssl_certificate /certs/cert.pem;
ssl_certificate_key /certs/key.pem;
location / {
proxy_pass http://mailhog:8025;
}
}
If two server blocks on the same port have no
server_name, NGINX emits a warning and ignores the second one.
6.5 HTTP → HTTPS redirect
# Dedicated block for HTTP → HTTPS redirect
server {
listen 80 default_server;
server_name _; # Matches all domains
# Permanent 301 redirect to HTTPS
return 301 https://$host$request_uri;
}
# Separate HTTPS blocks
server {
listen 443 ssl;
http2 on;
server_name example.com;
ssl_certificate /certs/cert.pem;
ssl_certificate_key /certs/key.pem;
location / {
proxy_pass http://backend:3000;
}
}
Test the redirect:
curl -v http://localhost/
# → HTTP/1.1 301 Moved Permanently
# → Location: https://localhost/
7. Request and Response Rewriting
7.1 The return directive
return generates an immediate response without consulting the back-end or file system. Useful for redirects and debugging:
# Return an NGINX variable for debugging
location /inspect {
default_type text/plain;
return 200 "URI: $request_uri\nHost: $host\nRemote: $remote_addr";
}
# Permanent redirect
location /old-page {
return 301 /new-page;
}
# Temporary redirect
location /promo {
return 302 https://promo.example.com;
}
# Return an error code
location /forbidden {
return 403;
}
Common HTTP codes with return:
| Code | Meaning |
|---|---|
200 | OK — return a body |
301 | Moved Permanently — permanent redirect |
302 | Found — temporary redirect |
307 | Temporary Redirect — preserves HTTP method |
403 | Forbidden |
404 | Not Found |
7.2 Redirecting URLs with rewrite
# Syntax: rewrite regex replacement [flag];
# Simple rewrite: /old/path → /path
rewrite ^/old/(.*)$ /$1 last;
# Redirect JPGs to a single image
location ~ \.jpg$ {
return 301 /nginx.png;
}
# Rewrite with capturing regex
rewrite ^/articles/(\d{4})/(\d{2})/(.+)$ /blog/$1-$2/$3 permanent;
Flags for the rewrite directive:
| Flag | Description |
|---|---|
last | Stops rewrite processing in the current block, restarts location search |
break | Stops rewrite processing, continues in the current block |
redirect | Returns a temporary 302 |
permanent | Returns a permanent 301 |
Debugging rewrites:
# Enable debug logs
error_log /var/log/nginx/error.log debug;
Or use nginx-debug (binary with debug enabled at compile time):
# docker-compose.yml
services:
nginx:
command: [nginx-debug, '-g', 'daemon off;']
7.3 A/B Testing with split_clients
http {
# Define the a_or_b variable based on a timestamp hash (pseudo-random)
split_clients "${date_gmt}" $a_or_b {
50% siteA; # 50% of requests → siteA
* siteB; # the remaining 50% → siteB
}
server {
listen 80;
location /site {
# Redirect based on the variable value
return 301 /$a_or_b;
}
location /siteA {
proxy_pass http://version-stable:3000;
}
location /siteB {
proxy_pass http://version-canary:3000;
}
}
}
split_clients can be based on any NGINX variable: $remote_addr, $cookie_uid, $request_uri, etc.
7.4 Modifying response body with sub_filter
location / {
proxy_pass http://backend:3000;
# Replace all occurrences of "http://" with "https://"
sub_filter 'http://' 'https://';
sub_filter_once off; # Replace all occurrences (not just the first)
# Update accepted content types for filtering
sub_filter_types text/html text/css application/javascript;
}
Practical example — fix Twitter links from HTTP to HTTPS:
sub_filter 'href="http://twitter.com' 'href="https://twitter.com';
sub_filter_once off;
Limitation:
sub_filteronly works on uncompressed content. Disablegzipor usegunzip on;if necessary.
7.5 Gzip compression
http {
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6; # 1 (fast) to 9 (maximum)
gzip_buffers 16 8k;
gzip_http_version 1.1;
# Content types to compress
gzip_types
text/plain
text/css
text/javascript
application/javascript
application/json
application/xml
image/svg+xml;
server { ... }
}
Test with curl:
# Without compression (no Accept-Encoding)
curl http://localhost/ -o - | wc -c
# With gzip compression
curl -H "Accept-Encoding: gzip" http://localhost/ -o - | gunzip | wc -c
The compressed response will have the header: Content-Encoding: gzip
7.6 Directive inheritance
An important principle in NGINX: a directive at a lower level overrides all directives of the same type at higher levels.
http {
add_header X-HTTP "1"; # HTTP level
server {
add_header X-SERVER "1"; # Server level — overrides X-HTTP for this server
location /inspect {
add_header X-INSPECT "1"; # Location level — overrides X-SERVER and X-HTTP
# Only X-INSPECT will be sent in responses to /inspect
}
location /other {
# Inherits X-SERVER (no add_header here)
# X-HTTP IS NOT inherited because X-SERVER overrides it at server level
}
}
}
Common pitfall: If you define
add_headerin alocationblock, headers defined in parentserverandhttpblocks are ignored for that location.
Best practice: Use an include file for common headers:
# /etc/nginx/conf.d/security-headers.conf
add_header X-Content-Type-Options nosniff;
add_header X-Frame-Options SAMEORIGIN;
add_header X-XSS-Protection "1; mode=block";
# In nginx.conf
server {
location / {
include conf.d/security-headers.conf;
proxy_pass http://backend:3000;
}
}
8. Architecture Diagrams
NGINX Architecture: Master process and Worker processes
graph TD
subgraph OS["Operating System"]
M["🔧 Master Process\n(nginx -c nginx.conf)"]
W1["⚙️ Worker Process 1"]
W2["⚙️ Worker Process 2"]
W3["⚙️ Worker Process 3"]
WN["⚙️ Worker Process N\n(1 per CPU core)"]
M -->|"fork()"| W1
M -->|"fork()"| W2
M -->|"fork()"| W3
M -->|"fork()"| WN
end
Config["📄 nginx.conf"] -->|"read"| M
Signals["📡 Signals\n(reload, quit, stop)"] --> M
C1["Client 1"] --> W1
C2["Client 2"] --> W1
C3["Client 3"] --> W2
CN["Client N..."] --> WN
style M fill:#d4610a,color:#fff
style W1 fill:#1565c0,color:#fff
style W2 fill:#1565c0,color:#fff
style W3 fill:#1565c0,color:#fff
style WN fill:#1565c0,color:#fff
Event Loop in a Worker Process
graph LR
subgraph Worker["Worker Process (non-blocking)"]
EL["Event Loop\n(epoll / kqueue)"]
Q["Event\nqueue"]
H1["Handler: Accept"]
H2["Handler: Read"]
H3["Handler: Write"]
H4["Handler: Close"]
EL --> Q
Q --> H1
Q --> H2
Q --> H3
Q --> H4
H1 --> EL
H2 --> EL
H3 --> EL
H4 --> EL
end
Clients["Thousands of\nTCP connections"] --> EL
style EL fill:#2e7d32,color:#fff
Reverse Proxy Flow
sequenceDiagram
participant C as Client (Browser)
participant N as NGINX (Proxy)
participant B as Back-end (Ghost/Node/etc.)
C->>N: GET /articles HTTP/1.1<br/>Host: example.com
Note over N: Server block selection<br/>based on Host header
N->>B: GET /articles HTTP/1.1<br/>Host: backend:3000<br/>X-Real-IP: 203.0.113.42<br/>X-Forwarded-For: 203.0.113.42
B-->>N: HTTP/1.1 200 OK<br/>Content-Type: text/html<br/>[body]
Note over N: Response buffering<br/>(if proxy_buffering on)
N-->>C: HTTP/1.1 200 OK<br/>Server: nginx<br/>[body]
Load Balancing Architecture
graph TD
subgraph Internet
C1["👤 Client A"]
C2["👤 Client B"]
C3["👤 Client C"]
end
subgraph NGINX["NGINX Load Balancer"]
LB["upstream backend_pool\n(Round Robin / ip_hash / least_conn)"]
end
subgraph Backends["Back-end Pool"]
B1["🖥️ app1:3000\n(weight=1)"]
B2["🖥️ app2:3000\n(weight=5)"]
B3["🖥️ app3:3000\n(weight=1)"]
BK["🔒 standby:3000\n(backup)"]
end
C1 --> LB
C2 --> LB
C3 --> LB
LB -->|"1/7 requests"| B1
LB -->|"5/7 requests"| B2
LB -->|"1/7 requests"| B3
LB -.->|"if B1+B2+B3 down"| BK
style LB fill:#d4610a,color:#fff
style BK fill:#b71c1c,color:#fff
SSL/TLS Termination
sequenceDiagram
participant C as Client
participant N as NGINX<br/>(TLS Termination)
participant B1 as Back-end 1<br/>(HTTP)
participant B2 as Back-end 2<br/>(HTTP)
C->>N: HTTPS / TLS Handshake<br/>(certificate presented)
Note over N: TLS decryption
alt Port 443, Host: example.com
N->>B1: HTTP (unencrypted)<br/>internal network
B1-->>N: HTTP 200 OK
else Port 443, Host: api.example.com
N->>B2: HTTP (unencrypted)<br/>internal network
B2-->>N: HTTP 200 OK
end
Note over N: TLS re-encryption
N-->>C: HTTPS encrypted response
Note over C,B2: Back-ends have no<br/>awareness of TLS
Caching Flow
flowchart TD
REQ["Incoming request"] --> CACHE_CHECK{"Cache hit?"}
CACHE_CHECK -->|"HIT ✅"| SERVE_CACHE["Serve from\ndisk cache\n⚡ Immediate response"]
CACHE_CHECK -->|"MISS ❌"| FORWARD["Forward to back-end"]
CACHE_CHECK -->|"EXPIRED ⏰"| FORWARD
FORWARD --> BACKEND["Back-end responds\n(potentially slow)"]
BACKEND --> CACHEABLE{"Cache-Control\nallows caching?"}
CACHEABLE -->|"max-age > 0"| STORE["Store in\nproxy_cache_path"]
CACHEABLE -->|"no-store / private"| NO_STORE["Do not cache"]
STORE --> CLIENT["Response to client"]
NO_STORE --> CLIENT
SERVE_CACHE --> CLIENT
style SERVE_CACHE fill:#2e7d32,color:#fff
style FORWARD fill:#1565c0,color:#fff
style STORE fill:#f57f17,color:#fff
9. Reference Tables
9.1 Essential NGINX directives
main context (global)
| Directive | Example | Description |
|---|---|---|
worker_processes | worker_processes auto; | Number of worker processes (auto = nb of cores) |
error_log | error_log /var/log/nginx/error.log warn; | Error log path and level |
pid | pid /run/nginx.pid; | Master process PID file |
include | include /etc/nginx/conf.d/*.conf; | Include configuration files |
events context
| Directive | Example | Description |
|---|---|---|
worker_connections | worker_connections 1024; | Max connections per worker |
use | use epoll; | I/O mechanism (epoll, kqueue, select) |
multi_accept | multi_accept on; | Accept multiple connections at once |
http context
| Directive | Example | Description |
|---|---|---|
access_log | access_log /var/log/nginx/access.log main; | HTTP access log |
log_format | log_format main '$remote_addr ...' | Define a log format |
sendfile | sendfile on; | File transfer via sendfile syscall (performance) |
keepalive_timeout | keepalive_timeout 65; | Keep-alive connection timeout |
gzip | gzip on; | Enable gzip compression |
gzip_types | gzip_types text/html ...; | MIME types to compress |
proxy_cache_path | (see section 5.4) | Define a disk cache zone |
split_clients | (see section 7.3) | A/B testing |
upstream | upstream pool { server ...; } | Define a back-end group |
server context
| Directive | Example | Description |
|---|---|---|
listen | listen 80; / listen 443 ssl; | Port and protocol to listen on |
root | root /var/www/html; | File root directory |
index | index index.html; | Default index file |
error_page | error_page 404 /404.html; | Custom error page |
ssl_certificate | ssl_certificate /certs/cert.pem; | SSL certificate path |
ssl_certificate_key | ssl_certificate_key /certs/key.pem; | Private key path |
http2 | http2 on; | Enable HTTP/2 |
return | return 301 https://$host$request_uri; | Immediate response / redirect |
location context
| Directive | Example | Description |
|---|---|---|
proxy_pass | proxy_pass http://backend:3000; | Target back-end URL |
proxy_set_header | proxy_set_header Host $host; | Modify a proxy request header |
proxy_buffering | proxy_buffering off; | Enable/disable buffering |
proxy_cache | proxy_cache mycache; | Enable cache for this location |
proxy_cache_valid | proxy_cache_valid 200 10m; | Cache validity duration by code |
add_header | add_header X-Cache-Status $upstream_cache_status; | Add a header to the response |
sub_filter | sub_filter 'http://' 'https://'; | Replace text in the response |
rewrite | rewrite ^/old/(.*)$ /$1 last; | Rewrite the request URI |
try_files | try_files $uri $uri/ =404; | Look for a file or return 404 |
autoindex | autoindex on; | Directory listing |
9.2 Main NGINX modules
| Module | Description |
|---|---|
ngx_http_core_module | Basic HTTP features (server, location, root…) |
ngx_http_proxy_module | HTTP reverse proxy (proxy_pass, proxy_set_header…) |
ngx_http_upstream_module | Load balancing (upstream, server, keepalive…) |
ngx_http_ssl_module | HTTPS / TLS support |
ngx_http_v2_module | HTTP/2 support |
ngx_http_gzip_module | Response gzip compression |
ngx_http_cache_module | Proxy cache |
ngx_http_rewrite_module | URL rewriting (rewrite, return, set) |
ngx_http_sub_module | Filtering and replacement in response body (sub_filter) |
ngx_http_split_clients_module | A/B testing (split_clients) |
ngx_stream_core_module | TCP/UDP proxying (stream, server) |
ngx_http_mp4_module | MP4 pseudo-streaming |
ngx_http_auth_basic_module | HTTP Basic authentication |
ngx_http_limit_req_module | Request rate limiting |
Check compiled modules:
nginx -V 2>&1 | grep -o -- '--with-[^ ]*'
9.3 Built-in variables
| Variable | Description |
|---|---|
$host | Value of the request Host header |
$remote_addr | Client IP address |
$request_uri | Complete request URI (path + query string) |
$uri | Normalized URI (without query string) |
$args | Query string |
$scheme | http or https |
$server_name | Name of the matching server block |
$server_port | Port NGINX is listening on |
$http_<header> | Value of a request header (e.g.: $http_upgrade) |
$upstream_addr | Address of the contacted upstream server |
$upstream_cache_status | HIT, MISS, EXPIRED, etc. |
$upstream_response_time | Back-end response time |
$proxy_add_x_forwarded_for | Value to use for X-Forwarded-For |
$date_gmt | Current GMT date/time |
$request_time | Total request processing duration |
$status | HTTP response code |
$bytes_sent | Number of bytes sent to the client |
10. Useful Commands
Managing the NGINX process
# Start
nginx
sudo service nginx start
sudo systemctl start nginx
# Graceful stop (waits for ongoing requests to finish)
nginx -s quit
sudo service nginx stop
# Immediate stop
nginx -s stop
# Hot configuration reload
nginx -s reload
sudo nginx -s reload
sudo systemctl reload nginx
# Reopen log files (after rotation)
nginx -s reopen
# Test configuration without applying it
nginx -t
nginx -T # test + display full config
Information and diagnostics
# Version
nginx -v # short version
nginx -V # version + compiled modules
# View processes
ps aux | grep nginx
pstree -p $(cat /var/run/nginx.pid)
# Live logs
tail -f /var/log/nginx/access.log
tail -f /var/log/nginx/error.log
# Monitor cache
ls -la /var/cache/nginx/
Docker
# Start NGINX with custom config
docker run -d \
-p 80:80 -p 443:443 \
-v $(pwd)/nginx.conf:/etc/nginx/nginx.conf:ro \
-v $(pwd)/certs:/certs:ro \
--name nginx \
nginx
# Reload config in container
docker exec nginx nginx -s reload
# Open a shell in the container
docker exec -it nginx bash
# Docker Compose
docker compose up -d
docker compose down
docker compose logs -f nginx
Testing and debugging tools
# curl — simple HTTP request
curl http://localhost/
curl -v http://localhost/ # verbose (headers)
curl -I http://localhost/ # headers only (HEAD)
curl -H "Host: example.com" http://localhost/ # custom header
# httpie — more readable alternative to curl
http http://localhost/
http -v http://localhost/ # verbose
http --follow http://localhost/ # follow redirects
# Test SSL
openssl s_client -connect localhost:443
certigo connect localhost:443
sslyze localhost
testssl localhost
# icdiff — compare two requests
icdiff <(http localhost:5000/headers) <(http localhost:80/headers)
Official resources:
- NGINX documentation: nginx.org/en/docs
- Administration guide: docs.nginx.com/nginx/admin-guide
- NGINX wiki: wiki.nginx.org
- VS Code extension for NGINX config files: nginx.conf syntax highlighting
Search Terms
nginx · networking · web · servers · systems · security · context · architecture · directive · process · balancing · caching · headers · http · https · load · proxy · server · worker · blocks · buffering · directives · docker · flow