Skip to content

บทที่ 8 — Nginx ลึก (Reverse Proxy, Load Balancer, TLS, Performance)

← บทที่ 7 | สารบัญ DevOps | บทที่ 9: Traefik →

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 บทบาทหลัก:

  1. Web server — เสิร์ฟไฟล์ static (HTML, CSS, JS, รูปภาพ)
  2. Reverse proxy (รีเวิร์สพร็อกซี = ตัวกลางฝั่ง server) — รับ request จาก client แล้วส่งต่อให้ backend (Node, Python, Java, ...)
  3. Load balancer (ตัวกระจายโหลด) — กระจายโหลดไปยัง backend หลายตัว
  4. 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-alpine
bash
# ตรวจสอบว่าติดตั้งสำเร็จ
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):

  1. ตรงตัว (exact): example.com
  2. wildcard ฝั่งซ้าย (ดอกจันอยู่ซ้าย): *.example.com ใช้ครอบทุก subdomain
  3. wildcard ฝั่งขวา (ดอกจันอยู่ขวา): example.* ใช้ครอบทุก TLD
  4. regex (regular expression): ~^api\d+\.example\.com$
  5. ค่าเริ่มต้น — ใช้ 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):

  1. = ตรงตัว — ถ้าเจอใช้เลย ไม่ลองอย่างอื่น
  2. ^~ prefix แบบ preferred — ถ้าเจอใช้ ไม่ลอง regex
  3. regex (~ / ~*) ตามลำดับที่เขียนในไฟล์
  4. 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/sr/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 ทั้งหมดจะหายไป" ดังนั้นใช้ flag always ทุก 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=65536

Open 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

  1. proxy_pass http://backend/ กับ proxy_pass http://backend — การมีเครื่องหมาย / ตอนท้ายส่งผลให้พฤติกรรมต่างกัน
    • มี / = ตัด path prefix ที่ matched กับ location ทิ้ง (strip path prefix) แล้วต่อส่วนที่เหลือ
    • ไม่มี / = ส่ง full path ที่ client เรียกมาทั้งหมดไปยัง backend
  2. ลืมตั้ง Host header → backend สับสนเรื่องชื่อโฮสต์เดิมที่ผู้ใช้เรียกเข้ามา (original hostname) — มีผลกับ app ที่อาศัย Host เพื่อ generate URL หรือ route หลายโดเมน
  3. client_max_body_size ค่าเริ่มต้นแค่ 1m → อัปโหลดไฟล์ใหญ่กว่า 1MB จะได้ HTTP 413 (Payload Too Large)
  4. เรียก add_header ใน location จะลบ header ทั้งหมดที่ตั้งใน server (parent) → security header หาย
  5. server_tokens on (default) → response header Server: nginx/1.27.x รั่วเลขเวอร์ชันให้ผู้ไม่หวังดี → ตั้ง server_tokens off;
  6. access_log /dev/stdout ใน Docker แต่ไม่ใส่ buffer → CPU พุ่งบนเว็บที่ log หนาแน่น (เพราะ flush ทุกบรรทัด)
  7. ลืม redirect HTTPHTTPS → ผู้ใช้ยังเข้าผ่าน HTTP ได้
  8. worker_connections ต่ำกว่า traffic จริง → 504 หรือ connection refused
  9. proxy_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 — ลงมือทำ

  1. ติดตั้ง Nginx + serve static SPA + reverse proxy /api ไป backend Node
  2. ใส่ TLS ผ่าน Let's Encrypt
  3. ตั้ง rate limit ที่ login (5 req/min) + เพิ่มที่ general API (100 req/s)
  4. เปิด proxy cache สำหรับ /api/products 5 นาที — ส่ง 100 req ติดกัน → ดู X-Cache-Status
  5. ใช้ least_conn load balance ไป 3 backend → kill 1 backend → ดู Nginx mark down
  6. log เป็น JSON format → ส่งเข้า Loki → query {job="nginx"} | json | status>=500
  7. ดู 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

→ บทที่ 9: Traefik