SSD Nodes Learn 🎉 VPS เริ่ม $5.50/เดือน
คู่มือ Matt Connorโดย Matt Connor · อัปเดตเมื่อ 2026-08-13

HTTP คืออะไร? สรุปพื้นฐานการทำงานสำหรับ Server Admin

เรียนรู้การทำงานของ HTTP สำหรับผู้ดูแลระบบเซิร์ฟเวอร์ ตั้งแต่การตีความ status code ใน access log ของ Nginx ไปจนถึงการตั้งค่า header และความแตกต่างของโปรโตคอล HTTP/3 กับ TLS

HTTP คืออะไร

HTTP (hypertext transfer protocol) คือชุดกฎเกณฑ์ที่ไคลเอนต์และเว็บเซิร์ฟเวอร์ใช้ในการร้องขอและส่งข้อมูลกลับหากัน ไคลเอนต์จะส่งคำขอซึ่งประกอบด้วย method เช่น GET, path เช่น /pricing, เวอร์ชันของโปรโตคอล, รายการของ header และบางครั้งอาจมี body ด้วย เซิร์ฟเวอร์จะตอบกลับด้วยรหัสสถานะ (status code) เช่น 200 ตามด้วย header ของเซิร์ฟเวอร์เองและมักจะมี body แนบมาด้วย การเข้าชมหน้าเว็บทุกครั้งและการเรียกใช้งาน API (application programming interface) บนเซิร์ฟเวอร์ของคุณคือการแลกเปลี่ยนข้อมูลในลักษณะนี้ที่เกิดขึ้นซ้ำๆ

HTTP ไม่มีสถานะในตัวมันเอง เซิร์ฟเวอร์จะไม่จดจำว่าคุณเพิ่งร้องขออะไรไปเมื่อครู่ ดังนั้นสิ่งใดก็ตามที่ทำหน้าที่เสมือนหน่วยความจำ เช่น session การเข้าสู่ระบบ จะถูกส่งผ่านไปใน header ของทุกคำขอ คุณสมบัติข้อนี้อธิบายถึงสิ่งที่จะกล่าวต่อไปได้เป็นอย่างดี นั่นคือการทำ caching จะขึ้นอยู่กับ header ทั้งหมด และ load balancer สามารถส่งคำขอถัดไปของคุณไปยัง backend อื่นได้โดยไม่ทำให้ระบบเกิดข้อผิดพลาด

เนื้อหาทั้งหมดด้านล่างนี้คือสิ่งที่โมเดลดังกล่าวปรากฏให้เห็นจากฝั่งเซิร์ฟเวอร์ ทั้งใน access log และใน nginx config ของคุณ

ตัวอย่าง HTTP request และ response แบบดิบพร้อมคำอธิบาย

นี่คือตัวอย่าง HTTP/1.1 request ที่สมบูรณ์ บรรทัดว่างจะเป็นตัวกำหนดจุดสิ้นสุดของส่วน header และข้อมูลหลังจากนั้นคือส่วน body โดยปกติแล้ว GET จะไม่มี body

GET /pricing HTTP/1.1
Host: example.com
User-Agent: curl/8.5.0
Accept: */*
Accept-Encoding: gzip
  • GET คือ method ซึ่งระบุสิ่งที่ต้องการให้ทำ GET ใช้สำหรับอ่านข้อมูล POST ใช้สำหรับส่งข้อมูล PUT ใช้สำหรับแทนที่ข้อมูล DELETE ใช้สำหรับลบข้อมูล HEAD ใช้สำหรับขอเฉพาะ header ของ GET โดยไม่เอา body
  • /pricing คือ path ส่วน hostname จะไม่รวมอยู่ใน request line นี่คือเหตุผลว่าทำไมจึงต้องมี header บรรทัดถัดไป
  • HTTP/1.1 คือเวอร์ชันของโปรโตคอลที่ client ใช้งาน
  • Host: example.com ระบุชื่อเว็บไซต์ที่ client ต้องการเข้าถึง HTTP/1.1 บังคับให้ต้องมีส่วนนี้ ดังนั้น nginx จะตอบกลับ request ที่ไม่มีข้อมูลนี้ด้วย 400 Bad Request
  • ส่วนที่เหลือคือค่ากำหนดต่างๆ Accept-Encoding: gzip ระบุว่า client สามารถคลายการบีบอัดข้อมูลได้ ดังนั้น server จึงได้รับอนุญาตให้ส่ง body แบบบีบอัดมาได้

ส่วน response จะมีรูปแบบเดียวกันโดยมี status line อยู่ด้านบน

HTTP/1.1 200 OK
Date: Thu, 06 Aug 2026 09:12:44 GMT
Server: nginx
Content-Type: text/html; charset=utf-8
Content-Length: 5310
Cache-Control: public, max-age=300

<!doctype html>...
  • 200 OK คือ status code พร้อมเหตุผลประกอบ ตัวเลข code คือส่วนที่สำคัญ ส่วนข้อความเหตุผลเป็นเพียงการตกแต่งและ client จะเพิกเฉยต่อส่วนนี้
  • Content-Type บอก client ว่าควรจัดการกับข้อมูลไบต์ที่ตามมาอย่างไร
  • Content-Length คือขนาดของ body ในหน่วยไบต์ เพื่อให้ client ทราบว่า body สิ้นสุดที่ตรงไหน ในกรณีที่ไม่ทราบขนาดล่วงหน้า server จะส่ง Transfer-Encoding: chunked แทนและระบุจุดสิ้นสุดด้วย chunk ที่มีความยาวเป็นศูนย์
  • Cache-Control บอกเบราว์เซอร์และ cache ระหว่างทางว่าสามารถเก็บ response นี้ไว้ได้นานเท่าใด
  • บรรทัดว่างหลัง header จะทำหน้าที่แยก header ออกจาก body ทั้งในฝั่ง request และ response

ชื่อ header ไม่คำนึงถึงตัวพิมพ์เล็กหรือใหญ่ และทุกบรรทัดจะลงท้ายด้วย carriage return ตามด้วย line feed แทนที่จะเป็น newline ปกติ คุณไม่จำเป็นต้องพิมพ์สิ่งเหล่านี้ด้วยตัวเอง แต่คุณจะพบเห็นได้ในการทำ packet capture

หากต้องการดูการรับส่งข้อมูลจริง ให้รันคำสั่งนี้กับเว็บไซต์ที่คุณดูแล:

curl -sS -o /dev/null -D - https://example.com/

-D - จะเขียน response header ลงใน terminal ของคุณ และ -o /dev/null จะทิ้งส่วน body ไป ควรเลือกใช้คำสั่งนี้แทน curl -I เพราะ -I จะส่ง HEAD request ไปยัง server ซึ่งหาก application server จัดการ HEAD ต่างจาก GET (ซึ่งหลายแห่งเป็นเช่นนั้น) คุณจะเห็น header ที่เบราว์เซอร์ทั่วไปไม่ได้รับ curl -v จะแสดงข้อมูลทั้งสองฝั่ง โดยบรรทัด request จะถูกทำเครื่องหมายด้วย > และบรรทัด response จะถูกทำเครื่องหมายด้วย <

ลักษณะของบรรทัดคำขอใน access log ของ nginx

nginx มาพร้อมกับรูปแบบ log แบบ combined ซึ่งมีนิยามดังนี้:

log_format combined '$remote_addr - $remote_user [$time_local] '
                    '"$request" $status $body_bytes_sent '
                    '"$http_referer" "$http_user_agent"';

ตัวอย่างหนึ่งบรรทัดที่สร้างขึ้นจากรูปแบบดังกล่าว:

203.0.113.45 - - [06/Aug/2026:09:12:44 +0000] "GET /pricing HTTP/1.1" 200 5310 "https://example.com/" "Mozilla/5.0 (X11; Linux x86_64) Chrome/127.0.0.0 Safari/537.36"
  • 203.0.113.45 คือ $remote_addr ซึ่งเป็นที่อยู่ของผู้ที่เปิดการเชื่อมต่อ TCP (transmission control protocol) หากอยู่หลัง proxy ค่านี้จะเป็น proxy ไม่ใช่ผู้เข้าชม
  • - ตัวแรกเป็นตัวยึดตำแหน่งคงที่ ส่วนตัวที่สองคือ $remote_user ซึ่งจะถูกเติมค่าก็ต่อเมื่อมีการใช้งาน HTTP basic authentication เท่านั้น
  • "GET /pricing HTTP/1.1" คือ $request ซึ่งเป็นบรรทัดคำขอที่คัดลอกมาตามที่ได้รับจริงทุกประการ
  • 200 คือสถานะที่เซิร์ฟเวอร์ของคุณส่งกลับ ไม่ใช่สถานะที่ผู้เข้าชมได้รับจริง
  • 5310 คือ $body_bytes_sent ซึ่งนับเฉพาะส่วนเนื้อหา (body) เท่านั้น ไม่รวมส่วนหัว (response headers) ดังนั้นตัวเลขนี้จึงน้อยกว่าจำนวนไบต์ที่ส่งออกไปจริงเสมอ
  • ฟิลด์ในเครื่องหมายคำพูดสองฟิลด์สุดท้ายคือ Referer และ User-Agent ทั้งสองฟิลด์มาจากฝั่งไคลเอนต์ จึงสามารถบรรจุข้อมูลใดก็ได้

เนื่องจาก $request ถูกคัดลอกมาแบบคำต่อคำ ข้อมูลขยะจึงปรากฏออกมาแบบคำต่อคำเช่นกัน ไคลเอนต์ที่พยายามสื่อสารด้วย TLS (transport layer security) มายังพอร์ต 80 ที่เป็น plaintext ของคุณ จะทิ้งบรรทัด 400 ไว้ โดยที่ฟิลด์คำขอจะขึ้นต้นด้วยไบต์ที่ถูก escape เช่น "\x16\x03\x01\x02\x00\x01" ซึ่ง \x16 คือประเภทของ TLS handshake record ดังนั้นไบต์เหล่านั้นจึงเป็นจุดเริ่มต้นของ ClientHello ไม่ใช่บรรทัดคำขอแต่อย่างใด เซิร์ฟเวอร์ของคุณทำงานถูกต้องแล้ว แต่มีบางอย่างกำลังส่ง HTTPS มายังพอร์ต HTTP

ให้เพิ่ม $server_protocol ลงในรูปแบบ log ของคุณด้วย มันจะแสดงค่า HTTP/1.1, HTTP/2.0 หรือ HTTP/3.0 และเป็นวิธีที่เร็วที่สุดในการพิสูจน์ว่าการเปลี่ยนแปลงโปรโตคอลมีผลใช้งานจริงแล้ว

ความหมายของสถานะโค้ดทั่วไปเมื่อเว็บไซต์ของคุณส่งกลับมา

ตัวเลขหลักแรกคือประเภทของสถานะ ซึ่งเป็นสิ่งที่คุณควรพิจารณาก่อนเป็นอันดับแรก

2xx หมายถึงการทำงานสำเร็จ 200 OK สำหรับการอ่านข้อมูลปกติ 201 Created หลังจากคำสั่ง POST ที่สร้างข้อมูลขึ้นมาใหม่ 204 No Content สำหรับความสำเร็จที่ไม่มีเนื้อหาต้องส่งกลับ ซึ่งเป็นคำตอบปกติสำหรับคำสั่ง DELETE

3xx หมายถึงให้ไปดูที่อื่น 301 เป็นการย้ายถาวรและเบราว์เซอร์จะแคชไว้อย่างแน่นหนา บางครั้งจนกว่าผู้ใช้จะล้างโปรไฟล์ของตนเอง ดังนั้นการตั้งค่า 301 ไปยังชื่อโฮสต์ที่ผิดจึงแก้ไขได้ยาก ให้ใช้ 302 ในขณะที่คุณยังอยู่ในขั้นตอนทดสอบการเปลี่ยนเส้นทาง 304 Not Modified คือความสำเร็จ ไม่ใช่ข้อผิดพลาด: ไคลเอนต์ส่ง If-None-Match ที่มี ETag (entity tag) ซึ่งคุณยังจำได้ คุณจึงตอบกลับด้วยส่วนหัวโดยไม่มีเนื้อหา หาก log เต็มไปด้วย 304 แสดงว่าการแคชกำลังทำงานได้ดี

4xx หมายถึงคำขอไม่ถูกต้อง 400 Bad Request คืออินพุตที่ผิดรูปแบบ 401 Unauthorized หมายถึงไม่ได้ยืนยันตัวตน และต้องมีส่วนหัว WWW-Authenticate ระบุรูปแบบการยืนยันตัวตนไว้ด้วย 403 Forbidden หมายถึงคำขอเป็นที่เข้าใจแต่ถูกปฏิเสธ 404 Not Found คือ path ที่ไม่มีอยู่จริง 405 Method Not Allowed คือ path ที่ถูกต้องแต่ใช้ method ผิด ซึ่งเป็นสิ่งที่ POST ส่งกลับมาเมื่อเรียกไปยังตำแหน่งไฟล์คงที่ 413 คือเนื้อหาที่มีขนาดใหญ่กว่า client_max_body_size ของ nginx ซึ่งค่าเริ่มต้นคือ 1 เมกะไบต์ และ log ข้อผิดพลาดจะยืนยันด้วย client intended to send too large body

403 บนไฟล์คงที่มักเกิดจากระบบไฟล์มากกว่ากฎของ HTTP ให้อ่าน /var/log/nginx/error.log ก่อนเปลี่ยนการตั้งค่าใดๆ open() "/srv/site/index.html" failed (13: Permission denied) หมายถึงผู้ใช้ที่เป็น worker ของ nginx ไม่สามารถอ่านไฟล์ได้ ซึ่งมักเกิดจากไดเรกทอรีแม่ขาดสิทธิ์ execute สำหรับผู้อื่น directory index of "/srv/site/" is forbidden หมายถึง path ชี้ไปยังไดเรกทอรีที่ไม่มีไฟล์ index ในขณะที่ autoindex ถูกปิดอยู่

5xx หมายถึงฝั่งของคุณมีปัญหา 500 คือข้อผิดพลาดที่ไม่ได้จัดการในแอปพลิเคชันของคุณ 502 Bad Gateway หมายถึง nginx ไม่สามารถรับการตอบกลับที่ใช้งานได้จาก upstream และ log ข้อผิดพลาดจะระบุสาเหตุ: connect() failed (111: Connection refused) while connecting to upstream หมายถึงไม่มีบริการใดฟังอยู่ที่ที่อยู่ตาม proxy_pass 504 Gateway Timeout หมายถึง upstream ยอมรับการเชื่อมต่อแต่ไม่ตอบกลับภายใน proxy_read_timeout (ค่าเริ่มต้นคือ 60 วินาที) ซึ่ง log จะบันทึกเป็น upstream timed out (110: Connection timed out) while reading response header from upstream 503 Service Unavailable คือการปฏิเสธโดยเจตนา โปรดทราบว่าตัวจำกัดอัตรา (rate limiter) ของ nginx เองจะส่งกลับ 503 เนื่องจาก limit_req_status มีค่าเริ่มต้นเป็น 503 หากคุณกำลังค้นหา 429 Too Many Requests ใน log แต่พบ 503 แทน นั่นคือสาเหตุ ให้ตั้งค่า limit_req_status 429; เพื่อให้ได้รหัสที่ถูกต้องแม่นยำ

ส่วนหัว (headers) ที่สำคัญเมื่อรันเซิร์ฟเวอร์

Host ใช้สำหรับเลือกเว็บไซต์ IP address หนึ่งหมายเลขสามารถให้บริการชื่อโฮสต์ได้หลายร้อยชื่อ โดย nginx จะนำ Host มาเทียบกับ server_name เพื่อตัดสินใจว่าบล็อก server ใดจะเป็นผู้ตอบสนอง หากไม่มีชื่อใดตรงกัน nginx จะใช้เซิร์ฟเวอร์เริ่มต้น ซึ่งเป็นบล็อกแรกที่ฟังพอร์ตและที่อยู่นั้น เว้นแต่จะมีบล็อกอื่นที่ระบุว่าเป็น default_server การได้รับเว็บไซต์ที่ไม่ถูกต้องจาก virtual host ใหม่มักเกิดจากสาเหตุนี้เสมอ คือชื่อไม่ตรงกัน คำขอจึงถูกส่งไปยังเซิร์ฟเวอร์เริ่มต้น ทดสอบได้โดยไม่ต้องแก้ไข DNS:

curl -sS -o /dev/null -D - -H 'Host: app.example.com' http://127.0.0.1/

User-Agent คือคำอธิบายตนเองที่ไคลเอนต์เขียนขึ้นและเป็นข้อความอิสระ ใช้เป็นเบาะแสเมื่ออ่าน log อย่าใช้เป็นตัวควบคุม เพราะไคลเอนต์ที่ต้องการโกหกสามารถทำได้ง่าย การบล็อก scraper ด้วย User-Agent จึงกรองได้เฉพาะตัวที่สุภาพเท่านั้น

Content-Type ตัดสินว่าไบต์เหล่านั้นจะถูกตีความอย่างไร เช่น application/json สำหรับคำขอ API หรือ text/html; charset=utf-8 สำหรับหน้าเว็บ nginx จะจับคู่นามสกุลไฟล์กับประเภทข้อมูลโดยใช้ /etc/nginx/mime.types และไฟล์ nginx.conf ที่มากับแพ็กเกจจะกำหนด default_type application/octet-stream; ไว้ ดังนั้นไฟล์ที่มีนามสกุลที่ nginx ไม่รู้จักจะถูกเสนอให้ดาวน์โหลดแทนที่จะแสดงผล อาการที่สังเกตได้คือหน้าเว็บโหลดขึ้นมาโดยไม่มีการจัดรูปแบบ (styling) ในขณะที่คอนโซลของเบราว์เซอร์จะแสดงข้อความ Refused to apply style from ... because its MIME type ('text/plain') is not a supported stylesheet MIME type คำว่า MIME ย่อมาจาก multipurpose internet mail extensions ซึ่งเป็นรูปแบบการตั้งชื่อของสตริงประเภทข้อมูลเหล่านี้

Cache-Control คือวิธีที่คุณใช้ควบคุมแคชทุกจุดระหว่างเซิร์ฟเวอร์ของคุณกับผู้อ่าน public, max-age=31536000, immutable เหมาะสำหรับสินทรัพย์ (assets) ที่ชื่อไฟล์มี content hash เพราะชื่อไฟล์จะเปลี่ยนไปเมื่อเนื้อหาเปลี่ยน no-store ควรใช้กับข้อมูลเฉพาะของผู้ใช้ เพราะหากแคชส่วนกลางเก็บหน้าเว็บที่ล็อกอินอยู่ไว้ มันจะส่งหน้านั้นให้กับบุคคลถัดไปที่เรียก URL เดียวกัน private คือการตั้งค่าระดับกลาง: เบราว์เซอร์อาจเก็บไว้ได้ แต่แคชส่วนกลางห้ามเก็บ

X-Forwarded-For มีไว้เนื่องจากพร็อกซีจะซ่อนตัวตนของผู้เข้าชม เมื่อคำขอผ่าน reverse proxy แล้ว $remote_addr จะกลายเป็นที่อยู่ของพร็อกซี ทำให้ log, ข้อมูลตำแหน่งทางภูมิศาสตร์ และการจำกัดอัตรา (rate limiting) ของคุณเห็นไคลเอนต์เพียงรายเดียว พร็อกซีจึงต้องส่งต่อที่อยู่เดิมไปด้วย:

proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;

จากนั้นเซิร์ฟเวอร์ปลายทางต้องถูกตั้งค่าให้เชื่อถือข้อมูลนี้ และต้องระบุให้ชัดเจนว่าควรเชื่อถือใคร:

set_real_ip_from 10.0.0.0/8;
real_ip_header X-Forwarded-For;

ให้ระบุเฉพาะช่วง IP ที่คุณควบคุมเท่านั้น X-Forwarded-For เป็นข้อความธรรมดาที่ไคลเอนต์ใดๆ ก็ส่งได้ ดังนั้น set_real_ip_from 0.0.0.0/0; จึงเปิดช่องให้ผู้เข้าชมเลือกที่อยู่ที่คุณจะบันทึกลง log และที่อยู่ที่ตัวจำกัดอัตราของคุณจะนับได้

X-Forwarded-Proto ป้องกันความล้มเหลวที่เฉพาะเจาะจงและพบบ่อย พร็อกซีของคุณทำ TLS termination และส่งต่อคำขอไปยังแอปพลิเคชันผ่าน HTTP ธรรมดา แอปพลิเคชันเห็นคำขอธรรมดา จึงตัดสินใจว่าผู้เข้าชมควรอยู่บน HTTPS และตอบกลับด้วย 301 https://example.com/ เบราว์เซอร์ทำตาม พร็อกซีทำ TLS termination อีกครั้งและส่งต่อ HTTP ธรรมดาอีก วนลูปเช่นนี้ไปเรื่อยๆ จนกว่าเบราว์เซอร์จะยอมแพ้ด้วยข้อความ ERR_TOO_MANY_REDIRECTS การส่ง X-Forwarded-Proto: https จะบอกแอปพลิเคชันว่าผู้เข้าชมอยู่บน HTTPS แล้ว แอปพลิเคชันจึงหยุดการเปลี่ยนเส้นทาง (redirect)

HTTP/1.1 เทียบกับ HTTP/2 และ HTTP/3: สิ่งที่เปลี่ยนไปสำหรับคุณ

HTTP/1.1 เป็นรูปแบบข้อความและจัดการคำขอได้ทีละรายการต่อหนึ่งการเชื่อมต่อ Connection: keep-alive ช่วยให้คำขอถัดไปสามารถใช้การเชื่อมต่อ TCP เดิมซ้ำได้ ซึ่งช่วยประหยัดค่าใช้จ่ายในการตั้งค่าการเชื่อมต่อ แต่การตอบกลับยังคงส่งกลับมาตามลำดับที่ร้องขอ หากมีการตอบกลับที่ล่าช้าจะขวางคำขออื่นที่รออยู่ในคิวทั้งหมด ปัญหานี้เรียกว่า head-of-line blocking ซึ่งเบราว์เซอร์แก้ไขด้วยการเปิดการเชื่อมต่อหลายรายการไปยัง hostname เดียวกันพร้อมกัน

HTTP/2 ยังคงใช้ method และ status code เดิม แต่เปลี่ยนรูปแบบการรับส่งข้อมูลเป็นไบนารี คำขอจำนวนมากสามารถแชร์การเชื่อมต่อเดียวในรูปแบบ stream ที่เป็นอิสระต่อกัน และข้อความส่วนหัวที่ซ้ำกันจะถูกบีบอัด ซึ่งมีความสำคัญเนื่องจากคำขอในปัจจุบันมีข้อมูลส่วนหัวจำนวนมาก การเชื่อมต่อยังคงเป็น TCP ดังนั้นหากแพ็กเก็ตสูญหายจะทำให้ทุก stream บนการเชื่อมต่อนั้นหยุดชะงักจนกว่าจะได้รับข้อมูลที่ส่งซ้ำ ปัญหา head-of-line blocking จึงไม่ได้หายไป แต่ย้ายจากระดับ HTTP ลงไปอยู่ที่ระดับ transport แทน ส่วน Server push ซึ่งเคยเป็นส่วนหนึ่งของ HTTP/2 นั้นแทบไม่มีการใช้งานจริงแล้ว เนื่องจาก Chrome ได้ยกเลิกการรองรับไปในปี 2022

HTTP/3 ยังคงใช้ semantics เดิม แต่เปลี่ยนจาก TCP มาเป็น QUIC ซึ่งเป็นโปรโตคอล transport ที่สร้างบน UDP (user datagram protocol) stream ของ QUIC จะเป็นอิสระต่อกันโดยสมบูรณ์ ดังนั้นหากแพ็กเก็ตสูญหายจะส่งผลกระทบเฉพาะ stream ที่แพ็กเก็ตนั้นสังกัดอยู่เท่านั้น TLS 1.3 ถูกรวมเข้าไว้ในขั้นตอน handshake ของ QUIC แทนที่จะวางซ้อนทับด้านบน ทำให้การเชื่อมต่อใหม่ใช้จำนวน round trip น้อยลง ผลลัพธ์ในทางปฏิบัติมี 2 ประการคือ ต้องเปิดพอร์ต UDP 443 ในทุก firewall บนเส้นทาง และเครือข่ายใดก็ตามที่จำกัดความเร็วหรือบล็อก UDP จะทำให้ไคลเอนต์ต้องถอยกลับไปใช้ HTTP/2

สิ่งที่เปลี่ยนไปสำหรับคุณในทางปฏิบัติคือ เบราว์เซอร์จะไม่เริ่มต้นด้วย HTTP/3 เสมอไป โดยจะเชื่อมต่อผ่าน HTTP/2 หรือ HTTP/1.1 ก่อน จากนั้นจะเห็น header Alt-Svc: h3=":443"; ma=86400 ในการตอบกลับ แล้วจึงใช้ HTTP/3 สำหรับการเชื่อมต่อในภายหลังไปยัง host นั้น ดังนั้น header นี้จึงไม่ใช่แค่การตกแต่ง แต่เป็นกลไกในการค้นหาบริการ สำหรับ nginx นั้น HTTP/2 ได้กลายเป็น directive แยกต่างหากในเวอร์ชัน 1.25.1 (ใช้ http2 on; ภายในบล็อก server แทนพารามิเตอร์ listen ... http2 แบบเดิม) และ QUIC เริ่มมีใน mainline เวอร์ชัน 1.25.0 โดยเว็บไซต์ที่ใช้ HTTP/3 จำเป็นต้องตั้งค่า listen 443 quic reuseport; ควบคู่ไปกับ listen 443 ssl; ตามปกติ

ความพร้อมใช้งานของ proxy แต่ละตัวมีความแตกต่างกัน ซึ่งควรตรวจสอบกับเวอร์ชันที่คุณใช้งานจริง ณ เดือนสิงหาคม 2026 Caddy ให้บริการ HTTP/3 เป็นค่าเริ่มต้นโดยไม่ต้องตั้งค่าใดๆ ส่วน nginx ต้องการ listener quic ที่ชัดเจนรวมถึง header Alt-Svc ตามที่กล่าวไปข้างต้น สำหรับ Traefik จะเปิดใช้งานผ่านตัวเลือก http3 ใน entry point หากคุณทำ TLS termination ที่ Traefik หน้าแอป Docker หลายตัว เวอร์ชันของโปรโตคอลที่ผู้เข้าชมได้รับจะถูกตัดสินที่จุดนั้น และการเชื่อมต่อจาก proxy ไปยัง container ของคุณมักจะเป็น HTTP/1.1 ปกติ ไม่ว่าเบราว์เซอร์จะเจรจาต่อรองมาเป็นเวอร์ชันใดก็ตาม

ควรตรวจสอบให้แน่ใจแทนการคาดเดา curl --http3 -sS -o /dev/null -D - https://example.com/ จะทำงานได้ก็ต่อเมื่อ curl -V ระบุว่ามี HTTP3 เป็นฟีเจอร์หนึ่ง ซึ่ง build ส่วนใหญ่ที่แจกจ่ายทั่วไปมักไม่รวมฟีเจอร์นี้ไว้ วิธีตรวจสอบที่เชื่อถือได้ที่สุดคือการดู log ของคุณเอง โดยเพิ่ม $server_protocol เข้าไปในรูปแบบ log แล้วอ่านค่าที่เบราว์เซอร์จริงเจรจาต่อรองมา ก่อนจะดำเนินการใดๆ ให้ยืนยันว่าพอร์ต UDP 443 เปิดใช้งานอยู่จริง เพราะ firewall ที่อนุญาตเฉพาะ TCP 443 จะทำให้ HTTP/3 ล้มเหลวโดยไม่มีข้อความแจ้งเตือน ในขณะที่เว็บไซต์ยังคงทำงานผ่าน HTTP/2 ได้ตามปกติ การทราบว่า พอร์ตใดบ้างที่เปิดและกำลังฟังอยู่บนเซิร์ฟเวอร์ Linux ของคุณ คือสิ่งแรกที่ควรตรวจสอบ

HTTPS: HTTP คือโปรโตคอล, TLS คือส่วนห่อหุ้ม

HTTPS ไม่ใช่โปรโตคอลแยกต่างหาก แต่เป็นคำขอและรหัสสถานะชุดเดียวกันที่ถูกส่งผ่านภายในเซสชัน TLS โดยพอร์ต 80 จะส่งข้อมูลแบบไม่เข้ารหัส และพอร์ต 443 จะส่งข้อมูลแบบเข้ารหัส การทำ TLS handshake จะเสร็จสิ้นก่อน จากนั้นคำขอ HTTP จึงจะถูกส่งผ่านช่องทางที่เข้ารหัสไว้ ลำดับการทำงานนี้เป็นเหตุผลว่าทำไมปัญหาเกี่ยวกับใบรับรอง (certificate) จึงไม่มีรหัสสถานะกำกับ เพราะความล้มเหลวเกิดขึ้นก่อนที่จะมีการส่งไบต์ของ HTTP แม้แต่ไบต์เดียว จึงไม่มีการตอบกลับที่เป็นตัวเลข

รายละเอียดเรื่องลำดับความสำคัญมีความสำคัญมากบนเซิร์ฟเวอร์ที่โฮสต์หลายเว็บไซต์ ใบรับรองจะถูกเลือกโดยใช้ SNI (server name indication) ซึ่งเป็นฟิลด์ใน TLS handshake ที่ระบุชื่อโฮสต์แบบไม่เข้ารหัสก่อนที่จะมี HTTP header ใดๆ เกิดขึ้น ดังนั้นเซิร์ฟเวอร์จะเลือกใบรับรองจาก SNI ก่อน แล้วจึงเลือก virtual host จาก header Host เป็นลำดับถัดไป ทั้งสองส่วนนี้เป็นการค้นหาแยกจากกันซึ่งโดยปกติแล้วจะต้องตรงกัน หากไม่ตรงกัน เบราว์เซอร์จะแสดงข้อผิดพลาดชื่อไม่ตรงกัน เช่น NET::ERR_CERT_COMMON_NAME_INVALID และจะไม่ส่งคำขอใดๆ ออกไปเลย เนื่องจากเซิร์ฟเวอร์ได้ส่งใบรับรองของค่าเริ่มต้น (default server) ซึ่งไม่ครอบคลุมชื่อโฮสต์ที่ร้องขอ

สำหรับเว็บไซต์สาธารณะ ควรใช้ใบรับรองจริงและตั้งค่าให้ต่ออายุอัตโนมัติ Certbot กับ Let's Encrypt บน nginx จะเขียน path ของใบรับรองลงใน server block และติดตั้งตัวตั้งเวลาต่ออายุให้คุณโดยอัตโนมัติ สำหรับชื่อโฮสต์ที่หน่วยงานสาธารณะไม่สามารถตรวจสอบได้ เช่น ชื่อภายในเครือข่ายหรือ IP address ภายในเครือข่ายของคุณเอง ใบรับรองที่ลงนามเอง (self-signed certificate) บน Ubuntu เป็นทางเลือกที่ตรงไปตรงมา ตราบใดที่คุณยอมรับว่าต้องตั้งค่าให้ไคลเอนต์ทุกเครื่องเชื่อถือใบรับรองนั้น

เมื่อ TLS ทำงานได้แล้ว ให้ส่งต่อการเชื่อมต่อทั้งหมดจากพอร์ต 80 ไปยังพอร์ต 443:

server {
    listen 80;
    server_name example.com;
    return 301 https://$host$request_uri;
}

ให้เพิ่ม Strict-Transport-Security ก็ต่อเมื่อคุณมั่นใจแล้วเท่านั้น header add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always; จะสั่งให้เบราว์เซอร์ปฏิเสธการเชื่อมต่อ HTTP แบบปกติสำหรับชื่อโฮสต์นั้นเป็นเวลา 2 ปี และเบราว์เซอร์จะจดจำคำสั่งนี้ไว้ในแคชของตนเอง ซึ่งหมายความว่าการลบ header นี้ออกในภายหลังจะไม่สามารถยกเลิกผลลัพธ์ดังกล่าวได้ ให้เริ่มต้นด้วย max-age เพียงไม่กี่ชั่วโมง ตรวจสอบให้แน่ใจว่าทุก subdomain ใช้งาน HTTPS ได้จริง แล้วจึงค่อยเพิ่มระยะเวลาขึ้น

FAQ

HTTP และ HTTPS แตกต่างกันอย่างไร?

HTTPS คือ HTTP ที่ถูกส่งผ่านเซสชัน TLS (transport layer security) โดยวิธีการและรหัสสถานะ (status codes) จะเหมือนกันทุกประการ สิ่งที่เปลี่ยนไปคือข้อมูลไบต์จะถูกเข้ารหัสระหว่างไคลเอนต์กับจุดที่ทำ TLS termination และพอร์ตเริ่มต้นจะเปลี่ยนจาก 80 เป็น 443 เนื่องจากกระบวนการ TLS handshake จะเสร็จสิ้นก่อนที่ไบต์แรกของ HTTP จะถูกส่ง ดังนั้นความผิดพลาดของใบรับรอง (certificate failure) จึงไม่ทำให้เกิดรหัสสถานะ นี่คือเหตุผลที่คำเตือนใบรับรองในเบราว์เซอร์แสดงชื่อข้อผิดพลาด เช่น NET::ERR_CERT_COMMON_NAME_INVALID แทนที่จะเป็นตัวเลขอย่าง 403

ทำไมเว็บไซต์ของฉันถึงแสดงข้อผิดพลาด 502 Bad Gateway?

502 จาก nginx หมายความว่า nginx ไม่สามารถรับการตอบกลับที่ใช้งานได้จาก upstream ที่ทำหน้าที่เป็นพร็อกซี ดังนั้นคำขอของผู้เข้าชมจึงถูกต้อง แต่มีบางอย่างที่อยู่หลัง nginx ทำงานผิดพลาด โปรดอ่าน /var/log/nginx/error.log ส่วน connect() failed (111: Connection refused) while connecting to upstream หมายความว่าไม่มีบริการใดกำลังฟังอยู่ที่แอดเดรสและพอร์ตใน proxy_pass ให้ตรวจสอบว่าแอปพลิเคชันกำลังทำงานอยู่และผูก (bind) อยู่ในตำแหน่งที่คุณคาดหวัง ส่วน no live upstreams while connecting to upstream หมายความว่าเซิร์ฟเวอร์ทุกตัวในบล็อก upstream ถูกทำเครื่องหมายว่าใช้งานไม่ได้หลังจากล้มเหลวซ้ำๆ ให้เปรียบเทียบกับ 504 Gateway Timeout ซึ่งหมายความว่า upstream ยอมรับการเชื่อมต่อแล้ว แต่ไม่สามารถตอบกลับได้ภายใน proxy_read_timeout

ทำไม access log ของฉันถึงแสดง IP address เดียวกันสำหรับผู้เข้าชมทุกคน?

เพราะ $remote_addr จะบันทึกแอดเดรสที่เปิดการเชื่อมต่อ TCP และเมื่ออยู่หลัง reverse proxy หรือ content delivery network แอดเดรสนั้นจะเป็นของพร็อกซีแทน แอดเดรสของผู้เข้าชมจะถูกส่งมาในเฮดเดอร์ X-Forwarded-For แทน ให้ตั้งค่า proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; ที่พร็อกซี จากนั้นที่ nginx ฝั่งรับให้ตั้งค่า set_real_ip_from เป็นช่วงแอดเดรสของพร็อกซีและใช้ real_ip_header X-Forwarded-For; ให้ระบุเฉพาะช่วงแอดเดรสที่คุณควบคุมเท่านั้น เนื่องจากเฮดเดอร์ดังกล่าวเป็นข้อความที่ไคลเอนต์ใดๆ ก็สามารถส่งมาได้ การเชื่อถือเฮดเดอร์นี้จากอินเทอร์เน็ตทั้งหมดจะทำให้ผู้เข้าชมสามารถเลือกแอดเดรสที่คุณบันทึกใน log และแอดเดรสที่คุณใช้จำกัดอัตราการเข้าถึง (rate limit) ได้

ฉันจำเป็นต้องเปิดใช้งาน HTTP/2 หรือ HTTP/3 หรือไม่?

HTTP/2 คุ้มค่าที่จะเปิดใช้งาน เพราะเป็นเพียงคำสั่งเดียวบนเว็บไซต์ที่มี TLS อยู่แล้ว และช่วยขจัดข้อจำกัดจำนวนคำขอต่อการเชื่อมต่อ ซึ่งเป็นสาเหตุที่ทำให้หน้าเว็บที่มีไฟล์ขนาดเล็กจำนวนมากโหลดช้า ส่วน HTTP/3 ให้ผลลัพธ์ที่น้อยกว่าและไม่แน่นอนกว่า อีกทั้งยังมีต้นทุนคือต้องเปิดพอร์ต UDP 443 และต้องใช้พร็อกซีที่คอมไพล์พร้อมรองรับ QUIC โปรดจำไว้ว่าเบราว์เซอร์จะเปลี่ยนไปใช้ HTTP/3 ก็ต่อเมื่อเห็นเฮดเดอร์ Alt-Svc ในการตอบกลับก่อนหน้าเท่านั้น ดังนั้นหากไม่มีเฮดเดอร์ดังกล่าว จะไม่มีอะไรเปลี่ยนแปลงไม่ว่าบรรทัด listen ของคุณจะระบุไว้อย่างไรก็ตาม ให้เพิ่ม $server_protocol ลงในรูปแบบ log ของคุณและวัดผลว่าผู้เข้าชมมีการเจรจาต่อรอง (negotiate) อย่างไรจริงก่อนที่จะเสียเวลาดำเนินการ

403 Forbidden หมายความว่าอย่างไรในเมื่อไฟล์นั้นมีอยู่จริง?

สำหรับเว็บไซต์แบบ static ข้อผิดพลาด 403 มักเกิดจากสิทธิ์ในระบบไฟล์ (filesystem permission) มากกว่ากฎของ HTTP โดย open() ... failed (13: Permission denied) ใน /var/log/nginx/error.log หมายความว่าผู้ใช้งาน nginx worker ไม่สามารถอ่านไฟล์ได้ ซึ่งส่วนใหญ่มักเกิดจากไดเรกทอรีแม่ (parent directory) ขาดสิทธิ์ execute สำหรับผู้อื่น (others) มากกว่าที่จะเป็นเพราะโหมดของไฟล์เองไม่ถูกต้อง ส่วน directory index of ... is forbidden หมายความว่าคำขอถูกส่งไปยังไดเรกทอรีที่ไม่มีไฟล์ index ในขณะที่ autoindex ถูกปิดอยู่ นอกจากนี้กฎ deny ที่ระบุไว้อย่างชัดเจนในบล็อก location ที่ตรงกันก็จะส่งคืนค่า 403 เช่นกัน ดังนั้นให้อ่านบล็อกนั้นเมื่อ log ข้อผิดพลาดไม่แสดงรายละเอียดใดๆ

#http#https#web-server#headers#http3