Module 01

Introduction

What Nginx is, why it matters, and how it fits into modern infrastructure.

What is Nginx?

Nginx (pronounced 'engine-x') is a high-performance, open-source web server, reverse proxy, load balancer, and HTTP cache. Created by Igor Sysoev in 2004 to solve the C10K problem — handling 10,000+ concurrent connections — it uses an event-driven, asynchronous architecture instead of the traditional thread-per-request model.

💡 Nginx powers over 34% of all websites. Understanding its async model is key to everything else.

Nginx vs Apache

Apache uses a process/thread-per-request model (MPM). Nginx uses a single-threaded event loop with non-blocking I/O. Under high concurrency, Nginx uses significantly less memory and handles more requests per second. Apache excels at .htaccess flexibility and certain legacy PHP setups. Nginx wins on static file serving and as a reverse proxy.

💡 For modern stacks (Node.js, Python, PHP-FPM), Nginx is almost always the better frontend.
Apache vs Nginx memory under load text
# 1000 concurrent connections
Apache:  ~300MB RAM  (process/thread per request)
Nginx:   ~2.5MB RAM  (event-driven, single thread per worker)

Installation

Nginx is available in most Linux package managers. The stable branch is recommended for production. The mainline branch gets new features faster but is less battle-tested.

💡 Always check `nginx -v` after install. Use `systemctl enable nginx` to start on boot.
Install on Ubuntu/Debian bash
sudo apt update
sudo apt install nginx -y

# Start and enable
sudo systemctl start nginx
sudo systemctl enable nginx

# Check status
sudo systemctl status nginx
Install on RHEL/CentOS/Fedora bash
sudo dnf install nginx -y

sudo systemctl start nginx
sudo systemctl enable nginx
Verify installation bash
nginx -v
# nginx version: nginx/1.24.0

nginx -t
# nginx: configuration file /etc/nginx/nginx.conf syntax is ok
# nginx: configuration file /etc/nginx/nginx.conf test is successful

File Structure

Nginx has a well-defined directory layout. Knowing where everything lives is essential before you start editing configs.

💡 On Debian/Ubuntu the main config is in /etc/nginx/. On RHEL it's the same. Logs go to /var/log/nginx/.
Nginx filesystem layout text
/etc/nginx/
├── nginx.conf              # Main config entry point
├── conf.d/                 # Additional config files (*.conf)
├── sites-available/        # Virtual host definitions (Debian style)
├── sites-enabled/          # Symlinks to active sites
├── snippets/               # Reusable config fragments
├── mime.types              # MIME type mappings
└── fastcgi_params          # FastCGI parameters

/var/log/nginx/
├── access.log              # All HTTP requests
└── error.log               # Errors and warnings

/var/www/html/              # Default web root
/run/nginx.pid              # PID file
+ Add Section
Module 02
🧠

Core Concepts

The mental model: processes, directives, contexts, and configuration syntax.

Master & Worker Processes

Nginx runs as a master process and one or more worker processes. The master reads config, manages workers, and handles signals. Workers handle actual client connections. Each worker is single-threaded and uses non-blocking I/O via epoll/kqueue to handle thousands of connections simultaneously.

💡 The master process runs as root; workers run as www-data or nginx. Never run workers as root.
Check running processes bash
ps aux | grep nginx
# root     12345  nginx: master process /usr/sbin/nginx
# www-data 12346  nginx: worker process
# www-data 12347  nginx: worker process
Send signals to master bash
# Reload config without downtime
nginx -s reload

# Graceful shutdown
nginx -s quit

# Fast shutdown
nginx -s stop

# Reopen log files
nginx -s reopen

Configuration Syntax

Nginx config is made of directives. Simple directives end with a semicolon. Block directives (contexts) wrap other directives in curly braces. The main contexts are: main, events, http, server, location, upstream.

💡 Inheritance: directives in outer contexts are inherited by inner contexts, but can be overridden. Always run `nginx -t` before reloading.
Basic nginx.conf structure nginx
# main context
worker_processes auto;
error_log /var/log/nginx/error.log warn;
pid /run/nginx.pid;

events {
    worker_connections 1024;
    use epoll;               # Linux: epoll | macOS: kqueue
    multi_accept on;
}

http {
    include       mime.types;
    default_type  application/octet-stream;

    sendfile        on;
    keepalive_timeout 65;

    # Include all server blocks
    include /etc/nginx/conf.d/*.conf;
    include /etc/nginx/sites-enabled/*;
}

Directives & Contexts

Every Nginx behavior is controlled by directives placed in specific contexts. Understanding which directive belongs in which context prevents cryptic errors. Some directives are only valid in http{}, others only in server{} or location{}.

💡 If Nginx throws 'directive not allowed here', you have a context mismatch.
Context hierarchy text
main
└── events { }          # Connection processing settings
└── http { }            # HTTP/HTTPS traffic
    └── server { }      # Virtual host (one per domain)
        └── location { } # URL pattern matching
    └── upstream { }    # Load balancing pools
+ Add Section
Module 03
🗄️

Server Blocks

The heart of Nginx: defining virtual hosts, name-based routing, and multi-site setups.

Basic Server Block

A server block defines how Nginx responds to requests for a given domain, IP, or port. It's analogous to Apache's VirtualHost. At minimum, you need listen, server_name, and a root.

💡 Always include both IPv4 and IPv6 listen directives. Use a trailing slash on root, never on alias.
Minimal server block nginx
server {
    listen 80;
    listen [::]:80;          # IPv6

    server_name example.com www.example.com;
    root /var/www/example.com;
    index index.html index.php;

    location / {
        try_files $uri $uri/ =404;
    }

    error_log /var/log/nginx/example.error.log;
    access_log /var/log/nginx/example.access.log;
}
Enable site (Debian/Ubuntu style) bash
# Save config to sites-available
sudo nano /etc/nginx/sites-available/example.com

# Create symlink to enable
sudo ln -s /etc/nginx/sites-available/example.com \
           /etc/nginx/sites-enabled/

# Test and reload
sudo nginx -t && sudo systemctl reload nginx

Multiple Server Blocks

Nginx can host multiple sites on one machine using name-based virtual hosting. Each server block handles a different domain. Nginx matches the request's Host header against server_name values to pick the right block.

💡 You can have unlimited server blocks. Name-based is preferred over IP-based for shared hosting.
Two sites on one server nginx
# /etc/nginx/sites-available/site-a
server {
    listen 80;
    server_name site-a.com www.site-a.com;
    root /var/www/site-a;

    location / {
        try_files $uri $uri/ =404;
    }
}

# /etc/nginx/sites-available/site-b
server {
    listen 80;
    server_name site-b.com www.site-b.com;
    root /var/www/site-b;

    location / {
        try_files $uri $uri/ =404;
    }
}

Default Server & Catch-All

When a request doesn't match any server_name, Nginx falls back to the 'default_server'. If none is explicitly marked, Nginx uses the first server block in config order. A catch-all block is important for security — it prevents unknown Host headers from leaking content.

💡 Always define a default_server that returns 444 (no response) or a generic page. This stops host header attacks.
Secure catch-all default server nginx
server {
    listen 80 default_server;
    listen [::]:80 default_server;

    # Drop connections with unknown Host header
    return 444;
}

# Or: show a generic page
server {
    listen 80 default_server;
    server_name _;
    root /var/www/default;

    location / {
        return 200 'Nothing here.';
        add_header Content-Type text/plain;
    }
}

Server Name Matching Order

Nginx matches server_name in a specific priority order: 1) Exact name match, 2) Longest wildcard starting with *, 3) Longest wildcard ending with *, 4) First matching regex (in config order), 5) default_server.

💡 Regex server names start with ~ and use PCRE. They're slowest — use exact matches where possible.
Matching priority examples nginx
server_name example.com;              # 1. Exact
server_name *.example.com;            # 2. Wildcard prefix
server_name example.*;                # 3. Wildcard suffix
server_name ~^(www\.)?example\.com$; # 4. Regex
server_name _;                        # 5. Catch-all (default_server)
+ Add Section
Module 04
📍

Location Blocks

Routing requests by URL pattern — the most powerful and misunderstood feature of Nginx.

Location Matching Types

Location blocks match the request URI and determine how to handle it. Five modifiers exist, each with different matching behavior and priority.

💡 = (exact) is fastest. @ (named) is for internal redirects only. ~* is case-insensitive regex.
All location modifier types nginx
server {
    # 1. Exact match (highest priority)
    location = /ping {
        return 200 'pong';
        add_header Content-Type text/plain;
    }

    # 2. Regex match, case-sensitive
    location ~ \.php$ {
        fastcgi_pass unix:/run/php/php8.2-fpm.sock;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    }

    # 3. Regex match, case-insensitive
    location ~* \.(jpg|jpeg|png|gif|ico|webp)$ {
        expires 30d;
        add_header Cache-Control "public, immutable";
    }

    # 4. Prefix match (with regex bypass ^~)
    location ^~ /static/ {
        root /var/www;
        expires max;
    }

    # 5. Default prefix match
    location / {
        try_files $uri $uri/ =404;
    }

    # Named location (internal only)
    location @fallback {
        proxy_pass http://backend;
    }
}
Priority order (simplified) text
1. = exact match
2. ^~ prefix (stops regex search)
3. ~ and ~* regex (first match wins)
4. Longest prefix match
5. / catch-all

try_files

try_files checks for the existence of files in order and serves the first one found. It's essential for SPAs, WordPress, and clean URL routing. The last argument is always a fallback (URI or =CODE).

💡 $uri tries the exact path. $uri/ tries as a directory (looking for index). =404 returns a 404 if nothing matches.
Common try_files patterns nginx
# Static site
location / {
    try_files $uri $uri/ =404;
}

# PHP app (e.g., Laravel, WordPress)
location / {
    try_files $uri $uri/ /index.php?$query_string;
}

# SPA (React, Vue, Angular)
location / {
    try_files $uri $uri/ /index.html;
}

# Named location fallback
location / {
    try_files $uri $uri/ @backend;
}
location @backend {
    proxy_pass http://127.0.0.1:3000;
}
+ Add Section
Module 05
🔀

Reverse Proxy

Proxying requests to backends — Node.js, Python, PHP-FPM, and load balancing.

Basic Reverse Proxy

Nginx sits in front of your app server and forwards requests. This enables SSL termination, caching, logging, and rate limiting at the proxy layer — keeping your app simple.

💡 Always pass Host, X-Real-IP, and X-Forwarded-For headers to backends. Without them, your app won't see the real client IP.
Proxy to Node.js / Python app nginx
server {
    listen 80;
    server_name api.example.com;

    location / {
        proxy_pass http://127.0.0.1:3000;

        # Pass real client info
        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;

        # Timeouts
        proxy_connect_timeout 10s;
        proxy_read_timeout    30s;
        proxy_send_timeout    30s;

        # Buffer settings
        proxy_buffering on;
        proxy_buffer_size 4k;
        proxy_buffers 8 16k;
    }
}

Load Balancing

Nginx can distribute requests across multiple backend servers using upstream blocks. Built-in methods: round-robin (default), least_conn, ip_hash, and hash. The upstream block lives in the http{} context.

💡 Use `least_conn` for long-lived connections. `ip_hash` for session persistence. Add `backup` servers for failover.
Upstream load balancing nginx
upstream app_servers {
    least_conn;                         # Method: least connections

    server 10.0.0.1:3000 weight=3;     # Gets 3x traffic
    server 10.0.0.2:3000 weight=1;
    server 10.0.0.3:3000 backup;       # Only used if others fail

    keepalive 32;                       # Keep connections open
}

server {
    listen 80;
    server_name example.com;

    location / {
        proxy_pass http://app_servers;
        proxy_http_version 1.1;
        proxy_set_header Connection "";
    }
}
+ Add Section
Module 06
🔒

SSL / TLS

HTTPS setup, Let's Encrypt, TLS best practices, and HTTP to HTTPS redirects.

Let's Encrypt with Certbot

Certbot automates TLS certificate issuance and renewal from Let's Encrypt. The Nginx plugin handles both obtaining certificates and automatically modifying your server block config.

💡 Certbot auto-renews via a systemd timer or cron. Always test renewal: `certbot renew --dry-run`.
Install and run Certbot bash
# Install Certbot
sudo apt install certbot python3-certbot-nginx -y

# Obtain certificate (modifies nginx config automatically)
sudo certbot --nginx -d example.com -d www.example.com

# Or: obtain only, configure manually
sudo certbot certonly --nginx -d example.com

# Test auto-renewal
sudo certbot renew --dry-run
Manual HTTPS server block nginx
# Redirect HTTP → HTTPS
server {
    listen 80;
    listen [::]:80;
    server_name example.com www.example.com;
    return 301 https://$host$request_uri;
}

# HTTPS server
server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name example.com www.example.com;

    ssl_certificate     /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

    # Modern TLS settings
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384;
    ssl_prefer_server_ciphers off;
    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout 1d;

    # HSTS (1 year)
    add_header Strict-Transport-Security "max-age=31536000" always;

    root /var/www/example.com;
    index index.html;

    location / {
        try_files $uri $uri/ =404;
    }
}
+ Add Section
Module 07
🚀

Performance

Gzip, caching, worker tuning, and static asset optimization.

Gzip Compression

Gzip compresses HTTP responses before sending, reducing bandwidth by 60-80% for text-based content. Configure it in the http{} block to apply globally, or in specific server/location blocks.

💡 Don't gzip images, videos, or already-compressed files — it wastes CPU with no size benefit.
Gzip configuration nginx
http {
    gzip on;
    gzip_vary on;
    gzip_proxied any;
    gzip_comp_level 6;          # 1-9, sweet spot is 4-6
    gzip_min_length 256;        # Don't compress tiny responses
    gzip_types
        text/plain
        text/css
        text/javascript
        application/javascript
        application/json
        application/xml
        image/svg+xml
        font/woff2;
}

Static File Caching

Browsers cache static assets (CSS, JS, images) based on Cache-Control and Expires headers. Nginx can set these per file type. Content-addressed filenames (with hashes) allow aggressive long-term caching.

💡 Use `immutable` for hashed filenames (e.g., app.a3f9d2.js). Never cache HTML aggressively — it's your cache-busting entry point.
Cache headers per asset type nginx
# Hashed assets (aggressive caching)
location ~* \.(js|css)$ {
    expires 1y;
    add_header Cache-Control "public, immutable";
}

# Images
location ~* \.(png|jpg|jpeg|gif|webp|ico|svg)$ {
    expires 30d;
    add_header Cache-Control "public";
}

# Fonts
location ~* \.(woff|woff2|ttf|eot)$ {
    expires 1y;
    add_header Cache-Control "public, immutable";
    add_header Access-Control-Allow-Origin "*";
}

# HTML — short or no cache
location ~* \.html$ {
    expires -1;
    add_header Cache-Control "no-store, must-revalidate";
}
+ Add Section
Module 08
🛡️

Security

Rate limiting, access control, security headers, and hardening your Nginx setup.

Rate Limiting

Rate limiting protects your server from abuse, brute force attacks, and DDoS. Nginx uses the leaky bucket algorithm. Define zones in http{} and apply them in server/location blocks.

💡 Start lenient, then tighten. Use `limit_req_status 429` to return proper HTTP 429 Too Many Requests.
Rate limiting setup nginx
http {
    # Define zones (in http context)
    # 10mb zone stores ~160,000 IP states
    limit_req_zone $binary_remote_addr zone=general:10m rate=10r/s;
    limit_req_zone $binary_remote_addr zone=login:10m   rate=5r/m;
    limit_req_status 429;
}

server {
    # General rate limit
    location / {
        limit_req zone=general burst=20 nodelay;
    }

    # Strict limit on login endpoint
    location /login {
        limit_req zone=login burst=3;
        proxy_pass http://backend;
    }
}

Security Headers

HTTP security headers instruct browsers on how to handle your content. They prevent XSS, clickjacking, MIME sniffing, and information leakage. Add them to your HTTPS server block.

💡 Use https://securityheaders.com to audit your headers. Start with report-only for CSP before enforcing.
Essential security headers nginx
server {
    # Hide Nginx version from error pages and headers
    server_tokens off;

    # Prevent clickjacking
    add_header X-Frame-Options "SAMEORIGIN" always;

    # Prevent MIME type sniffing
    add_header X-Content-Type-Options "nosniff" always;

    # XSS protection (legacy, but still useful)
    add_header X-XSS-Protection "1; mode=block" always;

    # Referrer policy
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;

    # Permissions policy
    add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;

    # Content Security Policy (customize for your app)
    add_header Content-Security-Policy
        "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline';"
        always;
}
+ Add Section