วิธีตั้งค่า nginx reverse proxy อย่างละเอียดทุกขั้นตอน
เรียนรู้วิธีเขียนคอนฟิก nginx reverse proxy บน Ubuntu 24.04 ตั้งแต่การตั้งค่า proxy_pass การส่ง Header ที่จำเป็นสำหรับแอปพลิเคชัน การรองรับ Websockets จนถึงการจัดการไฟล์อัปโหลด
หน้าที่ของคอนฟิก nginx reverse proxy
nginx reverse proxy จะรับคำขอที่เข้ามาทางพอร์ต 80 และพอร์ต 443 แล้วส่งต่อคำขอแต่ละรายการไปยังแอปพลิเคชันที่กำลังฟัง (listen) อยู่บนพอร์ตภายในเครื่อง จากนั้นจึงส่งคำตอบของแอปพลิเคชันนั้นกลับไปยังเบราว์เซอร์ คอนฟิกดังกล่าวจะเป็นบล็อก server เพียงบล็อกเดียวและมีความยาวไม่มาก ความยากเกือบทั้งหมดจะอยู่ที่ 5 หรือ 6 บรรทัดที่ทำหน้าที่แจ้งให้แอปพลิเคชันของคุณทราบว่าไคลเอนต์ตัวจริงคือใครและใช้โปรโตคอลใด
เนื้อหาทั้งหมดด้านล่างนี้สร้างขึ้นใหม่บน Ubuntu 24.04 โดยใช้แพ็กเกจ nginx จาก distribution โดยมีจุดเริ่มต้นคือแอปพลิเคชันที่ตอบสนองบน 127.0.0.1:3000 อยู่แล้ว หากคุณยังไม่ได้ตัดสินใจเลือกใช้พร็อกซีตัวใด การเปรียบเทียบ nginx กับ Caddy และ Traefik คือบทความที่คุณควรอ่านเป็นอันดับแรก สิ่งที่ตามมาคือรายละเอียดของคอนฟิก nginx ทีละบรรทัด
ให้ทดสอบคอนฟิกเหล่านี้บนเซิร์ฟเวอร์ของคุณเอง ตรวจสอบการเปลี่ยนแปลงทุกครั้งด้วย sudo nginx -t ก่อนทำการ reload และอ่านผลลัพธ์ที่แสดงออกมา
ตำแหน่งที่ nginx เก็บไฟล์คอนฟิกบน Ubuntu
sudo apt update
sudo apt install -y nginx
ls -l /etc/nginx/sites-enabled/ไฟล์หลักคือ /etc/nginx/nginx.conf ซึ่งกำหนดตัวเลือกส่วนกลางไว้ภายในบล็อก http { } จากนั้นจะดึงไฟล์จากสองไดเรกทอรีคือ /etc/nginx/conf.d/*.conf และ /etc/nginx/sites-enabled/* บน Ubuntu และ Debian คุณสามารถเขียนไฟล์คอนฟิกแยกหนึ่งไฟล์ต่อหนึ่งเว็บไซต์ไว้ใน /etc/nginx/sites-available/ และเปิดใช้งานโดยการสร้าง symlink ไปยัง /etc/nginx/sites-enabled/ การลบ symlink จะเป็นการปิดใช้งานเว็บไซต์นั้นโดยที่ไฟล์คอนฟิกยังคงอยู่
คำสั่ง (directive) สองรายการที่ใช้ในภายหลังจะทำงานได้เฉพาะในบริบท http เท่านั้น ไม่สามารถใช้ภายในบล็อก server ได้ นั่นคือ map และ upstream ให้ใส่คำสั่งเหล่านี้ไว้ในไฟล์แยกต่างหากภายใต้ /etc/nginx/conf.d/ เนื่องจากไดเรกทอรีดังกล่าวถูกรวมเข้ามาที่ระดับ http
แพ็กเกจนี้มาพร้อมกับเว็บไซต์ที่เปิดใช้งานไว้แล้วชื่อ default ซึ่งถูกกำหนดค่าเป็น default_server หมายความว่ามันจะตอบสนองต่อทุกคำขอที่ส่วนหัว Host ไม่ตรงกับ server_name ใดๆ ในคอนฟิกของคุณ ตราบใดที่เว็บไซต์นี้ยังเปิดใช้งานอยู่ คำขอที่ไม่ตรงกับชื่อโดเมนของคุณจะถูกส่งไปยังเว็บไซต์นี้แทนที่จะเป็นแอปของคุณ ให้ลบ symlink ออกเมื่อเว็บไซต์ของคุณทำงานได้ตามปกติแล้ว
sudo rm /etc/nginx/sites-enabled/default
sudo nginx -t
sudo systemctl reload nginxบล็อกเซิร์ฟเวอร์ขนาดเล็กที่สุดสำหรับการทำ proxy ให้แอปเดียว
server {
listen 80;
listen [::]:80;
server_name app.example.com;
location / {
proxy_pass http://127.0.0.1:3000;
}
}บันทึกไฟล์นี้เป็น /etc/nginx/sites-available/app.example.com จากนั้นเปิดใช้งานและโหลดการตั้งค่าใหม่
sudo ln -s /etc/nginx/sites-available/app.example.com /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
curl -sI -H 'Host: app.example.com' http://127.0.0.1/listen 80; จะผูกกับ IPv4 และ listen [::]:80; จะผูกกับ IPv6 หากละบรรทัดที่สองไว้ ผู้เข้าชมที่ DNS (domain name system) ค้นหาแล้วได้ AAAA record สำหรับเซิร์ฟเวอร์ของคุณจะพบกับการปฏิเสธการเชื่อมต่อ ในขณะที่ผู้ใช้ IPv4 ทุกคนจะใช้งานเว็บไซต์ได้ตามปกติ รายงานข้อผิดพลาดที่คุณจะได้รับคือ "มันใช้งานได้สำหรับฉัน"
server_name จะถูกนำไปจับคู่กับ header Host ที่เบราว์เซอร์ส่งมา คุณสามารถระบุชื่อได้หลายชื่อโดยคั่นด้วยช่องว่าง หากไม่มีบล็อกใดตรงกัน nginx จะใช้บล็อกที่เป็น default_server ซึ่งเป็นเหตุผลว่าทำไมเว็บไซต์ที่ติดตั้งมาพร้อมแพ็กเกจจึงต้องถูกลบออก
location / เป็นการจับคู่แบบ prefix กับ path ของคำขอ และ / จะจับคู่กับทุก path ส่วน proxy_pass คือที่อยู่ซึ่ง nginx จะเปิดการเชื่อมต่อไปยังแอปนั้น ให้คงการผูกแอปไว้ที่ 127.0.0.1 เพื่อให้เส้นทางเดียวที่เข้าถึงได้คือผ่าน nginx หากแอปทำงานในคอนเทนเนอร์ ให้เผยแพร่พอร์ตเป็น 127.0.0.1:3000:3000 แทนที่จะเป็น 3000:3000 เพราะ Docker จะเขียนกฎของตัวเองและเผยแพร่พอร์ตโดยข้าม ufw ไปเลย ดังนั้นพอร์ตที่เผยแพร่โดยตรงจะสามารถเข้าถึงได้จากอินเทอร์เน็ตไม่ว่า firewall ของคุณจะตั้งค่าไว้อย่างไรก็ตาม
บรรทัด curl จะส่ง header Host ที่ถูกต้องจากตัวเซิร์ฟเวอร์เอง เพื่อให้คุณสามารถทดสอบบล็อกได้ก่อนที่จะชี้ DNS ไปยังที่ใดก็ตาม
สิ่งที่ nginx ส่งไปยัง upstream เมื่อคุณไม่ได้กำหนดค่าอื่นเพิ่มเติม
proxy_pass เพียงอย่างเดียวจะซ่อนข้อมูลสี่ประการจากแอปพลิเคชันของคุณ
โดยค่าเริ่มต้น nginx จะสื่อสารกับ backend ด้วยโปรโตคอล HTTP/1.0 และส่ง Connection: close ทำให้ทุกคำขอต้องเปิดการเชื่อมต่อ upstream ใหม่เสมอและไม่สามารถอัปเกรดโปรโตคอลได้
ส่วนหัว Host จะถูกเขียนทับด้วยค่าใน proxy_pass ซึ่งก็คือ 127.0.0.1:3000 ส่งผลให้แอปที่สร้างลิงก์แบบสัมบูรณ์จาก Host สร้างลิงก์ที่ไม่มีใครภายนอกเซิร์ฟเวอร์สามารถเปิดใช้งานได้
การเชื่อมต่อที่ส่งถึงแอปมาจาก nginx ทำให้แอปมองเห็นที่อยู่ของไคลเอนต์เป็น 127.0.0.1 ส่งผลให้ทุกบรรทัดใน log และทุกการจำกัดอัตรา (rate limit) ภายในแอปบันทึกข้อมูลของ proxy แทนที่จะเป็นของผู้เข้าชมจริง
แอปไม่สามารถทราบได้ว่าเบราว์เซอร์ใช้ HTTPS เนื่องจากได้รับเป็นการเชื่อมต่อ HTTP ปกติผ่าน loopback address
การเพิ่มสี่บรรทัดนี้จะแก้ไขปัญหาทั้งหมดดังกล่าวได้
ส่วนหัว 4 รายการที่ต้องตั้งค่า และสิ่งที่แต่ละรายการช่วยให้แบ็กเอนด์มองเห็น
location / {
proxy_pass http://127.0.0.1:3000;
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;
}Host จะระบุชื่อโฮสต์ที่ผู้เข้าชมพิมพ์เข้ามา $host คือชื่อจากคำขอที่ตัดพอร์ตออกและแปลงเป็นตัวพิมพ์เล็กทั้งหมด การตั้งค่านี้ช่วยให้แอปพลิเคชันของคุณสร้าง absolute URL ที่ถูกต้องได้ เช่น การเปลี่ยนเส้นทางหลังล็อกอิน หรือลิงก์ในอีเมลรีเซ็ตรหัสผ่าน หากไม่ตั้งค่านี้ URL เหล่านั้นจะชี้ไปที่ 127.0.0.1:3000 ส่งผลให้การล็อกอินนำเบราว์เซอร์ไปยังที่อยู่ที่ปฏิเสธการเชื่อมต่อ หากแอปของคุณจำเป็นต้องใช้พอร์ตด้วยเนื่องจากรันบนพอร์ต 8080 ให้ใช้ $http_host ซึ่งเป็นส่วนหัวที่ส่งมาจากไคลเอนต์โดยตรง
X-Real-IP จะระบุค่าเพียงค่าเดียวคือ $remote_addr ซึ่งเป็นที่อยู่ที่ nginx ยอมรับการเชื่อมต่อเข้ามา แอปพลิเคชันจะอ่านค่านี้เพื่อใช้ใน access log และการทำ rate limiting ของตนเอง
X-Forwarded-For จะระบุรายการที่อยู่ $proxy_add_x_forwarded_for จะทำการต่อท้าย $remote_addr เข้ากับค่าเดิมที่ไคลเอนต์ส่งมาในส่วนหัวนั้น ทำให้ค่าที่ได้ถูกคั่นด้วยเครื่องหมายจุลภาคและรายการที่ nginx เพิ่มเข้าไปจะอยู่ท้ายสุด รายละเอียดนี้เป็นตัวตัดสินว่าส่วนหัวนั้นเชื่อถือได้หรือไม่ เนื่องจากไคลเอนต์สามารถส่ง X-Forwarded-For ใดๆ มาก็ได้ แอปที่อ่านค่ารายการแรกจึงอาจถูกหลอกด้วยที่อยู่ปลอมได้ เมื่อ nginx ทำหน้าที่เป็น edge server ให้เขียน $remote_addr แทนเพื่อละทิ้งค่าที่ไคลเอนต์ส่งมา หากมี CDN หรือพร็อกซีอื่นอยู่ด้านหน้า ให้ใช้ set_real_ip_from และ real_ip_header จากโมดูล realip เพื่อให้ $remote_addr กลายเป็นที่อยู่จริงของไคลเอนต์
X-Forwarded-Proto จะระบุค่า http หรือ https เฟรมเวิร์กจะอ่านค่านี้เพื่อตัดสินใจว่าจะทำเครื่องหมายคุกกี้เป็น Secure หรือบังคับเปลี่ยนเส้นทางไปยัง HTTPS หรือไม่ หากละเว้นส่วนนี้ในไซต์ที่ใช้ TLS แอปที่ตั้งค่าให้บังคับ HTTPS จะมองเห็นค่าเป็น http และตอบกลับด้วยการเปลี่ยนเส้นทางไปยังที่อยู่ HTTPS จากนั้นเมื่อได้รับคำขอถัดไปผ่าน nginx ก็ยังคงมองเห็นเป็น http และเปลี่ยนเส้นทางซ้ำอีกครั้ง จนเบราว์เซอร์ยอมแพ้และแสดงข้อผิดพลาด ERR_TOO_MANY_REDIRECTS
การทำซ้ำบรรทัดทั้งสี่นี้ในทุก location จะทำให้การตั้งค่าไม่สอดคล้องกัน ควรนำไปไว้ในไฟล์เดียวแล้วใช้คำสั่ง include แทน
# /etc/nginx/snippets/proxy-headers.conf
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;location / {
include snippets/proxy-headers.conf;
proxy_pass http://127.0.0.1:3000;
}การสืบทอดค่าในส่วนนี้มีข้อควรระวัง คือ location จะสืบทอดคำสั่ง proxy_set_header จาก server block ก็ต่อเมื่อ location นั้นไม่มีการกำหนดค่าของตนเองเลย หากคุณเพิ่ม proxy_set_header เข้าไปใน location เพียงรายการเดียว ส่วนหัวทั้งหมดที่กำหนดไว้ในระดับ server จะถูกยกเลิกสำหรับ location นั้น ดังนั้นควรเก็บค่าทั้งหมดไว้ในระดับเดียว หรือใช้ include เพื่อเรียกใช้ snippet ในทุก location ที่มีการทำ proxy
เหตุใดแอป WebSocket ของฉันจึงเชื่อมต่อได้แล้วตัดการเชื่อมต่อ?
เนื่องจากค่าเริ่มต้นไม่อนุญาตให้ทำการอัปเกรด และค่า read timeout เริ่มต้นจะปิด tunnel ที่ไม่มีการใช้งานหลังจากผ่านไป 60 วินาที WebSocket เริ่มต้นจากการเป็นคำขอ HTTP ที่มี Upgrade: websocket และ Connection: Upgrade ซึ่งเป็น hop-by-hop headers หมายความว่า proxy ควรเป็นผู้จัดการส่วนหัวเหล่านี้แทนที่จะส่งต่อไปยังปลายทาง และ HTTP/1.0 ไม่มีกลไกการอัปเกรดใดๆ ทั้งสิ้น ดังนั้นจึงต้องกำหนดค่าทั้งสองส่วนนี้กลับเข้าไปด้วยตนเอง
ให้ใส่ map ไว้ในบริบท http โดยแยกไว้ในไฟล์ของตนเอง
# /etc/nginx/conf.d/websocket.conf
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}จากนั้นจึงกำหนดค่าในส่วน location
location / {
include snippets/proxy-headers.conf;
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
}การใช้ map มีไว้เพื่อให้ location เดียวสามารถรองรับการรับส่งข้อมูลได้ทั้งสองประเภท ในคำขอปกติ $http_upgrade จะว่างเปล่า ดังนั้น $connection_upgrade จะกลายเป็น close แต่ในคำขออัปเกรด ค่านี้จะมี websocket ดังนั้น header ที่ส่งไปยัง upstream จึงเป็น Connection: upgrade การกำหนดค่า proxy_set_header Connection "upgrade"; แบบตายตัวจะส่ง header ดังกล่าวไปกับทุกคำขอหน้าเว็บปกติด้วย ซึ่ง backend บางตัวอาจตอบกลับคำขอเหล่านั้นด้วยสถานะ 400
proxy_read_timeout คือสาเหตุที่ทำให้เกิดรายงานว่า "โหลดได้แล้วหยุดอัปเดต" โดยค่าเริ่มต้นคือ 60 วินาที ซึ่งเป็นการวัดระยะห่างระหว่างการอ่านข้อมูลจาก backend สองครั้ง ไม่ใช่ระยะเวลาทั้งหมดของอายุการเชื่อมต่อ WebSocket ที่ไม่มีการรับส่งข้อมูลเป็นเวลา 60 วินาทีจะถูกปิดโดย nginx และใน console ของเบราว์เซอร์จะแสดงการปิด socket ด้วยรหัส 1006 แอปที่ส่ง heartbeat ของตนเองบ่อยกว่านาทีละครั้งจะไม่พบปัญหานี้ ส่วนแอปที่ไม่ส่งจะถูกตัดการเชื่อมต่อเมื่อครบนาที ปัญหานี้มักพบได้บ่อยในโปรแกรมแก้ไขแบบเรียลไทม์และแดชบอร์ด โดยมีตัวอย่างที่พบบ่อยคือ อินสแตนซ์ n8n ที่โฮสต์เองหลัง HTTPS
เหตุใด trailing slash ใน proxy_pass จึงเปลี่ยน URL ของคุณ
กฎนี้สรุปได้ในประโยคเดียว หาก proxy_pass ลงท้ายด้วย URI (uniform resource identifier) แม้จะเป็นเพียง / เปล่าๆ nginx จะลบส่วนของ request path ที่ตรงกับ prefix ของ location ออก แล้วนำ URI นั้นไปแทนที่ แต่หาก proxy_pass สิ้นสุดที่ host และ port ตัว request path จะถูกส่งต่อไปโดยไม่มีการเปลี่ยนแปลงใดๆ
location /app/ {
proxy_pass http://127.0.0.1:3000/;
}คำขอสำหรับ /app/status จะไปถึง backend ในรูปแบบ /status
location /app/ {
proxy_pass http://127.0.0.1:3000;
}คำขอสำหรับ /app/status จะไปถึง backend ในรูปแบบ /app/status
รูปแบบที่คุณควรเลือกขึ้นอยู่กับแอปพลิเคชันนั้นๆ แอปที่มีการตั้งค่า base-path หรือ sub-folder จะต้องการรูปแบบที่สอง โดยต้องระบุค่า /app ไว้ในการตั้งค่าด้วย ส่วนแอปที่ไม่รู้จัก prefix ใดๆ เลยจะต้องการรูปแบบแรก รูปแบบแรกมีข้อเสียที่เห็นได้ชัดทันที คือ HTML ที่แอปส่งกลับมายังคงมี absolute path เช่น /static/main.css อยู่ ซึ่งเบราว์เซอร์จะร้องขอไฟล์เหล่านั้นจาก root ของเว็บไซต์ แต่เนื่องจากไม่มี location ใดที่ตรงกัน หน้าเว็บจึงแสดงผลโดยไม่มีการจัดรูปแบบ (styling) ในแท็บ network ของเบราว์เซอร์จะแสดงว่าคำขอ asset เหล่านั้นได้รับสถานะ 404 วิธีแก้ไขคือการตั้งค่า base-path ภายในตัวแอปเอง หรือใช้ location /static/ ตัวที่สองที่ชี้ไปยัง backend เดียวกัน
location แบบ regex ไม่สามารถระบุ URI ใน proxy_pass ได้ โดย sudo nginx -t จะปฏิเสธการตั้งค่าดังกล่าวและระบุสาเหตุไว้ว่า "proxy_pass" cannot have URI part in location given by regular expression, or inside named location, or inside "if" statement, or inside "limit_except" block
ปัญหาประเภทนี้ทั้งหมดจะหมดไปเมื่อแต่ละแอปมีชื่อโดเมนเป็นของตนเองผ่าน app.example.com ซึ่งทำ proxy มาจาก location / การใช้ sub-path จะคุ้มค่ากับความยุ่งยากก็ต่อเมื่อคุณไม่สามารถเพิ่ม DNS record ได้เท่านั้น
ฉันจะกำหนด backend มากกว่าหนึ่งรายการภายใต้ชื่อเดียวได้อย่างไร
ให้ใช้บล็อก upstream ซึ่งอยู่ในบริบทของ http ดังนั้นให้เขียนไว้เหนือบล็อก server ในไฟล์เดียวกัน หรือใน /etc/nginx/conf.d/
upstream app_backend {
least_conn;
server 127.0.0.1:3000 max_fails=3 fail_timeout=30s;
server 127.0.0.1:3001 max_fails=3 fail_timeout=30s;
keepalive 32;
}จากนั้นระบุชื่อในส่วน location ด้วยคำสั่ง proxy_pass http://app_backend;
วิธีการเริ่มต้นคือ round robin ส่วน least_conn จะส่งคำขอแต่ละรายการไปยัง backend ที่มีการเชื่อมต่อใช้งานอยู่น้อยที่สุด ซึ่งเหมาะกับคำขอที่มีระยะเวลาประมวลผลไม่เท่ากัน ส่วน ip_hash จะผูกที่อยู่ของไคลเอนต์หนึ่งรายการเข้ากับ backend หนึ่งรายการ คุณจำเป็นต้องใช้ ip_hash เมื่อแอปพลิเคชันเก็บ session ไว้ในหน่วยความจำของตัวเอง เพราะการทำ round robin สลับไปมาระหว่างสอง backend จะทำให้ผู้ใช้หลุดจากระบบแบบสุ่มเมื่อคำขอถูกส่งไปยัง instance ที่ไม่มีข้อมูล session นั้น การย้าย session ไปเก็บใน shared storage เป็นวิธีแก้ไขที่ดีกว่า
max_fails=3 fail_timeout=30s หมายความว่าหากการเชื่อมต่อล้มเหลว 3 ครั้งภายใน 30 วินาที เซิร์ฟเวอร์นั้นจะถูกนำออกจากระบบเป็นเวลา 30 วินาที เมื่อเซิร์ฟเวอร์ทุกตัวในบล็อกอยู่ในสถานะดังกล่าว ไคลเอนต์จะได้รับข้อผิดพลาด 502 และ log จะแสดงข้อความ no live upstreams while connecting to upstream
keepalive 32 จะคงการเชื่อมต่อที่ไม่ได้ใช้งานกับ backend ไว้สูงสุด 32 รายการต่อหนึ่ง worker process ซึ่งช่วยลดขั้นตอน TCP handshake ในคำขอส่วนใหญ่ วิธีนี้ใช้ได้กับ proxy_http_version 1.1 เท่านั้นและต้องไม่มี Connection: close ส่งไปยัง upstream หาก location เดียวกันมีการใช้ WebSocket map ด้วย ให้เปลี่ยนกรณีว่างจาก close เป็นสตริงว่าง เพื่อให้คำขอปกติไม่มี header Connection และสามารถนำการเชื่อมต่อที่พักไว้กลับมาใช้ใหม่ได้
map $http_upgrade $connection_upgrade {
default upgrade;
'' '';
}ชื่อภายในบล็อก upstream จะถูก resolve เมื่อ nginx เริ่มทำงาน หาก backend ของคุณเป็นคอนเทนเนอร์ที่ได้รับที่อยู่ใหม่เมื่อรีสตาร์ท nginx จะยังคงใช้ที่อยู่เดิมจนกว่าคุณจะ reload บริการ ภายในเครือข่าย Docker คุณสามารถย้ายการค้นหาที่อยู่ไปไว้ที่เวลาประมวลผลคำขอได้โดยใช้ embedded resolver
resolver 127.0.0.11 valid=10s;
set $backend http://app:3000;
proxy_pass $backend;เมื่อคอนเทนเนอร์มีการสร้างและทำลายบ่อยจนคุณต้องคอยแก้ไขไฟล์คอนฟิกของ nginx อยู่ตลอดเวลา การใช้ proxy ที่อ่านค่าจาก container labels จะเป็นเครื่องมือที่เหมาะสมกว่า Traefik สำหรับแอปพลิเคชัน Docker Compose หลายรายการ จะสร้างเส้นทาง (routes) จากตัวคอนเทนเนอร์โดยอัตโนมัติ
เหตุใดการอัปโหลดจึงล้มเหลวด้วยข้อผิดพลาด 413 Request Entity Too Large?
client_max_body_size มีค่าเริ่มต้นอยู่ที่ 1 เมกะไบต์ Nginx จะปฏิเสธคำขอที่มีขนาดใหญ่กว่านี้ก่อนที่แอปพลิเคชันของคุณจะได้รับข้อมูล และบันทึกข้อผิดพลาดไว้ใน client intended to send too large body คุณสามารถเพิ่มค่านี้ได้ใน server block หรือใน location ที่มีการอัปโหลดเกิดขึ้น
client_max_body_size 512m;การตั้งค่าเป็น 0 จะเป็นการปิดการตรวจสอบนี้โดยสมบูรณ์ อย่างไรก็ตาม แอปพลิเคชันเองก็มีขีดจำกัดของมันเช่นกัน ดังนั้นหากยังคงพบข้อผิดพลาด 413 หลังจากเปลี่ยนการตั้งค่านี้ แสดงว่าข้อผิดพลาดมาจากฝั่ง backend และคุณควรตรวจสอบการตั้งค่าการอัปโหลดของตัวแอปพลิเคชันเองเป็นลำดับถัดไป
โดยปกติแล้ว Nginx จะอ่าน request body ทั้งหมดก่อนที่จะเปิดการเชื่อมต่อกับ upstream โดยจะเขียนข้อมูลขนาดใหญ่ลงในไฟล์ชั่วคราวบนดิสก์ก่อน วิธีนี้ช่วยป้องกันแอปพลิเคชันจากไคลเอนต์ที่ทำงานช้า เนื่องจาก backend จะได้รับข้อมูลการอัปโหลดด้วยความเร็วเต็มที่ภายในเครือข่ายท้องถิ่น สำหรับการอัปโหลดขนาดใหญ่มาก คุณสามารถเลือกใช้การสตรีมข้อมูลแทนได้
proxy_request_buffering off;ในกรณีนี้ backend จะได้รับ body ทันทีที่ข้อมูลมาถึงและต้องสามารถจัดการข้อมูลนั้นได้ นอกจากนี้ Nginx จะสูญเสียความสามารถในการลองส่งคำขอซ้ำไปยัง upstream อื่น เนื่องจากข้อมูล body ได้ถูกส่งออกไปหมดแล้ว
client_body_timeout ซึ่งมีค่าเริ่มต้นที่ 60 วินาที จะถูกนำมาใช้ระหว่างการอ่านข้อมูล body สองช่วงที่ต่อเนื่องกัน ไม่ใช่ระยะเวลาทั้งหมดของการอัปโหลด การอัปโหลดที่ช้าแต่มีความต่อเนื่องจะสามารถดำเนินการต่อไปได้ แต่หากการอัปโหลดหยุดชะงัก Nginx จะตัดการเชื่อมต่อทันที
การทำ Response buffering และการตั้งค่าที่ทำให้การแสดงผลแบบสดใช้งานไม่ได้
proxy_buffering ถูกเปิดใช้งานเป็นค่าเริ่มต้นและโดยปกติแล้วเป็นสิ่งที่คุณต้องการ Nginx จะอ่านการตอบกลับจากแอปของคุณให้เร็วที่สุดเท่าที่แอปจะเขียนได้ จากนั้นจะพักข้อมูลไว้และส่งต่อไปยังไคลเอนต์ที่ช้าตามความเร็วของไคลเอนต์นั้นๆ ทำให้ worker ของแอปทำงานเสร็จสิ้นได้เร็วขึ้น แทนที่จะต้องค้างอยู่กับการดาวน์โหลดที่ช้าตลอดทั้งกระบวนการ
การตั้งค่านี้จะทำให้การตอบกลับแบบ streaming ใช้งานไม่ได้ โดย Server-sent events และการแสดงผล log แบบสดจะไม่ปรากฏให้ผู้ใช้เห็นจนกว่า buffer จะเต็ม ให้ปิดการทำ buffering เฉพาะในตำแหน่งนั้นเท่านั้น
proxy_buffering off;หากคุณเป็นผู้ควบคุมแอป วิธีที่ดีกว่าคือการส่ง header X-Accel-Buffering: no ไปพร้อมกับการตอบกลับแบบ streaming เท่านั้น Nginx จะอ่าน header ดังกล่าวในการตอบกลับแต่ละครั้งและปิดการทำ buffering เฉพาะรายการนั้น ทำให้หน้าเว็บทั่วไปยังคงได้รับประโยชน์จากการทำ buffering อยู่
เมื่อ error log แจ้งว่า upstream sent too big header while reading response header from upstream แสดงว่า header ของการตอบกลับมีขนาดใหญ่เกินกว่าจะใส่ใน buffer เดียวได้ proxy_buffer_size มีค่าเริ่มต้นเป็นหน่วยความจำหนึ่งหน้า (memory page) ซึ่งมีขนาด 4 หรือ 8 กิโลไบต์ขึ้นอยู่กับแพลตฟอร์ม หากมี cookie ที่ยาวหรือ header สำหรับการยืนยันตัวตนที่มีขนาดใหญ่จะทำให้เกิดการ overflow ให้เพิ่มค่าทั้งสองรายการนี้ขึ้น
proxy_buffer_size 16k;
proxy_buffers 8 16k;TLS ควรอยู่ในส่วนใดของการตั้งค่านี้
ให้วางไว้ที่ Nginx ซึ่งอยู่หน้าสุดของทุกอย่าง TLS (Transport Layer Security) จะสิ้นสุดที่ตัว Proxy และการเชื่อมต่อจาก Nginx ไปยังแอปพลิเคชันจะเป็น HTTP แบบปกติผ่าน loopback address ซึ่งไม่มีส่วนอื่นในเครือข่ายที่สามารถอ่านข้อมูลได้ แอปพลิเคชันจะทราบว่าผู้เข้าชมใช้ HTTPS ผ่านทาง X-Forwarded-Proto ซึ่งเป็น header ลำดับที่สี่จากทั้งหมดสี่รายการ
ห้ามเขียน path ของ certificate ด้วยตนเอง ให้ชี้ DNS record มาที่เซิร์ฟเวอร์ เปิด firewall แล้วปล่อยให้ Certbot แก้ไข server block นี้โดยอัตโนมัติ มันจะเพิ่มบรรทัด listen 443 ssl พร้อมกับ path ssl_certificate รวมถึงการทำ redirect จากพอร์ต 80 การออกใบรับรอง Let's Encrypt สำหรับ Nginx ด้วย Certbot ได้ครอบคลุมขั้นตอนการออกใบรับรองและตัวตั้งเวลาต่ออายุไว้แล้ว
sudo ufw allow 'Nginx Full'
sudo ufw statusNginx Full คือ application profile ที่แพ็กเกจ Nginx ติดตั้งมาให้ ซึ่งจะเปิดพอร์ต 80 และพอร์ต 443 พร้อมกัน พอร์ต 80 จำเป็นต้องเปิดไว้สำหรับการทำ HTTP-01 renewal challenge แม้ว่าผู้เข้าชมทุกคนจะถูกเปลี่ยนเส้นทางไปยัง HTTPS แล้วก็ตาม
ทดสอบการตั้งค่าแล้วจึงโหลดซ้ำ
sudo nginx -t
sudo systemctl reload nginxnginx -t จะตรวจสอบไฟล์ที่ถูกรวมเข้ามาทั้งหมด หากพบข้อผิดพลาด ระบบจะรายงานว่าการทดสอบสำเร็จหรือแสดงไฟล์และบรรทัดที่เกิดปัญหาให้ทราบ โปรดอ่านผลลัพธ์ดังกล่าวก่อนทำการโหลดซ้ำ การโหลดซ้ำด้วยการตั้งค่าที่ผิดพลาดจะไม่ส่งผลใดๆ เนื่องจาก Nginx จะยังคงให้บริการด้วยการตั้งค่าเดิม ทำให้เว็บไซต์ยังคงทำงานได้ตามปกติในขณะที่การแก้ไขของคุณไม่มีผลใดๆ ทั้งสิ้น systemctl restart มีพฤติกรรมที่ต่างออกไปและส่งผลเสียมากกว่า เนื่องจากการรีสตาร์ทจะหยุดการทำงานของเซิร์ฟเวอร์ก่อน หากการตั้งค่ามีข้อผิดพลาดจะส่งผลให้ Nginx ไม่สามารถทำงานได้เลย ควรเลือกใช้การโหลดซ้ำเป็นค่าเริ่มต้น และเก็บการรีสตาร์ทไว้ใช้เฉพาะกรณีที่จำเป็นต้องทำเท่านั้น
sudo tail -f /var/log/nginx/error.log
sudo ss -lntp | grep -E ':(80|443|3000)'บรรทัด ss จะแสดงให้เห็นว่าโพรเซสใดกำลังใช้งานพอร์ตแต่ละพอร์ตอยู่ เพื่อให้คุณยืนยันได้ว่าแอปพลิเคชันกำลังรับฟังการเชื่อมต่อในจุดที่ proxy_pass ระบุไว้จริง
ความล้มเหลวที่คุณจะพบจริง
502 Bad Gateway พร้อม connect() failed (111: Connection refused) while connecting to upstream ใน error log ไม่มีบริการใดฟังอยู่ที่ที่อยู่ตามที่ระบุใน proxy_pass แอปพลิเคชันอาจหยุดทำงาน ถูกผูกไว้กับพอร์ตอื่น หรือผูกไว้กับที่อยู่ภายในคอนเทนเนอร์ที่โฮสต์ไม่สามารถเข้าถึงได้
502 พร้อม no live upstreams while connecting to upstream เซิร์ฟเวอร์ทุกตัวในบล็อก upstream ถูกทำเครื่องหมายว่าล้มเหลวโดย max_fails ให้ซ่อมแซมแบ็กเอนด์เหล่านั้น nginx จะลองเชื่อมต่อใหม่อีกครั้งเมื่อ fail_timeout หมดอายุ
504 Gateway Time-out พร้อม upstream timed out (110: Connection timed out) while reading response header from upstream แบ็กเอนด์ยอมรับการเชื่อมต่อแล้วแต่ไม่ส่งข้อมูลใดๆ กลับมาเป็นเวลา proxy_read_timeout วินาที การเพิ่มค่า timeout เป็นวิธีที่ถูกต้องสำหรับรายงานที่ช้าจริง แต่เป็นวิธีที่ผิดสำหรับแอปพลิเคชันที่ค้าง
ทุก path ส่งกลับ 404 จากแอปพลิเคชัน กฎเรื่อง trailing slash ได้เขียน path ใหม่ ให้เปรียบเทียบ path ที่แอปพลิเคชันบันทึกใน log กับ path ที่คุณร้องขอ
เว็บไซต์อื่นตอบกลับมา server_name ไม่ตรงกับ header Host คำขอดังกล่าวจึงหลุดไปที่บล็อก default_server
หน้าเว็บโหลดได้ แต่หน้าจอค้างหลังจากผ่านไปประมาณหนึ่งนาที นี่คือกรณีของ WebSocket: ขาดการจัดการ Upgrade หรือค่า proxy_read_timeout ยังคงเป็น 60 วินาที
FAQ
ทำไม nginx ถึงส่งสถานะ 502 Bad Gateway หลังจากที่ฉันเพิ่ม proxy_pass?
nginx ไม่สามารถเปิดการเชื่อมต่อกับที่อยู่ที่ระบุใน proxy_pass ได้ โดย log ข้อผิดพลาดที่ /var/log/nginx/error.log จะระบุสาเหตุไว้: connect() failed (111: Connection refused) while connecting to upstream หมายความว่าไม่มีบริการใดกำลังรอรับการเชื่อมต่อที่นั่น และ no live upstreams หมายความว่าเซิร์ฟเวอร์ทุกตัวในบล็อก upstream ถูกทำเครื่องหมายว่าล้มเหลว ให้รันคำสั่ง sudo ss -lntp | grep 3000 เพื่อตรวจสอบว่า process ใดกำลังใช้งานพอร์ตนั้นอยู่และผูกไว้กับที่อยู่ใด แอปที่ผูกไว้กับที่อยู่ภายใน container หรือผูกกับพอร์ตที่ไม่ตรงกับที่คุณระบุไว้จะทำให้เกิดข้อผิดพลาดนี้เสมอ
ทำไมแอปของฉันถึงตัดการเชื่อมต่อหลังจากผ่านไปประมาณหนึ่งนาทีเมื่ออยู่หลัง nginx?
การเชื่อมต่อดังกล่าวเป็น WebSocket และค่า proxy_read_timeout ยังคงเป็นค่าเริ่มต้นที่ 60 วินาที ซึ่งเป็นการวัดระยะห่างระหว่างการอ่านข้อมูลสองครั้งจาก backend หาก socket ไม่มีการรับส่งข้อมูล nginx จะปิดการเชื่อมต่อและ console ของเบราว์เซอร์จะรายงานรหัสปิดการเชื่อมต่อ 1006 ให้ตั้งค่า proxy_http_version 1.1, ส่งผ่าน Upgrade และ Connection โดยใช้ map ที่ $http_upgrade และเพิ่มค่า proxy_read_timeout ให้เป็นค่าที่เหมาะสม เช่น 3600s หากไม่มี header Upgrade การอัปเกรดการเชื่อมต่อจะไม่เกิดขึ้นเลย ทำให้แอปต้องกลับไปใช้การ polling หรือไม่แสดงผลการอัปเดตแบบเรียลไทม์
เครื่องหมาย slash ปิดท้ายใน proxy_pass มีผลหรือไม่?
มีผล และมันจะเปลี่ยน path ที่ backend ได้รับ ตัวอย่างเช่น หากใช้ location /app/ และ proxy_pass http://127.0.0.1:3000/ คำขอสำหรับ /app/status จะไปถึง backend ในรูปแบบ /status เนื่องจาก URI ใดๆ ที่ตามหลัง host และพอร์ตจะเข้ามาแทนที่ prefix ของ location ที่จับคู่ไว้ หากคุณตัด slash ตัวสุดท้ายออก คำขอเดียวกันจะไปถึงในรูปแบบ /app/status การตัด prefix มักจะทำให้ลิงก์ของ asset ในแอปเสีย เนื่องจากลิงก์เหล่านั้นเป็นแบบ absolute และจะกลายเป็น 404 ที่ root ของเว็บไซต์ ดังนั้นแอปที่มีการตั้งค่า base-path จึงควรใช้รูปแบบที่ส่งผ่าน path ไปด้วยจะดีกว่า
ทำไมแอปของฉันถึงบันทึก log ว่า 127.0.0.1 เป็น IP ของผู้เข้าชมทุกคน?
เพราะการเชื่อมต่อที่แอปได้รับนั้นมาจาก nginx บน loopback address จริงๆ ที่อยู่ของผู้เข้าชมจะไปถึงแอปได้ก็ต่อเมื่อคุณตั้งค่า header ไว้เท่านั้น ได้แก่ proxy_set_header X-Real-IP $remote_addr; สำหรับค่าเดียว และ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; สำหรับการต่อท้ายห่วงโซ่ จากนั้นแอปจะต้องถูกกำหนดค่าให้เชื่อถือ header เหล่านั้น โปรดจำไว้ว่าไคลเอนต์สามารถส่ง X-Forwarded-For ของตัวเองมาได้ ดังนั้นเมื่อ nginx เป็น edge server ให้เขียนทับด้วย $remote_addr แทนการต่อท้าย
ฉันจำเป็นต้องใช้ TLS ในการเชื่อมต่อระหว่าง nginx กับแอปของฉันหรือไม่?
ไม่จำเป็นหากแอปทำงานบนเซิร์ฟเวอร์เดียวกันและผูกไว้กับ 127.0.0.1 เพราะ traffic นั้นไม่ได้ออกจากเครื่อง ให้ทำ TLS termination ที่ nginx, ใช้ proxy_pass เป็น HTTP ธรรมดาบน loopback และส่ง X-Forwarded-Proto $scheme เพื่อให้แอปทราบว่าผู้เข้าชมใช้ HTTPS หาก backend อยู่บนโฮสต์อื่นผ่านเครือข่ายที่คุณไม่สามารถควบคุมได้ การเชื่อมต่อช่วงนั้นจำเป็นต้องมีการป้องกันของตัวเอง ไม่ว่าจะเป็นการใช้ HTTPS ไปยัง backend หรือการสร้างอุโมงค์ส่วนตัวระหว่างเครื่องทั้งสองเครื่อง