โหมดมืด
บทที่ 8 — Nginx ลึก (Reverse Proxy, Load Balancer, TLS, Performance)
TL;DR: บทนี้ตอบ "ตั้ง Nginx เป็น reverse proxy (รับ request แล้วส่งต่อให้ backend) / load balancer (กระจายโหลดไปหลาย backend) / TLS terminator (รับการเข้ารหัส HTTPS แล้วถอดให้ backend) ระดับ production" — เรียนตั้งแต่ syntax ของ config, การประกาศ
upstreamพร้อม algorithm กระจายโหลด, การตั้ง TLS + HTTP/2/3 (โปรโตคอลรุ่นใหม่ที่เร็วกว่า HTTP/1.1), caching (เก็บ response ไว้ตอบซ้ำ), rate limit (จำกัดจำนวน request), การ proxy WebSocket/gRPC, การจูน performance และ monitoring. ข้ามได้ถ้า: ใช้ Traefik หรือ K8s Ingress controller เป็นหลักทางเลือกอื่นในยุค 2026: Nginx ยังครองตลาด แต่มีทางเลือกที่น่าสนใจ — Caddy (auto-HTTPS ในตัว), OpenResty (Nginx + Lua scripting), Nginx Unit (app server รุ่นใหม่จากทีมเดียวกัน รัน Python/Node/PHP ในตัว), Cilium ingress + Gateway API (สำหรับ K8s ยุคใหม่ที่ย้ายจาก Ingress v1) ดูบทที่ 9 (Traefik) ประกอบ
🟡 หมายเหตุระดับ: บทนี้กว้างมาก ครอบคลุมตั้งแต่ syntax พื้นฐานไปจนถึง production tuning — "ลึก" ในเชิงครอบคลุมทุก directive ที่ใช้บ่อย ไม่ใช่ deep-dive ระดับ Nginx internals/source code สำหรับการ tune ระดับ kernel/sysctl ดูเอกสารทางการ (nginx.org) + Mozilla SSL Config Generator คู่กัน
Nginx เป็นหนึ่งใน HTTP server + reverse proxy ที่ใช้แพร่หลายที่สุดในโลก (ส่วนแบ่งตลาด exact เปลี่ยนทุกเดือน — ดู W3Techs หรือ Netcraft ก่อนอ้างตัวเลข; flagged 2026-06) — บทนี้สอนจากศูนย์ไปถึง production tuning
1. Nginx คืออะไร — และทำหน้าที่อะไรได้
Nginx (อ่านว่า "Engine-X") สร้างปี 2004 โดย Igor Sysoev เพื่อแก้ปัญหา C10K — ปัญหาคลาสสิกของ web server ยุคนั้น คือ "จะรับ connection พร้อมกัน 10,000 ตัว (10K) ได้ยังไงโดยเครื่องไม่ล่ม" สมัยนั้น web server ส่วนใหญ่จะตั้ง 1 thread หรือ 1 process ต่อ 1 connection พอ connection เกินไม่กี่พันก็ RAM หมด
ใช้ได้ 4 บทบาทหลัก:
- Web server — เสิร์ฟไฟล์ static (HTML, CSS, JS, รูปภาพ)
- Reverse proxy (รีเวิร์สพร็อกซี = ตัวกลางฝั่ง server) — รับ request จาก client แล้วส่งต่อให้ backend (Node, Python, Java, ...)
- Load balancer (ตัวกระจายโหลด) — กระจายโหลดไปยัง backend หลายตัว
- API gateway / Edge router (ประตูฝั่งขอบเครือข่าย) — ทำ TLS termination (รับการเข้ารหัสฝั่งนอก แล้วถอดเป็น HTTP ส่งให้ backend), rate limit (จำกัดจำนวน request), caching (เก็บ response ไว้ตอบซ้ำ), header rewrite (แก้ไข header ที่ส่งต่อ)
เทียบกับ Apache HTTPD (Apache HTTPD = web server รุ่นเก่าที่นิยมมาก่อน Nginx จะมาแรง):
Nginx = event-driven (ขับเคลื่อนด้วย event)
💡 event-driven คืออะไร: 1 process จัดการได้พันๆ connection พร้อมกัน แทนที่จะหยุดรอทีละ connection แบบเก่า Nginx จะ "ลงทะเบียน" event ไว้ก่อนแล้วไปทำงานอื่นต่อ พอ I/O (เช่นข้อมูลจาก network) เสร็จ OS จะเรียกกลับมาทำงานต่อให้เอง
Apache = process/thread per connection (1 connection = 1 thread → กินหน่วยความจำเร็ว)
Nginx ชนะใน static + reverse proxy / Apache มี
.htaccessยืดหยุ่นกว่าในสภาพ shared hosting
2. Architecture — ทำไมเร็ว
text
[Master process] ← รันด้วย root: อ่าน config, จัดการ worker, รับ signal จาก OS
↓
[Worker process 1] ← event loop แบบ non-blocking (ใช้ epoll/kqueue)
[Worker process 2]
[Worker process N] ← จำนวน worker มักเท่ากับจำนวน CPU coreแต่ละ worker:
- ใช้ epoll (บน Linux) / kqueue (บน BSD/macOS) → 1 thread จัดการได้พัน connection พร้อมกัน
- ไม่มีการ blocking (ไม่หยุดรอ) — ทุก I/O เป็นแบบ async (อะซิงโครนัส = ไม่รอผล ทำงานอื่นต่อได้แล้วค่อยกลับมาเก็บผลทีหลัง)
- memory footprint (ปริมาณหน่วยความจำที่ใช้) ต่อ connection น้อยมาก (~2.5KB) ทำให้รับ concurrent connection (connection ที่เปิดพร้อมกัน) ได้เยอะ
💡 epoll / kqueue = กลไกของ OS ที่ให้ 1 thread "เฝ้า" connection หลายพันตัวพร้อมกัน แล้ว OS จะปลุกเมื่อตัวไหนมีข้อมูลเข้ามา — แทนที่จะตั้ง 1 thread เฝ้า 1 connection (ซึ่งกิน RAM มหาศาล)
epollเป็นของ Linux ส่วนkqueueเป็นของ BSD/macOS
ผลลัพธ์: 1 worker = 1 CPU core = 100K+ concurrent connection ได้สบาย
3. ติดตั้ง + ไฟล์สำคัญ
bash
# Ubuntu/Debian
sudo apt install nginx
# Docker (เหมาะกับการลองเทสต์)
docker run -d -p 80:80 nginx:1.27-alpinebash
# ตรวจสอบว่าติดตั้งสำเร็จ
nginx -v
curl http://localhost # ควรเห็น Welcome to nginx!Path สำคัญ (บน Linux):
text
/etc/nginx/nginx.conf ← ไฟล์ config หลัก
/etc/nginx/conf.d/*.conf ← ไฟล์ย่อยสำหรับ config แต่ละแอป
/etc/nginx/sites-available/ ← (เฉพาะ Debian/Ubuntu) เก็บ virtual host ทั้งหมดที่มี
(virtual host = การให้ Nginx ตัวเดียวเสิร์ฟหลายเว็บ)
/etc/nginx/sites-enabled/ ← symlink (ลิงก์เสมือนใน Linux) ชี้ไปยังตัวที่เปิดใช้
/var/log/nginx/access.log ← log การเข้าใช้งาน
/var/log/nginx/error.log ← log เมื่อเกิด error
/usr/share/nginx/html/ ← โฟลเดอร์ static เริ่มต้น (หน้า welcome page)Reload config (ไม่มี downtime):
bash
sudo nginx -t # ทดสอบ syntax ว่าถูกไหม
sudo nginx -s reload # นำ config ใหม่มาใช้reload = master ส่ง signal ให้ worker เก่าทำงานจน connection หมด + spawn worker ใหม่ — ไม่มี downtime
4. Config Syntax — Block + Directive
nginx
# /etc/nginx/nginx.conf
# === main context (นอกสุด) ===
user nginx;
worker_processes auto; # auto = ให้ Nginx ตั้งเท่ากับจำนวน CPU core
error_log /var/log/nginx/error.log warn;
events {
# หมายเหตุ: ค่า worker_connections สูงต้องคู่กับ worker_rlimit_nofile
# (file descriptor limit — file descriptor คือตัวเลขที่ OS ใช้แทนทุกอย่างที่เปิดอยู่ ทั้งไฟล์และ socket
# ยังไม่ต้องเข้าใจลึกตอนนี้ก็ได้ อธิบายละเอียดในหัวข้อ 15 Performance Tuning ด้านล่าง)
# ไม่งั้น OS จะ refuse connection — ดูหัวข้อ Performance Tuning
worker_connections 4096; # จำนวน connection ต่อ worker = 4096 → ทั้งระบบ = workers × 4096
# use epoll; # ไม่จำเป็นต้องระบุ — Nginx เลือก event method ที่ดีสุดของแต่ละ OS เอง
# ตั้งเองทำให้พกพาข้าม OS ยากขึ้น (epoll = Linux เท่านั้น)
multi_accept on; # รับหลาย connection ใหม่พร้อมกันในหนึ่งรอบของ event loop
}
http {
# === http context (สำหรับ HTTP server ทั้งหมด) ===
include /etc/nginx/mime.types;
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent" '
'rt=$request_time ut="$upstream_response_time"';
access_log /var/log/nginx/access.log main;
# หมายเหตุ: $request/$http_referer/$http_user_agent มาจาก client โดยตรง ผู้ไม่หวังดีใส่ค่าอะไรก็ได้
# ถ้าจะส่ง log พวกนี้เข้า SIEM/log aggregator แนะนำใช้ escape=json (ดูตัวอย่างหัวข้อ 16) กัน log injection
sendfile on; # zero-copy (ส่งตรง ไม่ก๊อปปี้ข้ามพื้นที่ kernel↔user)
tcp_nopush on; # รวม TCP packet เป็นชุดก่อนส่ง (ลดจำนวน packet)
tcp_nodelay on; # ปิด Nagle algorithm (กลไกของ TCP ที่หน่วงเพื่อรวม packet เล็ก)
# — ปิดเพื่อให้ตอบ keepalive ทันที ไม่หน่วง
keepalive_timeout 65;
gzip on;
gzip_types text/plain text/css application/json application/javascript;
include /etc/nginx/conf.d/*.conf;
}ลำดับชั้น (Hierarchy): main → events / http → server → location
⚠️ Directive แต่ละ context มีต่างกัน —
listenอยู่ในserverไม่ใช่http
5. Server Block — Virtual Host
nginx
# /etc/nginx/conf.d/example.conf
server {
listen 80;
listen [::]:80; # IPv6
server_name example.com www.example.com;
root /var/www/example;
index index.html;
location / {
try_files $uri $uri/ =404;
}
location /api/ {
proxy_pass http://localhost:3000/;
}
error_page 404 /404.html;
error_page 500 502 503 504 /50x.html;
}หลาย server ใน Nginx = หลาย site แชร์ worker เดียวกัน
server_name ลำดับการจับคู่ (matching):
- ตรงตัว (exact):
example.com - wildcard ฝั่งซ้าย (ดอกจันอยู่ซ้าย):
*.example.comใช้ครอบทุก subdomain - wildcard ฝั่งขวา (ดอกจันอยู่ขวา):
example.*ใช้ครอบทุก TLD - regex (regular expression):
~^api\d+\.example\.com$ - ค่าเริ่มต้น — ใช้ flag
default_serverกับlistenของ server block ที่ต้องการเป็น fallback
6. Location Block — เลือก handler ตาม URL
nginx
location = / { ... } # ตรงตัว (exact)
location /api/ { ... } # ขึ้นต้นด้วย /api/ (prefix)
location ~ \.php$ { ... } # regex แบบสนใจตัวพิมพ์ (case-sensitive)
location ~* \.(jpg|png)$ { ... } # regex แบบไม่สนตัวพิมพ์ (case-insensitive)
location ^~ /static/ { ... } # prefix ที่ "ห้ามตกไปลอง regex ต่อ" (ไม่ fall through)ลำดับการจับคู่ (match):
=ตรงตัว — ถ้าเจอใช้เลย ไม่ลองอย่างอื่น^~prefix แบบ preferred — ถ้าเจอใช้ ไม่ลอง regex- regex (
~/~*) ตามลำดับที่เขียนในไฟล์ - prefix ปกติ (ใช้ตัวที่ตรงยาวที่สุด — longest match)
Try alternative:
nginx
location / {
try_files $uri $uri/ /index.html; # SPA fallback (React/Vue)
}try_files = ลองหาไฟล์ตาม path ในลิสต์ ถ้าไม่เจอใช้ตัวสุดท้าย — pattern คลาสสิกของ SPA
7. Reverse Proxy — Forward request ไป backend
หน้าที่ที่ใช้ Nginx บ่อยที่สุดคือ reverse proxy — รับ request จาก client แล้วส่งต่อไปยัง backend (app server) หลายตัว พร้อมกระจายโหลด (load balancing) ส่วนนี้ยังสอนการตั้ง header สำคัญ (X-Forwarded-For/Host), timeout, keepalive และ algorithm กระจายโหลดแต่ละแบบ:
nginx
upstream backend {
server 10.0.0.1:3000;
server 10.0.0.2:3000;
server 10.0.0.3:3000 backup; # backup = ใช้เมื่อตัวอื่นล้มเหลวหมด
keepalive 32; # connection pool (เก็บ connection ที่เปิดค้างไว้ 32 ตัว
# — ใช้ซ้ำได้ ไม่ต้องเปิดใหม่ทุก request)
}
server {
listen 80;
server_name api.example.com;
location / {
proxy_pass http://backend;
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;
proxy_set_header Connection ""; # ล้าง header Connection เพื่อให้ keepalive ใช้ได้
proxy_connect_timeout 5s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
# buffering (กันให้ Nginx อ่าน response จาก backend เก็บไว้ก่อนแล้วค่อยส่งให้ client)
proxy_buffering on;
proxy_buffer_size 16k;
proxy_buffers 16 32k;
}
}💡 resolver gotcha: ถ้า upstream ชี้ไปยังชื่อบริการ (เช่น service name ใน Docker Compose / Kubernetes ที่ IP เปลี่ยนได้) ต้องใส่ directive
resolver 127.0.0.11 valid=30s;(Docker DNS) ไม่งั้น Nginx จะ resolve IP ตอน start แล้วจำไว้ตลอด พอ container restart แล้ว IP เปลี่ยน connection จะพังเงียบ ๆ
Load balancing algorithms (algorithm กระจายโหลด)
⚠️ ตัวอย่างด้านล่างต้องเขียนเป็น
upstreamทีละ block แยกกัน — เลือก algorithm ได้แค่ 1 ตัวต่อ 1 upstream และห้ามประกาศserverเดิมซ้ำใน block เดียวกัน ถ้ารวมไว้ block เดียวnginx -tจะเจอ syntax error
nginx
# 1) round-robin (ค่าเริ่มต้น — สลับวนไปทีละ backend)
upstream backend_rr {
server a:80;
server b:80;
}
# 2) least_conn — ส่งไป backend ที่มี connection น้อยสุด
# (เหมาะกับ long-lived request = request ที่อยู่ยาว เช่น upload ไฟล์ใหญ่ ๆ,
# SSE — Server-Sent Events, วิธีให้ server ส่งข้อมูลต่อเนื่องไปหา browser ผ่าน HTTP ปกติ —, WebSocket)
upstream backend_lc {
least_conn;
server a:80;
server b:80;
}
# 3) ip_hash — แฮช IP ของ client เพื่อให้ user คนเดิมตกที่ backend ตัวเดิม
# (sticky session = ผูกผู้ใช้คนเดิมให้ไป backend ตัวเดิมตลอด — ใช้กับแอปที่เก็บ session ใน memory)
upstream backend_ip {
ip_hash;
server a:80;
server b:80;
}
# 4) hash — consistent hash จาก key ที่กำหนด (มักใช้กับ cache layer
# เพราะ key เดิมตกที่ backend เดิม → cache hit rate สูง)
upstream backend_hash {
hash $request_uri consistent;
server a:80;
server b:80;
}
# 5) weighted round-robin — ใส่น้ำหนัก เครื่องแรง weight สูง รับ request มากกว่า
upstream backend_weighted {
server a:80 weight=3; # รับ 3 ใน 4 ส่วน
server b:80 weight=1; # รับ 1 ใน 4 ส่วน
}Health check (ตรวจว่า backend ยังมีชีวิตไหม)
Nginx OSS (ฟรี) ทำได้แค่ passive (เช็คตอนมี request จริง — ถ้า request fail หลายครั้งติด ก็ mark backend ตัวนั้นเป็น down ชั่วคราว):
nginx
upstream backend {
server a:80 max_fails=3 fail_timeout=30s;
# ถ้าล้มเหลว 3 ครั้งใน 30 วินาที → mark down 30 วินาที
}Nginx Plus (เสียเงิน) ทำ active health check ได้ (Nginx จะส่ง probe ไปยัง URL ที่กำหนดเป็นระยะแม้ไม่มี traffic) หรือถ้าใช้ฉบับฟรีและอยากได้ active health check ให้ลง third-party module ngx_http_upstream_check_module
8. TLS / HTTPS
nginx
server {
listen 443 ssl;
http2 on; # ตั้งแยกออกมา (syntax ใหม่ตั้งแต่ Nginx 1.25.1 — ไม่ใส่ http2 ใน listen)
server_name example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
# === TLS รุ่นใหม่เท่านั้น ===
# ไม่ต้องเข้าใจศัพท์ crypto ด้านล่างลึกก็ได้ — แค่ copy ค่านี้ไปวางแล้วไปสร้างของจริงที่
# https://ssl-config.mozilla.org ก็พอสำหรับใช้งานจริง ศัพท์พวกนี้ใส่ไว้ให้รู้ที่มาเฉย ๆ
# cipher list อิงตาม Mozilla "Intermediate" profile (เวอร์ชัน 5.7, ปี 2024)
# ศัพท์สำคัญ:
# ECDHE = Elliptic Curve Diffie-Hellman Ephemeral
# → ให้คุณสมบัติ forward secrecy / PFS (Perfect Forward Secrecy)
# คือ ถ้า private key หลุดในอนาคต traffic เก่าก็ถอดรหัสไม่ได้
# AES-GCM, CHACHA20 = AEAD (Authenticated Encryption with Associated Data)
# → เข้ารหัสและตรวจสอบความถูกต้องในขั้นตอนเดียว ป้องกัน tamper
# ให้สร้างของจริงที่ https://ssl-config.mozilla.org แล้ว pin ตาม compliance ของระบบคุณ
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers off;
# cipher list ตาม Mozilla Intermediate 2024 (TLS 1.2; TLS 1.3 ใช้ cipher ของตัวเอง ไม่ต้องระบุ)
# หมายเหตุ: กลุ่มท้ายที่ขึ้นต้นด้วย DHE-RSA-* (ไม่ใช่ ECDHE) ใส่ไว้เพื่อรองรับ client เก่าเท่านั้น
# DHE ทำ handshake ช้ากว่า ECDHE (กิน CPU มากกว่า) ถ้ามั่นใจว่า client ทุกตัวรองรับ ECDHE แล้ว
# ตัดกลุ่ม DHE-RSA-* ออกได้เพื่อความเร็ว
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-CHACHA20-POLY1305;
# === OCSP stapling ===
# OCSP = Online Certificate Status Protocol — server แนบสถานะใบรับรอง (ยัง valid ไหม)
# มากับ response เลย client ไม่ต้องไปถาม CA เอง → เร็วขึ้น + privacy ดีขึ้น
ssl_stapling on;
ssl_stapling_verify on;
# ssl_trusted_certificate จำเป็นเพื่อให้ Nginx ตรวจ OCSP response ได้
# (ใช้ chain.pem จาก Let's Encrypt หรือ intermediate ของ CA ที่ออกใบรับรองให้)
ssl_trusted_certificate /etc/letsencrypt/live/example.com/chain.pem;
resolver 1.1.1.1 8.8.8.8 valid=300s; # ต้องมี resolver เพื่อให้ Nginx เรียก OCSP server ได้
# === HSTS (HTTP Strict Transport Security) ===
# บอก browser ว่า "ห้ามใช้ HTTP เปล่ากับโดเมนนี้อีก" ตลอดระยะที่กำหนด (max-age)
# ⚠️ คำเตือนสำคัญเรื่อง preload:
# preload submission ที่ส่งเข้า hstspreload.org เป็นการ hard-code โดเมนคุณ
# ลงในซอร์สโค้ดของ browser ทุกตัว — "ถาวร" สำหรับการใช้งานจริง
# การถอดออกใช้เวลาเป็นเดือนหรือเป็นปี และระหว่างนั้นถ้า HTTPS ของคุณพัง = ใช้งานเว็บไม่ได้เลย
# → อย่าใส่ preload จนกว่าจะมั่นใจว่ารัน HTTPS ทั้งโดเมน + ทุก subdomain ได้ยาว ๆ
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
# หลังทดสอบดีแล้วค่อยเพิ่ม preload:
# add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
# === session cache ===
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
ssl_session_tickets off; # ปิด session ticket (กัน issue forward secrecy เมื่อ key รั่ว)
# ssl_early_data off; # ค่า default = off; เปิดได้ถ้ารับ 0-RTT replay risk ได้
location / {
proxy_pass http://backend;
}
}
# Redirect HTTP → HTTPS
server {
listen 80;
server_name example.com;
return 301 https://$host$request_uri;
}Let's Encrypt ผ่าน Certbot
bash
# แนะนำ (ทางการของ EFF): ใช้ snap เพราะได้เวอร์ชันใหม่กว่า apt
sudo snap install --classic certbot
sudo ln -s /snap/bin/certbot /usr/bin/certbot
# หรือถ้าใช้ apt (เวอร์ชันใน apt มักจะเก่ากว่า)
# sudo apt install certbot python3-certbot-nginx
sudo certbot --nginx -d example.com -d www.example.com
# ทดสอบ auto-renew (renew อัตโนมัติ — ใบรับรองของ Let's Encrypt อายุ 90 วัน)
sudo certbot renew --dry-runใช้ flag --nginx = certbot แก้ config ของ Nginx ให้อัตโนมัติ + ตั้ง cronjob/systemd timer ให้ renew ก่อนหมดอายุ
9. HTTP/2 + HTTP/3
Nginx รองรับโปรโตคอลรุ่นใหม่ที่เร็วกว่า HTTP/1.1 มาก — HTTP/2 ใช้ connection เดียวส่งหลาย stream พร้อมกัน (multiplexing) ลด head-of-line blocking ส่วน HTTP/3 ไปไกลกว่าด้วยการใช้ QUIC (บน UDP) เปิดใช้ได้ด้วยการเพิ่มไม่กี่บรรทัดใน config (ต้องมี TLS):
⚠️ ทุก directive ด้านล่าง (
listen,http2,http3,add_header) ต้องอยู่ ในserver { ... }block เท่านั้น — โค้ดด้านล่างเป็นแค่ fragment ให้ดูว่าใส่บรรทัดไหนบ้าง ไม่ใช่ไฟล์ config สมบูรณ์ (ดูตัวอย่างเต็มในหัวข้อ 19)
nginx
# HTTP/2 (ต้องมี TLS เท่านั้น) — วางบรรทัดเหล่านี้ไว้ใน server {} ของคุณ
# syntax ใหม่ตั้งแต่ Nginx 1.25.1: แยก http2 ออกเป็น directive เดี่ยว
# (ไม่ใส่ http2 ต่อท้าย listen — syntax เก่าถูก deprecate แล้ว)
listen 443 ssl;
http2 on;
# HTTP/3 (ต้องใช้ Nginx 1.25 ขึ้นไป + OpenSSL/BoringSSL ที่รองรับ QUIC)
# QUIC = Quick UDP Internet Connections — โปรโตคอลของ Google ที่ build บน UDP
# (แทนที่จะ build บน TCP เหมือน HTTP/1.1 และ HTTP/2)
listen 443 quic reuseport; # listener สำหรับ HTTP/3 (UDP/443)
listen 443 ssl; # fallback สำหรับ HTTP/2/1.1 (TCP/443) — browser ใหม่ ๆ ลองทั้งสอง
http2 on; # เปิด HTTP/2 (syntax ใหม่ใช้ directive เดี่ยว)
# Nginx 1.27+ แนะนำใช้ directive เพิ่ม:
http3 on; # เปิด HTTP/3 อย่างเป็นทางการ (สั้นกว่า/ชัดกว่า)
add_header Alt-Svc 'h3=":443"; ma=86400'; # บอก browser ว่า "ลอง HTTP/3 ดูนะ"
ssl_protocols TLSv1.3; # HTTP/3 ต้องการ TLS 1.3 เท่านั้น- HTTP/2 ใช้ multiplexing (multiplexing = ส่งหลาย request ผ่าน connection เดียวพร้อมกัน) → 1 TCP connection มีหลาย stream → ลดปัญหา HOL blocking ในระดับ application
- HTTP/3 ใช้ QUIC (โปรโตคอลใหม่ที่ทำงานบน UDP แทน TCP) → ไม่มี TCP HOL blocking ระดับ transport เลย
💡 HOL blocking (Head-Of-Line blocking = "หัวแถวติด ทั้งแถวรอ") — ถ้า request ที่อยู่ต้นคิวช้าหรือ packet หาย request ที่ต่อแถวอยู่ข้างหลังก็ต้องรอตามไปด้วย แม้ตัวมันเองพร้อมแล้ว HTTP/2 ลดปัญหานี้ในระดับ application ส่วน HTTP/3 (บน QUIC) แก้ได้ถึงระดับ transport
10. Caching
Cache static asset (เก็บไฟล์ static ไว้ฝั่ง browser นาน ๆ)
nginx
location ~* \.(jpg|jpeg|png|gif|ico|css|js|woff2)$ {
expires 1y; # หมดอายุใน 1 ปี
add_header Cache-Control "public, immutable"; # public = cache ที่ proxy ก็ได้
# immutable = บอก browser ว่า "ไม่ต้องมาถามใหม่"
access_log off; # ปิด log เพื่อลดภาระดิสก์ (asset โหลดถี่)
}Proxy cache (เก็บ response จาก backend ไว้ตอบ request ถัดไป)
💡 ถ้าลง Nginx ผ่าน
aptบน Debian/Ubuntu โฟลเดอร์/var/cache/nginxมักถูกสร้างไว้ให้แล้ว แต่ถ้าไม่มี (เช่นรันใน container แบบ minimal) ต้องสร้างเองก่อนnginx -tถึงจะผ่าน:sudo mkdir -p /var/cache/nginx && sudo chown nginx:nginx /var/cache/nginx
nginx
# วางใน http context (ด้านบนสุดของ http block — ไม่ใช่ใน server หรือ location)
proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=api_cache:100m
max_size=10g inactive=60m use_temp_path=off;
# วางใน location ที่ต้องการเปิด cache
location /api/products/ {
proxy_cache api_cache;
proxy_cache_key "$scheme$host$request_uri";
proxy_cache_valid 200 5m; # cache 200 OK ไว้ 5 นาที
proxy_cache_valid 404 1m; # cache 404 ไว้ 1 นาที
proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
proxy_cache_lock on;
add_header X-Cache-Status $upstream_cache_status;
proxy_pass http://backend;
}$upstream_cache_status มีค่าได้: MISS, HIT, EXPIRED, STALE, UPDATING, BYPASS — ที่เจอบ่อยสุดคือ HIT (ตอบจาก cache เลย ไม่ไปหา backend), MISS (ยังไม่มีใน cache เลยต้องไปถาม backend ก่อน) และ STALE (cache หมดอายุแล้วแต่ยังใช้ตอบไปพลาง ๆ ระหว่างรอ backend ตอบค่าใหม่ — เกิดจาก proxy_cache_use_stale)
💡
proxy_cache_lock onป้องกัน cache stampede — ปัญหาตอน cache หมดอายุ พอดีกับมี request หลายตัวมาพร้อมกัน ทุกตัวจะวิ่งเข้าหา backend พร้อมกัน (backend ระเบิด)proxy_cache_lockบังคับให้แค่ request แรกเท่านั้นที่ไปถาม backend ส่วนตัวอื่นรอผลแล้วใช้ cache ที่เพิ่ง refresh
⚠️ Cache invalidation ยาก: Nginx OSS ไม่มี built-in mechanism สำหรับ purge cache (ลบ cache ก่อนหมดอายุ) — มีแค่ในตัวเชิงพาณิชย์ Nginx Plus วิธีเลี่ยง: ใช้ third-party module
ngx_cache_purge, ลบไฟล์ใน/var/cache/nginxตรง ๆ (ทื่อแต่ใช้ได้), หรือเปลี่ยนกลยุทธ์เป็น short TTL + stale-while-revalidate
11. Rate Limiting
เพื่อกัน abuse และ DDoS (Distributed Denial of Service = การโจมตีโดยส่ง request ถล่มจากหลายเครื่องพร้อมกัน) เบื้องต้น Nginx จำกัดจำนวน request ต่อวินาทีจากแต่ละ IP ได้ — กำหนด "zone" บอกอัตราที่ยอม (rate) แล้ว apply ที่ location ต่าง ๆ โดย endpoint อ่อนไหวอย่าง /login ควรเข้มกว่า ส่วน burst/nodelay คุมว่าจะให้ทะลักชั่วคราวได้แค่ไหน:
nginx
# วางใน http context
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s;
limit_conn_zone $binary_remote_addr zone=conn_limit:10m;
server {
location /api/ {
limit_req zone=api_limit burst=20 nodelay; # 10 req/วินาที, รับ burst (ทะลัก) ได้ 20
limit_conn conn_limit 5; # connection พร้อมกันสูงสุด 5
proxy_pass http://backend;
}
location /login {
limit_req zone=api_limit burst=3; # เข้มที่ /login (กัน brute force)
}
}burst= ขนาดคิวที่รองรับ traffic ทะลักชั่วคราวnodelay= ไม่ delay request ใน burst (ปล่อยผ่านทันทีจนกว่าจะเต็ม จากนั้น reject)delay=N= เริ่ม delay request หลังจากผ่าน N ตัวแรกใน burst
⚠️
$binary_remote_addrไม่ใช่ IP ของผู้ใช้จริงเสมอ — ถ้าเว็บคุณอยู่หลัง CDN (Cloudflare, CloudFront) หรือ load balancer ที่ระดับสูงกว่า$binary_remote_addrจะเป็น IP ของ CDN ทำให้ rate limit กลายเป็น "CDN ทั้งตัวอนุญาตได้กี่ req/s" ไม่ใช่ "user แต่ละคน" → ต้องตั้งset_real_ip_from <CDN_CIDR>;+real_ip_header X-Forwarded-For;ก่อน เพื่อให้ Nginx เอา IP จริงจาก header
💡 rate
5r/m(5 ครั้งต่อนาที) ใช้ได้ตั้งแต่ Nginx 1.x ขึ้นไป แต่ทำงานคนละโครงสร้างกับr/s—r/mจะแปลเป็น "1 request ทุก 12 วินาที" ภายใน ไม่ใช่ "5 request ใน 1 นาทีไหนก็ได้" ถ้าต้องการจำกัดจริง ๆ ต่อช่วงเวลา ให้ตั้งburstประกอบ
12. Security Headers
Security header แต่ละตัวมีหน้าที่คุ้มกัน browser ของผู้ใช้จากการโจมตีคนละแบบ — ใส่ครบช่วย "ลดพื้นที่โดน hack" ได้เยอะ ตัวที่ใช้บ่อย:
| Header | กันอะไร |
|---|---|
X-Frame-Options: DENY | กัน clickjacking — กัน site อื่นใส่เว็บคุณใน <iframe> (โดน CSP frame-ancestors รุ่นใหม่กว่าแทนได้ แต่ใส่ทั้งคู่เพื่อรองรับ browser เก่า) |
X-Content-Type-Options: nosniff | กัน browser เดา MIME type เอง (ป้องกัน XSS ผ่านไฟล์อัปโหลด) |
Referrer-Policy: no-referrer | ไม่ส่ง header Referer ออกไป → กันข้อมูล URL ภายในรั่ว |
Permissions-Policy | ปิดสิทธิ์ API ของ browser (กล้อง/ไมค์/GPS) ถ้าเว็บไม่ต้องใช้ |
Content-Security-Policy (CSP) | กัน XSS โดยบอกว่าเว็บโหลด resource ได้จาก source ไหนบ้าง |
Strict-Transport-Security (HSTS) | บังคับ browser ใช้ HTTPS เท่านั้น (ดู §8 เรื่อง preload) |
Cross-Origin-Opener-Policy (COOP) | แยก browsing context ของหน้านี้ออกจากหน้าอื่น (กัน cross-window attack) |
Cross-Origin-Embedder-Policy (COEP) | บังคับให้ resource cross-origin ต้อง opt-in (จำเป็นถ้าใช้ SharedArrayBuffer) |
Cross-Origin-Resource-Policy (CORP) | จำกัดว่าใครฝัง resource ของเว็บนี้ได้ |
nginx
add_header X-Frame-Options "DENY" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "no-referrer" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
# === CSP (Content-Security-Policy) ===
# ⚠️ "default-src 'self'" อย่างเดียวคือ naive — จะ block:
# - Google Analytics, Google Tag Manager, Sentry (ทุก script ภายนอก)
# - Google Fonts, fonts จาก CDN
# - inline <style> ใน HTML, inline <script>
# มือใหม่ deploy ปุ๊บ เว็บแตก ปิดทันที กลายเป็นไม่มี CSP เลย
# CSP จริง ๆ ต้อง audit ทุก source ที่เว็บใช้ + เปิด CSP-Report-Only ก่อนแล้วค่อยบังคับใช้
#
# ⚠️ สำคัญ — nonce ต้องสร้างที่ application ไม่ใช่ Nginx:
# $request_id ของ Nginx ไม่ใช่ค่า random ที่ปลอดภัยเพียงพอสำหรับ CSP nonce
# เพราะมันอาศัยข้อมูลที่คาดเดาได้ (timestamp + worker PID) ไม่ใช่ CSPRNG
# (Cryptographically Secure Pseudo-Random Number Generator)
# → application ต้องสร้าง nonce เองด้วย CSPRNG เช่น:
# Java: SecureRandom / Python: secrets.token_hex / Node.js: crypto.randomBytes
# แล้วส่งออกมาทั้งใน HTML <script nonce="..."> และใน CSP header พร้อมกัน
# ตัวอย่างด้านล่างใช้ $request_id เพื่อแสดงโครงสร้าง CSP เท่านั้น
# — อย่านำ $request_id ไปใช้จริงใน production โดยไม่เข้าใจข้อจำกัดนี้
add_header Content-Security-Policy "
default-src 'self';
script-src 'self' https://www.googletagmanager.com;
style-src 'self' https://fonts.googleapis.com;
font-src 'self' https://fonts.gstatic.com;
img-src 'self' data: https:;
connect-src 'self' https://api.example.com;
frame-ancestors 'none';
base-uri 'self';
form-action 'self';
" always;
# หมายเหตุ: ถ้าต้องการใช้ nonce กับ inline script ให้ application inject ทั้ง nonce
# ใน <script nonce="..."> และใน header นี้พร้อมกัน — ดูเอกสาร application framework ของคุณ
# Modern cross-origin isolation — ข้ามหัวข้อนี้ได้ถ้าเว็บคุณไม่ได้ใช้ multi-threading ขั้นสูงใน browser
# (เปิดเมื่อต้องการ SharedArrayBuffer — ฟีเจอร์ให้ JavaScript หลาย thread ใช้หน่วยความจำร่วมกัน /
# high-resolution timer — นาฬิกาความละเอียดสูงสำหรับวัด performance)
add_header Cross-Origin-Opener-Policy "same-origin" always;
add_header Cross-Origin-Embedder-Policy "require-corp" always;
add_header Cross-Origin-Resource-Policy "same-origin" always;⚠️
add_headerกับ inheritance: Nginx มีกฎ "ถ้า child block (เช่นlocation) เรียกadd_headerแม้แต่ครั้งเดียว → header ที่ตั้งใน parent block ทั้งหมดจะหายไป" ดังนั้นใช้ flagalwaysทุก header (ส่งทั้งกรณี success และ error) เสมอ และถ้าจำเป็นต้องadd_headerในlocationก็ต้องเขียน security header ทั้งชุดซ้ำให้ครบ ไม่ใช่แค่ตัวที่ต้องการเพิ่ม ทางออกที่สะอาดกว่า: เก็บ header ทั้งหมดไว้ในไฟล์เดียวแล้วincludeทั้งในserverและlocationที่ override
13. WebSocket Proxy
WebSocket ต้อง "upgrade" connection จาก HTTP ธรรมดาเป็น connection สองทางที่เปิดค้างไว้ — การ proxy ผ่าน Nginx จึงต้องส่ง header Upgrade/Connection ให้ถูกและตั้ง proxy_read_timeout ยาว ๆ ไม่งั้น connection จะถูกตัดกลางคัน:
nginx
location /ws/ {
proxy_pass http://backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 86400; # 24h — WebSocket long-lived
}14. gRPC Proxy
gRPC (gRPC Remote Procedure Call — framework RPC ของ Google สำหรับเรียกฟังก์ชันข้าม service เหมือนเรียก local function) วิ่งบน HTTP/2 ทำให้ proxy แบบ HTTP ธรรมดาไม่ได้ — Nginx มี directive เฉพาะคือ grpc_pass (และ grpcs:// สำหรับ gRPC ที่มี TLS) ที่เข้าใจโปรโตคอลนี้ ใช้เมื่อต้องวาง Nginx หน้า microservice ที่สื่อสารด้วย gRPC:
nginx
location / {
grpc_pass grpc://backend:50051;
error_page 502 = /error502grpc;
grpc_set_header X-Real-IP $remote_addr;
}grpc:// หรือ grpcs:// (TLS) แทน proxy_pass http://
15. Performance Tuning
📊 อ่านก่อนปรับ — วัดให้รู้ก่อนว่าติดตรงไหน: ค่า default ของ Nginx เพียงพอสำหรับเว็บส่วนใหญ่แล้ว — อย่า copy-paste config ด้านล่างไปทับโดยไม่ได้วัด คำสั่งด้านล่างต้องอาศัยความคุ้นเคยกับ Linux CLI (คำสั่งพื้นฐานที่ครอบคลุมไปแล้วในบทต้น ๆ ของ DevOps) — ถ้ายังไม่คุ้น ข้ามขั้นตอนวัดไปเลยก็ได้ แล้วใช้ค่า default ของ Nginx ไปก่อน เริ่มจาก:
- ดูว่าติด CPU หรือ network (CPU bound = CPU เต็ม ขยับไม่ได้ / network bound = แบนด์วิดท์เต็ม):
htopคู่กับss -s(socket summary = สรุปจำนวน socket)- ดูจำนวน file descriptor ที่ใช้จริง (file descriptor = ตัวเลขที่ OS ใช้แทนทุกอย่างที่เปิดอยู่ ทั้งไฟล์/socket):
cat /proc/$(pgrep -f "nginx: master")/limits | grep -i open- ดูสถานะของ connection (เช่น ESTABLISHED, TIME_WAIT):
ss -tan | awk '{print $1}' | sort | uniq -c- วิเคราะห์ traffic ของตัวเองว่าอยู่ระดับไหน (น้อย/กลาง/สูง) ก่อนเลือกค่าในหัวข้อนี้
ค่าด้านล่างเป็น จุดตั้งต้น (starting point) สำหรับ traffic สูง (หลายพัน connection ต่อวินาทีขึ้นไป) — เว็บที่ traffic ต่ำกว่านี้ใช้ค่า default ดีกว่า
Nginx เร็วอยู่แล้วโดย default แต่ปรับให้รีดประสิทธิภาพสูงสุดได้อีก — ตั้งจำนวน worker ตามจำนวน CPU core, เพิ่มจำนวน connection ต่อ worker, ปล่อยให้ Nginx เลือก event method เอง และเปิด gzip/sendfile หัวข้อนี้รวม tuning ที่คุ้มค่าสำหรับ traffic สูง:
Worker tuning
nginx
worker_processes auto; # auto = เท่ากับจำนวน CPU core
worker_rlimit_nofile 65536; # file descriptor limit ของแต่ละ worker
# (ต้อง >= worker_connections ไม่งั้น OS refuse connection)
events {
worker_connections 16384; # connection สูงสุดต่อ worker (ต้องน้อยกว่า worker_rlimit_nofile)
# use epoll; # ไม่ต้องระบุ — Nginx เลือกให้เองตาม OS
multi_accept on; # รับ connection ใหม่หลายตัวพร้อมกันในรอบเดียวของ event loop
}⚠️ gotcha:
worker_connections 16384ดูใหญ่ดี แต่ถ้า systemd limit (LimitNOFILE) หรือulimit -nของ user nginx ยังเป็น default 1024 ก็จะไม่มีประโยชน์ — OS จะ refuse connection ก่อน → ตั้งworker_rlimit_nofileใน Nginx, ตั้งLimitNOFILE=65536ใน systemd unit, และตั้ง/etc/security/limits.d/nginx.confให้พร้อมกันทั้งสามชั้นอีกจุดที่พลาดบ่อย: อย่ามอง
worker_rlimit_nofileกับworker_connectionsเป็นแค่ "ต้องมากกว่ากันนิดหน่อยก็พอ" — เวลา Nginx ทำหน้าที่ reverse proxy แต่ละ request ที่ active อยู่มักกิน 2 file descriptor (ฝั่ง client 1 + ฝั่ง upstream/backend อีก 1) ดังนั้นควรตั้งworker_rlimit_nofileให้มี headroom เผื่อไว้จริง ๆ ไม่ใช่แค่มากกว่าworker_connectionsแบบ 1:1
Buffering
nginx
client_body_buffer_size 16k;
client_max_body_size 100m; # default 1m — เปลี่ยนถ้ามี upload
client_header_buffer_size 1k;
large_client_header_buffers 4 8k;Timeout
nginx
client_body_timeout 12;
client_header_timeout 12;
keepalive_timeout 30;
send_timeout 10;File descriptor (จำนวนไฟล์/socket ที่เปิดพร้อมกันได้)
ตั้งระดับ OS — ulimit (user limit) คือเพดานที่ OS กำหนดต่อ user แต่ละคน Nginx เปิดเกินค่านี้ไม่ได้แม้ config จะอนุญาต:
bash
# /etc/security/limits.d/nginx.conf
nginx soft nofile 65536 # soft = ค่าเริ่มต้นที่ user เห็น
nginx hard nofile 65536 # hard = เพดานสูงสุด (root เท่านั้นที่ยกได้)ถ้ารันผ่าน systemd ต้องเพิ่มใน unit file ด้วย:
ini
# /etc/systemd/system/nginx.service.d/override.conf
[Service]
LimitNOFILE=65536Open file cache (สำหรับ static)
nginx
open_file_cache max=10000 inactive=60s;
open_file_cache_valid 60s;
open_file_cache_min_uses 2;
open_file_cache_errors on;16. Monitoring + Logging
จะดูแล Nginx ใน production ต้องมองเห็นว่ามันทำงานยังไง — เปิด stub_status ดูสถานะ connection สด ๆ, ตั้ง access log เป็น JSON เพื่อส่งเข้า Loki/Elasticsearch ได้ง่าย และ export metric ให้ Prometheus ส่วนนี้สอนตั้งทั้งสามอย่าง:
Status module
nginx
location /nginx_status {
stub_status on;
allow 127.0.0.1;
deny all;
}Output (พร้อมคำแปล):
text
Active connections: 291 ← จำนวน connection ที่เปิดอยู่ตอนนี้
server accepts handled requests ← header ของแถวล่าง
16630948 16630948 31070465 ← accepts | handled | requests ตั้งแต่เปิด Nginx
(accepts = รับเข้ามา, handled = ที่จัดการสำเร็จ,
requests = จำนวน HTTP request ทั้งหมด)
Reading: 6 Writing: 179 Keep-Alive: 106
↑ ↑ ↑
กำลังอ่าน กำลังเขียน เปิดค้างไว้รอ request ถัดไป
request header response (keepalive)JSON access log
nginx
log_format json_combined escape=json '{'
'"time":"$time_iso8601",'
'"remote":"$remote_addr",'
'"method":"$request_method",'
'"uri":"$request_uri",'
'"status":$status,'
'"bytes":$body_bytes_sent,'
'"referer":"$http_referer",'
'"agent":"$http_user_agent",'
'"rt":$request_time,'
'"ut":"$upstream_response_time",'
'"upstream":"$upstream_addr"'
'}';
access_log /var/log/nginx/access.json json_combined;ส่งเข้า Loki หรือ Elasticsearch ผ่าน:
- Promtail = agent ส่ง log ของ Grafana Loki (อ่าน file → ส่งเข้า Loki)
- Filebeat = agent ส่ง log ของ Elastic Stack (อ่าน file → ส่งเข้า Elasticsearch หรือ Logstash)
→ ดู observability/08-loki-and-logql.md
Prometheus exporter
ใช้ nginx-prometheus-exporter (อ่านจาก stub_status) หรือ vts module (VTS = Virtual host Traffic Status — third-party module ที่เก็บสถิติละเอียดกว่า เช่น แยกตาม server/upstream) → expose /metrics
⚠️ ข้อจำกัด: ทั้ง
stub_statusและvts moduleให้ metric รวม (aggregate) เท่านั้น — ไม่ได้แยกตาม route ทำให้สร้าง RED metrics (Rate, Errors, Duration ต่อ endpoint) ไม่ได้ ถ้าต้องการ RED metrics จริงจังต้องอ่าน access log แล้วประมวลด้วย Promtail/Vector หรือใช้ OpenTelemetry sidecar
17. Docker Use Case
วิธีที่นิยมที่สุดในการรัน Nginx ปัจจุบันคือใน container — ใช้เป็น web server เสิร์ฟ static site (เช่น build ของ React) หรือเป็น reverse proxy ก็ได้ ตัวอย่างนี้แสดง Dockerfile ที่ก๊อป config + ไฟล์ static เข้า image และเทคนิค template config ตาม env ด้วย envsubst:
dockerfile
# สำหรับ production: pin ด้วย SHA digest (ไม่ใช่แค่ tag) เพราะ tag ถูกเปลี่ยน image ภายหลังได้
# หา digest ของเวอร์ชันที่ต้องการด้วยคำสั่งนี้ก่อน:
# docker pull nginx:1.27-alpine
# docker inspect --format='{{index .RepoDigests 0}}' nginx:1.27-alpine
# แล้วนำค่า sha256:... ที่ได้มาแทนด้านล่าง — ตัวอย่างนี้ต้องแทนก่อนใช้งานจริง
FROM nginx:1.27-alpine@sha256:<วาง-digest-จาก-คำสั่งด้านบนตรงนี้>
COPY nginx.conf /etc/nginx/nginx.conf
COPY conf.d/ /etc/nginx/conf.d/
COPY dist/ /usr/share/nginx/html/
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]หรือใช้ entrypoint script ที่ template config ตาม environment variable:
bash
envsubst < /etc/nginx/conf.d/default.conf.tpl > /etc/nginx/conf.d/default.conf💡 2026 alternatives ใน container: ถ้าไม่ติด Nginx ตายตัว — Caddy เปิด HTTPS ให้อัตโนมัติ (auto-HTTPS) ไม่ต้องตั้ง certbot, OpenResty = Nginx + Lua scripting (เขียน logic ลึก ๆ ใน config ได้), Nginx Unit = app server รุ่นใหม่จากทีม Nginx รัน Python/Node/Go ในตัว, njs (NGINX JavaScript) = JavaScript module ใน Nginx เอง (อยู่ใน image official แล้ว) เขียน logic แทน Lua/C ได้
18. Common Pitfalls
proxy_pass http://backend/กับproxy_pass http://backend— การมีเครื่องหมาย/ตอนท้ายส่งผลให้พฤติกรรมต่างกัน- มี
/= ตัด path prefix ที่ matched กับlocationทิ้ง (strip path prefix) แล้วต่อส่วนที่เหลือ - ไม่มี
/= ส่ง full path ที่ client เรียกมาทั้งหมดไปยัง backend
- มี
- ลืมตั้ง
Hostheader → backend สับสนเรื่องชื่อโฮสต์เดิมที่ผู้ใช้เรียกเข้ามา (original hostname) — มีผลกับ app ที่อาศัย Host เพื่อ generate URL หรือ route หลายโดเมน client_max_body_sizeค่าเริ่มต้นแค่1m→ อัปโหลดไฟล์ใหญ่กว่า 1MB จะได้ HTTP 413 (Payload Too Large)- เรียก
add_headerในlocationจะลบ header ทั้งหมดที่ตั้งในserver(parent) → security header หาย server_tokens on(default) → response headerServer: nginx/1.27.xรั่วเลขเวอร์ชันให้ผู้ไม่หวังดี → ตั้งserver_tokens off;access_log /dev/stdoutใน Docker แต่ไม่ใส่ buffer → CPU พุ่งบนเว็บที่ log หนาแน่น (เพราะ flush ทุกบรรทัด)- ลืม redirect HTTP → HTTPS → ผู้ใช้ยังเข้าผ่าน HTTP ได้
worker_connectionsต่ำกว่า traffic จริง → 504 หรือ connection refusedproxy_buffering on+ response แบบ streaming → response ค้างไม่ออก → ปิดproxy_buffering off;สำหรับ SSE (Server-Sent Events = วิธีให้ server ส่งข้อมูลต่อเนื่องไปหา browser ผ่าน HTTP ปกติ) หรือ WebSocket
19. ตัวอย่าง Full Config (Production)
ปิดท้ายด้วยการรวมทุกอย่างในบทเป็น config สมบูรณ์ที่ใช้ใน production ได้จริง — ครบทั้ง worker tuning, gzip, security header, SSL, rate limit, reverse proxy และ cache อ่านไล่ลงมาเพื่อเห็นว่าแต่ละชิ้นวางตรงไหนในไฟล์เดียว:
nginx
# /etc/nginx/nginx.conf
user nginx;
worker_processes auto;
worker_rlimit_nofile 65536;
error_log /var/log/nginx/error.log warn;
pid /run/nginx.pid;
events {
worker_connections 16384;
# use epoll; # ไม่ต้องระบุ — Nginx เลือก event method ที่ดีสุดของแต่ละ OS เอง
multi_accept on;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
server_tokens off;
# JSON access log (เขียนเต็มรูปแบบ — ไม่ใช่ placeholder)
log_format main escape=json
'{'
'"time":"$time_iso8601",'
'"remote":"$remote_addr",'
'"method":"$request_method",'
'"uri":"$request_uri",'
'"status":$status,'
'"bytes":$body_bytes_sent,'
'"rt":$request_time,'
'"ut":"$upstream_response_time",'
'"upstream":"$upstream_addr",'
'"ua":"$http_user_agent"'
'}';
# buffer=64k flush=5s = สะสม log ใน memory แล้ว flush ทุก 5 วินาที หรือเมื่อ buffer เต็ม
# ลด disk I/O แต่ถ้า Nginx ตายกะทันหัน อาจเสีย log ในช่วง 5 วินาทีสุดท้าย
access_log /var/log/nginx/access.log main buffer=64k flush=5s;
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
keepalive_requests 1000;
types_hash_max_size 2048;
client_max_body_size 20m;
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_types text/plain text/css application/json application/javascript
text/xml application/xml application/xml+rss text/javascript;
# rate limit
limit_req_zone $binary_remote_addr zone=login_limit:10m rate=5r/m;
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=100r/s;
upstream backend {
least_conn;
server app-1:3000 max_fails=3 fail_timeout=30s;
server app-2:3000 max_fails=3 fail_timeout=30s;
server app-3:3000 max_fails=3 fail_timeout=30s;
keepalive 64;
}
# HTTP → HTTPS
server {
listen 80;
server_name example.com www.example.com;
location /.well-known/acme-challenge/ { root /var/www/certbot; }
location / { return 301 https://$host$request_uri; }
}
# HTTPS (+ HTTP/3)
server {
listen 443 ssl; # syntax ใหม่ตั้งแต่ Nginx 1.25.1 (http2 แยกออกเป็น directive เดี่ยว)
http2 on;
listen 443 quic reuseport; # HTTP/3 (ต้อง Nginx 1.25+ ที่ build รวม QUIC)
http3 on; # Nginx 1.27+ ใช้ directive นี้ได้
add_header Alt-Svc 'h3=":443"; ma=86400' always;
server_name example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
ssl_trusted_certificate /etc/letsencrypt/live/example.com/chain.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_session_cache shared:SSL:10m;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Frame-Options DENY always;
add_header X-Content-Type-Options nosniff always;
location /api/login {
limit_req zone=login_limit burst=2 nodelay;
proxy_pass http://backend;
include /etc/nginx/proxy_common.conf;
}
location /api/ {
limit_req zone=api_limit burst=200 nodelay;
proxy_pass http://backend;
include /etc/nginx/proxy_common.conf;
}
location / {
root /var/www/spa;
try_files $uri $uri/ /index.html;
}
}
}/etc/nginx/proxy_common.conf:
nginx
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;
proxy_set_header Connection "";
proxy_connect_timeout 5s;
proxy_read_timeout 60s;🛠️ Checkpoint 8 — ลงมือทำ
- ติดตั้ง Nginx + serve static SPA + reverse proxy
/apiไป backend Node - ใส่ TLS ผ่าน Let's Encrypt
- ตั้ง rate limit ที่ login (5 req/min) + เพิ่มที่ general API (100 req/s)
- เปิด proxy cache สำหรับ
/api/products5 นาที — ส่ง 100 req ติดกัน → ดูX-Cache-Status - ใช้
least_connload balance ไป 3 backend → kill 1 backend → ดู Nginx mark down - log เป็น JSON format → ส่งเข้า Loki → query
{job="nginx"} | json | status>=500 - ดู
nginx_status+ scrape ด้วย nginx-prometheus-exporter
สรุปบทที่ 8
ทบทวนแก่นของบทนี้ — Nginx ทำอะไรได้บ้าง (web server, reverse proxy, load balancer, edge router) และเครื่องมือสำคัญที่ครอบคลุมไป ตั้งแต่การจับคู่ location, การใช้ upstream, TLS กับ HTTP/2/3, การ cache, rate limit จนถึง monitoring:
- Nginx เป็นสถาปัตยกรรมแบบ event-driven (1 worker จัดการได้พัน connection)
- ใช้งานได้ 4 บทบาทหลัก — web server, reverse proxy (ตัวกลางฝั่ง server), load balancer (กระจายโหลด), edge router (ประตูฝั่งขอบ)
- การจับคู่
locationมีลำดับ — exact → preferred prefix → regex → prefix ปกติ upstream+ algorithm กระจายโหลด + health check = ระบบ load balance- TLS + HTTP/2/3 + caching + rate limit = พื้นฐานที่ระบบ production ต้องมี
- monitoring ผ่าน
stub_status+ JSON log + Prometheus exporter - ข้อพลาดที่พบบ่อย (common pitfalls) — เครื่องหมาย
/ท้ายproxy_pass, ลืมส่ง Host header,add_headerในlocationที่ลบของ parent ทิ้ง
🔤 Glossary · 📋 Style guide · 📅 last_verified: 2026-06-12