← Back to Curriculum
Admin Panel
Manage your curriculum content — all changes update data/curriculum.json instantly.
📋 Curriculum Info
Update the curriculum title, subtitle, and author.
➕ Add Section to Module
Add a new topic section to any existing module.
💻 Add Code Block to Section
Attach a code example to any section.
📦 Create New Module
Add a brand new top-level module to the curriculum.
🗑 Delete Section
Permanently remove a section. This cannot be undone.
📄 Raw JSON Preview
Current state of curriculum.json — copy and edit manually if needed.
{
"meta": {
"title": "NGINX — Field Manual",
"subtitle": "A living curriculum for mastering Nginx from the ground up",
"version": "1.0",
"author": "Your Name",
"updated": "2026-05-02"
},
"modules": [
{
"id": "mod-1",
"title": "Introduction",
"icon": "⚡",
"color": "#4ade80",
"description": "What Nginx is, why it matters, and how it fits into modern infrastructure.",
"sections": [
{
"id": "sec-1-1",
"title": "What is Nginx?",
"status": "done",
"tags": [
"basics",
"overview"
],
"body": "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.",
"notes": "Nginx powers over 34% of all websites. Understanding its async model is key to everything else.",
"codeblocks": []
},
{
"id": "sec-1-2",
"title": "Nginx vs Apache",
"status": "done",
"tags": [
"basics",
"comparison"
],
"body": "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.",
"notes": "For modern stacks (Node.js, Python, PHP-FPM), Nginx is almost always the better frontend.",
"codeblocks": [
{
"label": "Apache vs Nginx memory under load",
"lang": "text",
"code": "# 1000 concurrent connections\nApache: ~300MB RAM (process\/thread per request)\nNginx: ~2.5MB RAM (event-driven, single thread per worker)"
}
]
},
{
"id": "sec-1-3",
"title": "Installation",
"status": "in-progress",
"tags": [
"setup",
"install"
],
"body": "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.",
"notes": "Always check `nginx -v` after install. Use `systemctl enable nginx` to start on boot.",
"codeblocks": [
{
"label": "Install on Ubuntu\/Debian",
"lang": "bash",
"code": "sudo apt update\nsudo apt install nginx -y\n\n# Start and enable\nsudo systemctl start nginx\nsudo systemctl enable nginx\n\n# Check status\nsudo systemctl status nginx"
},
{
"label": "Install on RHEL\/CentOS\/Fedora",
"lang": "bash",
"code": "sudo dnf install nginx -y\n\nsudo systemctl start nginx\nsudo systemctl enable nginx"
},
{
"label": "Verify installation",
"lang": "bash",
"code": "nginx -v\n# nginx version: nginx\/1.24.0\n\nnginx -t\n# nginx: configuration file \/etc\/nginx\/nginx.conf syntax is ok\n# nginx: configuration file \/etc\/nginx\/nginx.conf test is successful"
}
]
},
{
"id": "sec-1-4",
"title": "File Structure",
"status": "todo",
"tags": [
"setup",
"filesystem"
],
"body": "Nginx has a well-defined directory layout. Knowing where everything lives is essential before you start editing configs.",
"notes": "On Debian\/Ubuntu the main config is in \/etc\/nginx\/. On RHEL it's the same. Logs go to \/var\/log\/nginx\/.",
"codeblocks": [
{
"label": "Nginx filesystem layout",
"lang": "text",
"code": "\/etc\/nginx\/\n├── nginx.conf # Main config entry point\n├── conf.d\/ # Additional config files (*.conf)\n├── sites-available\/ # Virtual host definitions (Debian style)\n├── sites-enabled\/ # Symlinks to active sites\n├── snippets\/ # Reusable config fragments\n├── mime.types # MIME type mappings\n└── fastcgi_params # FastCGI parameters\n\n\/var\/log\/nginx\/\n├── access.log # All HTTP requests\n└── error.log # Errors and warnings\n\n\/var\/www\/html\/ # Default web root\n\/run\/nginx.pid # PID file"
}
]
}
]
},
{
"id": "mod-2",
"title": "Core Concepts",
"icon": "🧠",
"color": "#60a5fa",
"description": "The mental model: processes, directives, contexts, and configuration syntax.",
"sections": [
{
"id": "sec-2-1",
"title": "Master & Worker Processes",
"status": "done",
"tags": [
"architecture",
"processes"
],
"body": "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.",
"notes": "The master process runs as root; workers run as www-data or nginx. Never run workers as root.",
"codeblocks": [
{
"label": "Check running processes",
"lang": "bash",
"code": "ps aux | grep nginx\n# root 12345 nginx: master process \/usr\/sbin\/nginx\n# www-data 12346 nginx: worker process\n# www-data 12347 nginx: worker process"
},
{
"label": "Send signals to master",
"lang": "bash",
"code": "# Reload config without downtime\nnginx -s reload\n\n# Graceful shutdown\nnginx -s quit\n\n# Fast shutdown\nnginx -s stop\n\n# Reopen log files\nnginx -s reopen"
}
]
},
{
"id": "sec-2-2",
"title": "Configuration Syntax",
"status": "in-progress",
"tags": [
"config",
"syntax"
],
"body": "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.",
"notes": "Inheritance: directives in outer contexts are inherited by inner contexts, but can be overridden. Always run `nginx -t` before reloading.",
"codeblocks": [
{
"label": "Basic nginx.conf structure",
"lang": "nginx",
"code": "# main context\nworker_processes auto;\nerror_log \/var\/log\/nginx\/error.log warn;\npid \/run\/nginx.pid;\n\nevents {\n worker_connections 1024;\n use epoll; # Linux: epoll | macOS: kqueue\n multi_accept on;\n}\n\nhttp {\n include mime.types;\n default_type application\/octet-stream;\n\n sendfile on;\n keepalive_timeout 65;\n\n # Include all server blocks\n include \/etc\/nginx\/conf.d\/*.conf;\n include \/etc\/nginx\/sites-enabled\/*;\n}"
}
]
},
{
"id": "sec-2-3",
"title": "Directives & Contexts",
"status": "todo",
"tags": [
"config",
"directives"
],
"body": "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{}.",
"notes": "If Nginx throws 'directive not allowed here', you have a context mismatch.",
"codeblocks": [
{
"label": "Context hierarchy",
"lang": "text",
"code": "main\n└── events { } # Connection processing settings\n└── http { } # HTTP\/HTTPS traffic\n └── server { } # Virtual host (one per domain)\n └── location { } # URL pattern matching\n └── upstream { } # Load balancing pools"
}
]
}
]
},
{
"id": "mod-3",
"title": "Server Blocks",
"icon": "🗄️",
"color": "#a78bfa",
"description": "The heart of Nginx: defining virtual hosts, name-based routing, and multi-site setups.",
"sections": [
{
"id": "sec-3-1",
"title": "Basic Server Block",
"status": "done",
"tags": [
"server-block",
"virtualhost"
],
"body": "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.",
"notes": "Always include both IPv4 and IPv6 listen directives. Use a trailing slash on root, never on alias.",
"codeblocks": [
{
"label": "Minimal server block",
"lang": "nginx",
"code": "server {\n listen 80;\n listen [::]:80; # IPv6\n\n server_name example.com www.example.com;\n root \/var\/www\/example.com;\n index index.html index.php;\n\n location \/ {\n try_files $uri $uri\/ =404;\n }\n\n error_log \/var\/log\/nginx\/example.error.log;\n access_log \/var\/log\/nginx\/example.access.log;\n}"
},
{
"label": "Enable site (Debian\/Ubuntu style)",
"lang": "bash",
"code": "# Save config to sites-available\nsudo nano \/etc\/nginx\/sites-available\/example.com\n\n# Create symlink to enable\nsudo ln -s \/etc\/nginx\/sites-available\/example.com \\\n \/etc\/nginx\/sites-enabled\/\n\n# Test and reload\nsudo nginx -t && sudo systemctl reload nginx"
}
]
},
{
"id": "sec-3-2",
"title": "Multiple Server Blocks",
"status": "done",
"tags": [
"server-block",
"multi-site"
],
"body": "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.",
"notes": "You can have unlimited server blocks. Name-based is preferred over IP-based for shared hosting.",
"codeblocks": [
{
"label": "Two sites on one server",
"lang": "nginx",
"code": "# \/etc\/nginx\/sites-available\/site-a\nserver {\n listen 80;\n server_name site-a.com www.site-a.com;\n root \/var\/www\/site-a;\n\n location \/ {\n try_files $uri $uri\/ =404;\n }\n}\n\n# \/etc\/nginx\/sites-available\/site-b\nserver {\n listen 80;\n server_name site-b.com www.site-b.com;\n root \/var\/www\/site-b;\n\n location \/ {\n try_files $uri $uri\/ =404;\n }\n}"
}
]
},
{
"id": "sec-3-3",
"title": "Default Server & Catch-All",
"status": "in-progress",
"tags": [
"server-block",
"default"
],
"body": "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.",
"notes": "Always define a default_server that returns 444 (no response) or a generic page. This stops host header attacks.",
"codeblocks": [
{
"label": "Secure catch-all default server",
"lang": "nginx",
"code": "server {\n listen 80 default_server;\n listen [::]:80 default_server;\n\n # Drop connections with unknown Host header\n return 444;\n}\n\n# Or: show a generic page\nserver {\n listen 80 default_server;\n server_name _;\n root \/var\/www\/default;\n\n location \/ {\n return 200 'Nothing here.';\n add_header Content-Type text\/plain;\n }\n}"
}
]
},
{
"id": "sec-3-4",
"title": "Server Name Matching Order",
"status": "todo",
"tags": [
"server-block",
"matching"
],
"body": "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.",
"notes": "Regex server names start with ~ and use PCRE. They're slowest — use exact matches where possible.",
"codeblocks": [
{
"label": "Matching priority examples",
"lang": "nginx",
"code": "server_name example.com; # 1. Exact\nserver_name *.example.com; # 2. Wildcard prefix\nserver_name example.*; # 3. Wildcard suffix\nserver_name ~^(www\\.)?example\\.com$; # 4. Regex\nserver_name _; # 5. Catch-all (default_server)"
}
]
}
]
},
{
"id": "mod-4",
"title": "Location Blocks",
"icon": "📍",
"color": "#fb923c",
"description": "Routing requests by URL pattern — the most powerful and misunderstood feature of Nginx.",
"sections": [
{
"id": "sec-4-1",
"title": "Location Matching Types",
"status": "in-progress",
"tags": [
"location",
"matching"
],
"body": "Location blocks match the request URI and determine how to handle it. Five modifiers exist, each with different matching behavior and priority.",
"notes": "= (exact) is fastest. @ (named) is for internal redirects only. ~* is case-insensitive regex.",
"codeblocks": [
{
"label": "All location modifier types",
"lang": "nginx",
"code": "server {\n # 1. Exact match (highest priority)\n location = \/ping {\n return 200 'pong';\n add_header Content-Type text\/plain;\n }\n\n # 2. Regex match, case-sensitive\n location ~ \\.php$ {\n fastcgi_pass unix:\/run\/php\/php8.2-fpm.sock;\n include fastcgi_params;\n fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;\n }\n\n # 3. Regex match, case-insensitive\n location ~* \\.(jpg|jpeg|png|gif|ico|webp)$ {\n expires 30d;\n add_header Cache-Control \"public, immutable\";\n }\n\n # 4. Prefix match (with regex bypass ^~)\n location ^~ \/static\/ {\n root \/var\/www;\n expires max;\n }\n\n # 5. Default prefix match\n location \/ {\n try_files $uri $uri\/ =404;\n }\n\n # Named location (internal only)\n location @fallback {\n proxy_pass http:\/\/backend;\n }\n}"
},
{
"label": "Priority order (simplified)",
"lang": "text",
"code": "1. = exact match\n2. ^~ prefix (stops regex search)\n3. ~ and ~* regex (first match wins)\n4. Longest prefix match\n5. \/ catch-all"
}
]
},
{
"id": "sec-4-2",
"title": "try_files",
"status": "todo",
"tags": [
"location",
"try_files"
],
"body": "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).",
"notes": "$uri tries the exact path. $uri\/ tries as a directory (looking for index). =404 returns a 404 if nothing matches.",
"codeblocks": [
{
"label": "Common try_files patterns",
"lang": "nginx",
"code": "# Static site\nlocation \/ {\n try_files $uri $uri\/ =404;\n}\n\n# PHP app (e.g., Laravel, WordPress)\nlocation \/ {\n try_files $uri $uri\/ \/index.php?$query_string;\n}\n\n# SPA (React, Vue, Angular)\nlocation \/ {\n try_files $uri $uri\/ \/index.html;\n}\n\n# Named location fallback\nlocation \/ {\n try_files $uri $uri\/ @backend;\n}\nlocation @backend {\n proxy_pass http:\/\/127.0.0.1:3000;\n}"
}
]
}
]
},
{
"id": "mod-5",
"title": "Reverse Proxy",
"icon": "🔀",
"color": "#34d399",
"description": "Proxying requests to backends — Node.js, Python, PHP-FPM, and load balancing.",
"sections": [
{
"id": "sec-5-1",
"title": "Basic Reverse Proxy",
"status": "todo",
"tags": [
"proxy",
"proxy_pass"
],
"body": "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.",
"notes": "Always pass Host, X-Real-IP, and X-Forwarded-For headers to backends. Without them, your app won't see the real client IP.",
"codeblocks": [
{
"label": "Proxy to Node.js \/ Python app",
"lang": "nginx",
"code": "server {\n listen 80;\n server_name api.example.com;\n\n location \/ {\n proxy_pass http:\/\/127.0.0.1:3000;\n\n # Pass real client info\n proxy_set_header Host $host;\n proxy_set_header X-Real-IP $remote_addr;\n proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n proxy_set_header X-Forwarded-Proto $scheme;\n\n # Timeouts\n proxy_connect_timeout 10s;\n proxy_read_timeout 30s;\n proxy_send_timeout 30s;\n\n # Buffer settings\n proxy_buffering on;\n proxy_buffer_size 4k;\n proxy_buffers 8 16k;\n }\n}"
}
]
},
{
"id": "sec-5-2",
"title": "Load Balancing",
"status": "todo",
"tags": [
"proxy",
"load-balancing",
"upstream"
],
"body": "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.",
"notes": "Use `least_conn` for long-lived connections. `ip_hash` for session persistence. Add `backup` servers for failover.",
"codeblocks": [
{
"label": "Upstream load balancing",
"lang": "nginx",
"code": "upstream app_servers {\n least_conn; # Method: least connections\n\n server 10.0.0.1:3000 weight=3; # Gets 3x traffic\n server 10.0.0.2:3000 weight=1;\n server 10.0.0.3:3000 backup; # Only used if others fail\n\n keepalive 32; # Keep connections open\n}\n\nserver {\n listen 80;\n server_name example.com;\n\n location \/ {\n proxy_pass http:\/\/app_servers;\n proxy_http_version 1.1;\n proxy_set_header Connection \"\";\n }\n}"
}
]
}
]
},
{
"id": "mod-6",
"title": "SSL \/ TLS",
"icon": "🔒",
"color": "#f472b6",
"description": "HTTPS setup, Let's Encrypt, TLS best practices, and HTTP to HTTPS redirects.",
"sections": [
{
"id": "sec-6-1",
"title": "Let's Encrypt with Certbot",
"status": "todo",
"tags": [
"ssl",
"certbot",
"letsencrypt"
],
"body": "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.",
"notes": "Certbot auto-renews via a systemd timer or cron. Always test renewal: `certbot renew --dry-run`.",
"codeblocks": [
{
"label": "Install and run Certbot",
"lang": "bash",
"code": "# Install Certbot\nsudo apt install certbot python3-certbot-nginx -y\n\n# Obtain certificate (modifies nginx config automatically)\nsudo certbot --nginx -d example.com -d www.example.com\n\n# Or: obtain only, configure manually\nsudo certbot certonly --nginx -d example.com\n\n# Test auto-renewal\nsudo certbot renew --dry-run"
},
{
"label": "Manual HTTPS server block",
"lang": "nginx",
"code": "# Redirect HTTP → HTTPS\nserver {\n listen 80;\n listen [::]:80;\n server_name example.com www.example.com;\n return 301 https:\/\/$host$request_uri;\n}\n\n# HTTPS server\nserver {\n listen 443 ssl http2;\n listen [::]:443 ssl http2;\n server_name example.com www.example.com;\n\n ssl_certificate \/etc\/letsencrypt\/live\/example.com\/fullchain.pem;\n ssl_certificate_key \/etc\/letsencrypt\/live\/example.com\/privkey.pem;\n\n # Modern TLS settings\n ssl_protocols TLSv1.2 TLSv1.3;\n ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384;\n ssl_prefer_server_ciphers off;\n ssl_session_cache shared:SSL:10m;\n ssl_session_timeout 1d;\n\n # HSTS (1 year)\n add_header Strict-Transport-Security \"max-age=31536000\" always;\n\n root \/var\/www\/example.com;\n index index.html;\n\n location \/ {\n try_files $uri $uri\/ =404;\n }\n}"
}
]
}
]
},
{
"id": "mod-7",
"title": "Performance",
"icon": "🚀",
"color": "#fbbf24",
"description": "Gzip, caching, worker tuning, and static asset optimization.",
"sections": [
{
"id": "sec-7-1",
"title": "Gzip Compression",
"status": "todo",
"tags": [
"performance",
"gzip"
],
"body": "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.",
"notes": "Don't gzip images, videos, or already-compressed files — it wastes CPU with no size benefit.",
"codeblocks": [
{
"label": "Gzip configuration",
"lang": "nginx",
"code": "http {\n gzip on;\n gzip_vary on;\n gzip_proxied any;\n gzip_comp_level 6; # 1-9, sweet spot is 4-6\n gzip_min_length 256; # Don't compress tiny responses\n gzip_types\n text\/plain\n text\/css\n text\/javascript\n application\/javascript\n application\/json\n application\/xml\n image\/svg+xml\n font\/woff2;\n}"
}
]
},
{
"id": "sec-7-2",
"title": "Static File Caching",
"status": "todo",
"tags": [
"performance",
"caching",
"headers"
],
"body": "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.",
"notes": "Use `immutable` for hashed filenames (e.g., app.a3f9d2.js). Never cache HTML aggressively — it's your cache-busting entry point.",
"codeblocks": [
{
"label": "Cache headers per asset type",
"lang": "nginx",
"code": "# Hashed assets (aggressive caching)\nlocation ~* \\.(js|css)$ {\n expires 1y;\n add_header Cache-Control \"public, immutable\";\n}\n\n# Images\nlocation ~* \\.(png|jpg|jpeg|gif|webp|ico|svg)$ {\n expires 30d;\n add_header Cache-Control \"public\";\n}\n\n# Fonts\nlocation ~* \\.(woff|woff2|ttf|eot)$ {\n expires 1y;\n add_header Cache-Control \"public, immutable\";\n add_header Access-Control-Allow-Origin \"*\";\n}\n\n# HTML — short or no cache\nlocation ~* \\.html$ {\n expires -1;\n add_header Cache-Control \"no-store, must-revalidate\";\n}"
}
]
}
]
},
{
"id": "mod-8",
"title": "Security",
"icon": "🛡️",
"color": "#f87171",
"description": "Rate limiting, access control, security headers, and hardening your Nginx setup.",
"sections": [
{
"id": "sec-8-1",
"title": "Rate Limiting",
"status": "todo",
"tags": [
"security",
"rate-limit"
],
"body": "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.",
"notes": "Start lenient, then tighten. Use `limit_req_status 429` to return proper HTTP 429 Too Many Requests.",
"codeblocks": [
{
"label": "Rate limiting setup",
"lang": "nginx",
"code": "http {\n # Define zones (in http context)\n # 10mb zone stores ~160,000 IP states\n limit_req_zone $binary_remote_addr zone=general:10m rate=10r\/s;\n limit_req_zone $binary_remote_addr zone=login:10m rate=5r\/m;\n limit_req_status 429;\n}\n\nserver {\n # General rate limit\n location \/ {\n limit_req zone=general burst=20 nodelay;\n }\n\n # Strict limit on login endpoint\n location \/login {\n limit_req zone=login burst=3;\n proxy_pass http:\/\/backend;\n }\n}"
}
]
},
{
"id": "sec-8-2",
"title": "Security Headers",
"status": "todo",
"tags": [
"security",
"headers"
],
"body": "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.",
"notes": "Use https:\/\/securityheaders.com to audit your headers. Start with report-only for CSP before enforcing.",
"codeblocks": [
{
"label": "Essential security headers",
"lang": "nginx",
"code": "server {\n # Hide Nginx version from error pages and headers\n server_tokens off;\n\n # Prevent clickjacking\n add_header X-Frame-Options \"SAMEORIGIN\" always;\n\n # Prevent MIME type sniffing\n add_header X-Content-Type-Options \"nosniff\" always;\n\n # XSS protection (legacy, but still useful)\n add_header X-XSS-Protection \"1; mode=block\" always;\n\n # Referrer policy\n add_header Referrer-Policy \"strict-origin-when-cross-origin\" always;\n\n # Permissions policy\n add_header Permissions-Policy \"camera=(), microphone=(), geolocation=()\" always;\n\n # Content Security Policy (customize for your app)\n add_header Content-Security-Policy\n \"default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline';\"\n always;\n}"
}
]
}
]
}
]
}