Beginner

Getting Started with NGINX

Serve static files, reverse proxy, load balance, cache and terminate TLS with NGINX.

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

  1. Course Overview
  2. Installing NGINX and Serving Static Files
  3. Reverse Proxy
  4. Load Balancing
  5. Caching, Buffering, and Proxy Headers
  6. HTTPS: TLS Termination
  7. Request and Response Rewriting
  8. Architecture Diagrams
  9. Reference Tables
  10. 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:

RoleDescription
Static file serverServes HTML, CSS, images directly from disk
Reverse proxyForwards incoming requests to one or more back-ends
Load balancerDistributes traffic across multiple service instances
CacheKeeps a local copy of responses to reduce back-end load
TLS terminationDecrypts incoming HTTPS traffic, communicates with back-ends over plain HTTP
Request/Response rewriterModifies 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 / stderr of 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.org repository.

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 auto creates 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

TypeExampleDescription
Simple directiveworker_processes auto;One line terminated by ;
Block directivehttp { ... }Groups other directives in { }
Contextmain, http, server, location, stream, mailContext 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:

DirectiveExampleRole
rootroot /var/www/html;Root directory for files
indexindex index.html;Default file
error_pageerror_page 404 /404.html;Custom error page
autoindexautoindex on;Enable directory listing
try_filestry_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_pass replaces root — we no longer serve a file, we forward.
  • The name ghost is resolved by DNS (Docker Compose automatically creates DNS entries for each service).
  • The location path is preserved in the transmitted request, unless a trailing slash is used in proxy_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:

  1. Exact match of server_name
  2. Wildcard match (*.example.com)
  3. Regex match (~^www\.(.+)\.com$)
  4. 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 stream block is at the same level as http in 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

AlgorithmDirectiveDescription
Round Robin(default)Cycle sequentially through servers
Weighted Round Robinweight=NDistribute proportionally by weights
Least Connectionsleast_conn;Send to server with fewest active connections
IP Haship_hash;Map each client IP to a fixed server (sticky sessions)
Generic Hashhash $request_uri;Hash based on an NGINX variable
Randomrandom;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;
}
ParameterDefaultDescription
max_fails1Number of consecutive failures before marking as down
fail_timeout10sDuration 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:

HeaderNGINX behavior
HostReplaced by $proxy_host (back-end name)
ConnectionReplaced by close
X-Real-IPNot added by default
X-Forwarded-ForNot 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:

ParameterDescription
levels=1:2Cache subdirectory structure
keys_zone=name:sizeZone name and hash table size in memory
max_sizeMaximum cache size on disk
inactivePurge 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:

ValueMeaning
HITResponse served from cache
MISSResponse not found in cache, back-end consulted
EXPIREDEntry expired, back-end re-consulted
BYPASSCache bypassed (proxy_cache_bypass)
REVALIDATEDResponse revalidated with back-end
UPDATINGStale 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 → certificate
  • localhost+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 the http2 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:

CodeMeaning
200OK — return a body
301Moved Permanently — permanent redirect
302Found — temporary redirect
307Temporary Redirect — preserves HTTP method
403Forbidden
404Not 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:

FlagDescription
lastStops rewrite processing in the current block, restarts location search
breakStops rewrite processing, continues in the current block
redirectReturns a temporary 302
permanentReturns 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_filter only works on uncompressed content. Disable gzip or use gunzip 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_header in a location block, headers defined in parent server and http blocks 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)

DirectiveExampleDescription
worker_processesworker_processes auto;Number of worker processes (auto = nb of cores)
error_logerror_log /var/log/nginx/error.log warn;Error log path and level
pidpid /run/nginx.pid;Master process PID file
includeinclude /etc/nginx/conf.d/*.conf;Include configuration files

events context

DirectiveExampleDescription
worker_connectionsworker_connections 1024;Max connections per worker
useuse epoll;I/O mechanism (epoll, kqueue, select)
multi_acceptmulti_accept on;Accept multiple connections at once

http context

DirectiveExampleDescription
access_logaccess_log /var/log/nginx/access.log main;HTTP access log
log_formatlog_format main '$remote_addr ...'Define a log format
sendfilesendfile on;File transfer via sendfile syscall (performance)
keepalive_timeoutkeepalive_timeout 65;Keep-alive connection timeout
gzipgzip on;Enable gzip compression
gzip_typesgzip_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
upstreamupstream pool { server ...; }Define a back-end group

server context

DirectiveExampleDescription
listenlisten 80; / listen 443 ssl;Port and protocol to listen on
rootroot /var/www/html;File root directory
indexindex index.html;Default index file
error_pageerror_page 404 /404.html;Custom error page
ssl_certificatessl_certificate /certs/cert.pem;SSL certificate path
ssl_certificate_keyssl_certificate_key /certs/key.pem;Private key path
http2http2 on;Enable HTTP/2
returnreturn 301 https://$host$request_uri;Immediate response / redirect

location context

DirectiveExampleDescription
proxy_passproxy_pass http://backend:3000;Target back-end URL
proxy_set_headerproxy_set_header Host $host;Modify a proxy request header
proxy_bufferingproxy_buffering off;Enable/disable buffering
proxy_cacheproxy_cache mycache;Enable cache for this location
proxy_cache_validproxy_cache_valid 200 10m;Cache validity duration by code
add_headeradd_header X-Cache-Status $upstream_cache_status;Add a header to the response
sub_filtersub_filter 'http://' 'https://';Replace text in the response
rewriterewrite ^/old/(.*)$ /$1 last;Rewrite the request URI
try_filestry_files $uri $uri/ =404;Look for a file or return 404
autoindexautoindex on;Directory listing

9.2 Main NGINX modules

ModuleDescription
ngx_http_core_moduleBasic HTTP features (server, location, root…)
ngx_http_proxy_moduleHTTP reverse proxy (proxy_pass, proxy_set_header…)
ngx_http_upstream_moduleLoad balancing (upstream, server, keepalive…)
ngx_http_ssl_moduleHTTPS / TLS support
ngx_http_v2_moduleHTTP/2 support
ngx_http_gzip_moduleResponse gzip compression
ngx_http_cache_moduleProxy cache
ngx_http_rewrite_moduleURL rewriting (rewrite, return, set)
ngx_http_sub_moduleFiltering and replacement in response body (sub_filter)
ngx_http_split_clients_moduleA/B testing (split_clients)
ngx_stream_core_moduleTCP/UDP proxying (stream, server)
ngx_http_mp4_moduleMP4 pseudo-streaming
ngx_http_auth_basic_moduleHTTP Basic authentication
ngx_http_limit_req_moduleRequest rate limiting

Check compiled modules:

nginx -V 2>&1 | grep -o -- '--with-[^ ]*'

9.3 Built-in variables

VariableDescription
$hostValue of the request Host header
$remote_addrClient IP address
$request_uriComplete request URI (path + query string)
$uriNormalized URI (without query string)
$argsQuery string
$schemehttp or https
$server_nameName of the matching server block
$server_portPort NGINX is listening on
$http_<header>Value of a request header (e.g.: $http_upgrade)
$upstream_addrAddress of the contacted upstream server
$upstream_cache_statusHIT, MISS, EXPIRED, etc.
$upstream_response_timeBack-end response time
$proxy_add_x_forwarded_forValue to use for X-Forwarded-For
$date_gmtCurrent GMT date/time
$request_timeTotal request processing duration
$statusHTTP response code
$bytes_sentNumber 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:


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

Interested in this course?

Contact us to book it or get a custom training plan for your team.