วิธีรัน Headless Browser บน VPS สำหรับ AI Agents
การรัน Chromium บน VPS มักพบปัญหา /dev/shm เต็ม การตั้งค่า Sandbox ผิดพลาด และฟอนต์ไม่ครบ เรียนรู้วิธีตั้งค่าทรัพยากรและจัดการ Process ให้เสถียรเพื่อรองรับการทำงานของ AI Agent
สิ่งที่คุณกำลังใช้งาน
Headless browser บน VPS คือ Chromium ที่ไม่มีหน้าต่างแสดงผล ซึ่งถูกควบคุมด้วยโค้ดของคุณแทนการใช้งานโดยบุคคล บนเซิร์ฟเวอร์มันคือกระบวนการทำงานแบบ long-lived process tree ที่เอเจนต์ของคุณสื่อสารด้วยผ่าน local socket การติดตั้งทำได้ด้วยคำสั่งเดียว แต่ภาระงานที่แท้จริงจะเกิดขึ้นหลังจากนั้น คุณต้องจำกัดทรัพยากรที่เบราว์เซอร์จะดึงไปใช้จากเครื่อง และต้องเก็บ control endpoint ของมันไว้ไม่ให้เข้าถึงได้จากอินเทอร์เน็ตสาธารณะ
คู่มือนี้ตั้งสมมติฐานว่าคุณได้เลือกเครื่องมือแล้วและอยู่ในขั้นตอนการดูแลระบบ หากคุณยังอยู่ในระหว่างการเปรียบเทียบตัวรวบรวมข้อมูล (crawler) และตัวดึงข้อมูล (extractor) ให้เริ่มต้นที่ ทางเลือกอื่นสำหรับ Firecrawl ที่โฮสต์เอง แล้วค่อยกลับมาที่นี่ เนื้อหาทั้งหมดด้านล่างใช้ Chromium ของ Playwright เนื่องจาก Playwright จัดเตรียม build ของเบราว์เซอร์และตัวติดตั้ง dependency ของตัวเองมาให้ ทำให้สามารถใช้คำสั่งเดียวกันได้ทั้งบน bare Ubuntu VPS และภายในคอนเทนเนอร์ ข้อมูลเวอร์ชันเป็นปัจจุบัน ณ เดือนสิงหาคม 2026
การติดตั้ง Chromium โดยไม่ต้องเดา dependency
npm i -D playwright@1.62.0
npx playwright install --with-deps chromium--with-deps จะรัน apt เพื่อติดตั้ง shared library และฟอนต์ที่ Chromium จำเป็นต้องใช้ โดยจะขอสิทธิ์ root เมื่อถึงขั้นตอนดังกล่าว ตัว build ของเบราว์เซอร์จะถูกดาวน์โหลดลงใน ~/.cache/ms-playwright สำหรับผู้ใช้ที่รันคำสั่งนั้น ซึ่งเป็นจุดสำคัญบนเซิร์ฟเวอร์เนื่องจาก service user มักไม่ใช่ผู้ใช้ที่คุณล็อกอินเข้าใช้งาน ให้ติดตั้ง system package ครั้งเดียวในฐานะผู้ดูแลระบบด้วย sudo npx playwright install-deps chromium จากนั้นกำหนด PLAYWRIGHT_BROWSERS_PATH=/opt/pw-browsers ทั้งในคำสั่งติดตั้งและใน service unit เพื่อให้สามารถใช้งานสำเนาเดียวกันร่วมกันได้ บริการที่ไม่สามารถมองเห็นเบราว์เซอร์ของตนจะล้มเหลวขณะเริ่มทำงานพร้อมข้อความระบุ path ที่ระบบได้พยายามค้นหา
ให้ระบุเวอร์ชันของ Playwright ให้คงที่ (pin) เนื่องจากแต่ละ release จะผูกกับ build ของเบราว์เซอร์เฉพาะรุ่น ดังนั้นการไม่ระบุเวอร์ชันใน npm update อาจทำให้เบราว์เซอร์ถูกเปลี่ยนไปในขณะที่บริการกำลังทำงานอยู่ โดย Playwright 1.62 คือเวอร์ชันปัจจุบัน ณ เดือนสิงหาคม 2026
Chromium มีอยู่ 2 build ซึ่งไม่ใช่โปรแกรมเดียวกัน การดาวน์โหลดแบบปกติจะเป็น headless shell ซึ่งเป็น binary ขนาดเล็กที่รันได้เฉพาะในโหมด headless เท่านั้น และ npx playwright install --with-deps --only-shell จะติดตั้งเฉพาะส่วนนี้ ส่วนเบราว์เซอร์ตัวเต็มคือสิ่งที่คุณจะได้รับจาก channel chromium ซึ่ง เอกสารเกี่ยวกับเบราว์เซอร์ของ Playwright ระบุว่าเป็น "เบราว์เซอร์ Chrome ตัวจริง จึงมีความสมจริง เชื่อถือได้มากกว่า และมีฟีเจอร์ครบถ้วนกว่า" ให้ใช้ shell สำหรับการดึงข้อมูลจำนวนมาก และใช้เบราว์เซอร์ตัวเต็มเมื่อเว็บไซต์แสดงผลผิดปกติและคุณต้องการหาสาเหตุ
เหตุใด headless browser ถึงทำงานล้มเหลวในคอนเทนเนอร์
Docker กำหนดขนาด /dev/shm ไว้ที่ 64 MB ให้กับทุกคอนเทนเนอร์ เอกสารของ Docker ระบุไว้อย่างชัดเจนว่า "หากคุณละเว้นการระบุขนาด ระบบจะใช้ค่าเริ่มต้นที่ 64m" Chromium จะส่งเนื้อหาที่เรนเดอร์แล้วระหว่างโพรเซสผ่านพื้นที่หน่วยความจำร่วมนี้ ดังนั้นหน้าเว็บที่มีเนื้อหาหนักเพียงหน้าเดียวก็อาจทำให้พื้นที่ดังกล่าวเต็มได้ ส่งผลให้ตัวเรนเดอร์หยุดทำงานและไคลเอนต์ของคุณรายงานว่าเป้าหมายเกิดการ crash ทั้งที่หน้าเว็บนั้นทำงานได้ปกติบนแล็ปท็อปของคุณ ให้ตรวจสอบขนาดจากภายในคอนเทนเนอร์ก่อนที่จะทำการเปลี่ยนแปลงใดๆ
df -h /dev/shmมีวิธีแก้ไขที่ใช้งานได้จริงสองวิธี ซึ่งเป็นทางเลือกที่ใช้แทนกันได้ ไม่ใช่สิ่งที่ต้องใช้คู่กัน --ipc=host จะนำคอนเทนเนอร์ไปอยู่ใน IPC namespace ของโฮสต์ ทำให้คอนเทนเนอร์ใช้ /dev/shm ของโฮสต์ซึ่งปกติจะมีขนาดเป็นครึ่งหนึ่งของ RAM คู่มือ Docker ของ Playwright แนะนำวิธีนี้เนื่องจากหากไม่ทำเช่นนั้น "Chromium อาจหน่วยความจำเต็มและ crash ได้" ข้อแลกเปลี่ยนคือคุณจะสูญเสียการแยก IPC ระหว่างคอนเทนเนอร์และโฮสต์ ส่วน --shm-size=1g จะยังคงใช้ private namespace ไว้ตามเดิมแต่จะขยายขนาด mount ให้ใหญ่ขึ้น
docker run --rm -it --init --ipc=host --user pwuser mcr.microsoft.com/playwright:v1.62.0-noble /bin/bashแฟล็ก --disable-dev-shm-usage คือคำตอบที่คุณจะพบในผลการค้นหาส่วนใหญ่ ซึ่งมันทำงานต่างออกไปโดยการย้ายไฟล์เหล่านั้นออกจาก /dev/shm ไปยังไดเรกทอรีชั่วคราว หาก /tmp อยู่บนดิสก์ คุณกำลังแลกปัญหาการ crash กับการเรนเดอร์ที่ช้าลงและการเขียนข้อมูลลงดิสก์ แต่ถ้า /tmp เป็น tmpfs ข้อมูลจะกลับไปอยู่ใน RAM โดยไม่มีการจำกัดขนาด ซึ่งเป็นวิธีหนึ่งที่เบราว์เซอร์จะกินทรัพยากรจนหมด VPS ขนาดเล็ก ดังนั้นควรตั้งค่า /dev/shm ให้เหมาะสมจะดีกว่า
ต้นทุนที่แท้จริงของการใช้ --no-sandbox
Chromium แยกการทำงานของ renderer แต่ละตัวไว้ใน sandbox ที่สร้างขึ้นบน Linux user namespaces โดย sandbox นี้ทำหน้าที่เป็นแนวป้องกันระหว่างหน้าเว็บที่เป็นอันตรายกับเซิร์ฟเวอร์ของคุณ เมื่อ sandbox เริ่มทำงานไม่ได้ Chromium จะปฏิเสธการทำงานและบันทึกข้อความใน log ดังนี้:
Failed to move to new namespace: PID namespaces supported, Network namespace supported, but failed: errno = Operation not permittedคำแนะนำที่พบบ่อยคือการใช้ --no-sandbox อย่างไรก็ตาม เอกสารด้านความปลอดภัยของ Chromium ระบุไว้อย่างชัดเจนถึงผลกระทบว่า flag นี้ "ปิดการทำงานของคุณสมบัติด้านความปลอดภัยที่สำคัญของ Chromium และไม่ควรนำไปใช้เมื่อเข้าถึงเว็บสาธารณะ" โดยนิยามแล้ว เอเจนต์ที่ติดตามลิงก์ต่างๆ ก็คือการเข้าถึงเว็บสาธารณะ ดังนั้นคุณควรหาสาเหตุที่แท้จริง
สาเหตุส่วนใหญ่มาจากสองกรณีนี้ การรันเบราว์เซอร์ด้วยสิทธิ์ root จะทำให้ sandbox ปิดการทำงานโดยอัตโนมัติ เนื่องจากไม่สามารถลดระดับสิทธิ์ (drop privileges) ที่มีอยู่ได้ นี่คือเหตุผลที่อิมเมจของ Playwright มีการสร้างผู้ใช้ทั่วไปที่ชื่อ pwuser ไว้ สำหรับ Ubuntu 24.04 ขึ้นไป AppArmor จะจำกัดการใช้งาน unprivileged user namespaces และไฟล์ binary ของ Chromium ที่อยู่ใน path ซึ่งไม่มีโปรไฟล์รองรับจะถูกปฏิเสธการเข้าถึง ซึ่งการดาวน์โหลดของ Playwright ภายใต้ ~/.cache/ms-playwright ก็เป็นหนึ่งใน path ดังกล่าว ให้ตรวจสอบทั้งสองกรณีดังนี้:
id -u
sysctl kernel.apparmor_restrict_unprivileged_userns
sudo dmesg | grep -i userns_createหากได้ค่า 1 จากคำสั่ง sysctl และมีบรรทัดที่ระบุ apparmor="DENIED" operation="userns_create" ใน kernel log แสดงว่าเกิดจากสาเหตุที่สอง ให้คุณอนุญาตไฟล์ binary นั้นใน /etc/apparmor.d/pw-chromium ซึ่งจะช่วยคงข้อจำกัดความปลอดภัยสำหรับส่วนอื่นๆ ของระบบไว้:
abi <abi/4.0>,
include <tunables/global>
profile pw-chromium /home/*/.cache/ms-playwright/*/chrome-linux/{chrome,headless_shell} flags=(unconfined) {
userns,
}โหลดการตั้งค่าด้วย sudo apparmor_parser -r /etc/apparmor.d/pw-chromium เนื่องจาก path ดังกล่าวมีการระบุเวอร์ชันของเบราว์เซอร์ไว้ จึงมีการเปลี่ยนแปลงทุกครั้งที่มีการอัปเกรด Playwright การใช้ glob ด้านบนจะช่วยให้การตั้งค่าใช้งานได้ต่อเนื่อง หากคุณเขียนโปรไฟล์โดยระบุ path ที่เจาะจงเพียงอย่างเดียว โปรไฟล์นั้นจะหยุดทำงานโดยไม่แจ้งเตือน และเบราว์เซอร์จะเริ่มล้มเหลวอีกครั้งหลังจากการอัปเดตที่ดูเหมือนไม่เกี่ยวข้อง
เหตุใดภาพหน้าจอที่บันทึกได้จึงว่างเปล่าหรือเต็มไปด้วยช่องสี่เหลี่ยม
ภาพหน้าจอที่ว่างเปล่าหรือเต็มไปด้วยช่องสี่เหลี่ยมมักเกิดจากปัญหาเรื่องฟอนต์มากกว่าข้อผิดพลาดในการเรนเดอร์ install-deps จะดึงฟอนต์พื้นฐานที่ใช้งานได้มาให้ ได้แก่ fonts-liberation, fonts-freefont-ttf, fonts-noto-color-emoji, fonts-unifont, fonts-ipafont-gothic สำหรับภาษาญี่ปุ่น, fonts-wqy-zenhei สำหรับภาษาจีน และ fonts-tlwg-loma-otf สำหรับภาษาไทย เนื่องจากในชุดดังกล่าวไม่มี Noto CJK ภาษาเกาหลีและอักขระอื่นๆ จึงต้องใช้ฟอนต์สำรองที่ fontconfig ค้นหาได้ ให้สอบถาม fontconfig แทนการคาดเดา:
fc-match "sans-serif:lang=ko"
fc-match "sans-serif:lang=ar"
fc-list | wc -lหากภาษาที่คุณต้องการใช้งานถูกแก้ไขไปเป็น unifont หรือเป็นฟอนต์สำรองที่ไม่มี glyph จริง ให้ติดตั้ง fonts-noto-core และ fonts-noto-cjk แล้วทำการตรวจสอบอีกครั้ง fontconfig จะเก็บผลลัพธ์ไว้ในแคช ดังนั้นควรเริ่มการทำงานของเบราว์เซอร์ใหม่หลังจากติดตั้งฟอนต์ หากภาพที่ได้ไม่มีฟอนต์เลย ระบบจะบันทึก Fontconfig error: Cannot load default config file ในช่วงเริ่มต้นและเรนเดอร์ทุกหน้าให้ว่างเปล่า
Locale และ time zone เป็นคนละส่วนกับฟอนต์ และสิ่งเหล่านี้ส่งผลต่อเนื้อหาที่แสดงบนหน้าเว็บ ไม่ใช่แค่ลักษณะการแสดงผล โดยปกติแล้วคอนเทนเนอร์จะไม่ได้ตั้งค่า LANG และมี TZ เป็น UTC ทำให้เว็บไซต์แสดงผลเป็นภาษาอังกฤษและพิมพ์เวลาแบบ UTC ซึ่งส่งผลให้ agent ของคุณรายงานเวลาที่ไม่ตรงกับสิ่งที่ผู้ใช้งานในประเทศนั้นๆ เห็น ควรตั้งค่าสิ่งเหล่านี้แยกตามบริบทของเบราว์เซอร์แทนการตั้งค่าทั้งเครื่อง เพื่อให้เบราว์เซอร์หนึ่งตัวสามารถรองรับงานสำหรับภูมิภาคที่แตกต่างกันได้
const context = await browser.newContext({
locale: 'en-GB',
timezoneId: 'Europe/Paris',
});เหตุใดกระบวนการของเบราว์เซอร์ที่รั่วไหลจึงทำให้เครื่องต้องทำ swap
ปัญหาที่แตกต่างกันสองประการใช้ชื่อเรียกเดียวกันว่า "zombie" โดย zombie ที่แท้จริงคือกระบวนการที่ทำงานเสร็จสิ้นแล้วแต่กระบวนการแม่ไม่เคยเรียก wait() มันจะคงรายการ PID ไว้โดยไม่มีอย่างอื่นเหลืออยู่ จึงไม่กินหน่วยความจำ คุณจะพบปัญหาเหล่านี้เมื่อรันเบราว์เซอร์เป็น PID 1 ในคอนเทนเนอร์ เนื่องจาก PID 1 ไม่มีตัวเก็บกวาด (reaper) เริ่มต้น แฟล็ก --init ของ Docker แก้ไขปัญหานี้ได้โดยตรงด้วยการรัน init ขนาดเล็กที่ทำหน้าที่ "ส่งต่อสัญญาณและเก็บกวาดกระบวนการ" ใน Compose จะใช้การตั้งค่าเดียวกันผ่าน init: true
การรั่วไหลที่ทำให้เครื่องของคุณต้องทำ swap จริงๆ นั้นต่างออกไป คือกระบวนการ Chromium ที่ยังมีชีวิตอยู่แต่ไม่มีใครปิดมัน ปัญหานี้เกิดขึ้นเมื่อ task เกิดข้อผิดพลาดระหว่าง newContext() และ close() หรือเมื่อสคริปต์ควบคุมถูกสั่ง kill และทิ้งให้โครงสร้างเบราว์เซอร์กลายเป็นกระบวนการกำพร้า (orphaned) รูปแบบที่เลวร้ายที่สุดคือโค้ดที่เปิดเบราว์เซอร์ใหม่สำหรับทุกคำขอ ลองนับจำนวนดู:
pgrep -c -f 'headless_shell|chrome'
ps -eo pid,ppid,rss,etime,comm --sort=-rss | head -20จำนวนนั้นควรกลับสู่ค่าปกติในช่วงที่ไม่ได้ใช้งานระหว่าง task หากจำนวนเพิ่มขึ้นเรื่อยๆ ตลอดทั้งวัน วิธีแก้ไขอยู่ที่โค้ดของคุณไม่ใช่ที่แฟล็กการเปิดใช้งาน: ให้ปิด context ในบล็อก finally, ปิดเบราว์เซอร์เมื่อเกิด SIGTERM และรีไซเคิลเบราว์เซอร์หลังจากทำงานครบจำนวน task ที่กำหนด แทนที่จะรันค้างไว้เป็นเดือน ภายใต้ systemd การสั่ง stop หรือ restart จะ kill ทุกอย่างใน cgroup ของ unit นั้น ดังนั้น sudo systemctl restart browser.service จึงเป็นการรีเซ็ตที่เชื่อถือได้ เบราว์เซอร์ที่เริ่มต้นด้วยมือภายใน terminal multiplexer จะไม่มีการรับประกันเช่นนั้น และกระบวนการกำพร้าของมันจะยังคงอยู่แม้เซสชันจะสิ้นสุดลงแล้วก็ตาม
หน่วยความจำ RAM ที่ browser context หนึ่งต้องการมีปริมาณเท่าใด
ต้องตั้งคำถามให้ชัดเจน เพราะ "หนึ่ง browser" ไม่ได้หมายถึงหนึ่ง process เสมอไป Chromium จะรัน browser process, GPU process, utility process และ renderer process แยกตามเว็บไซต์ โดยที่ site isolation จะทำให้ iframe ข้ามเว็บไซต์มี renderer เป็นของตัวเองด้วย BrowserContext คือพื้นที่จัดเก็บ cookie และข้อมูลแยกต่างหากภายในโครงสร้างเดียวกัน ดังนั้นการสร้าง context ที่สองจึงใช้ทรัพยากรน้อย แต่การเปิดหน้าเว็บที่สองนั้นใช้ทรัพยากรมาก เพราะต้องเริ่ม renderer process ใหม่ และหน้าเว็บที่มีโฆษณาจำนวนมากจะเริ่ม process เพิ่มขึ้นอีกหลายรายการ
ดังนั้น ตัวเลขที่ควรวัดคือหน่วยความจำสูงสุด (peak memory) ของทั้งโครงสร้างภายใต้ภาระงานของคุณ ตัวเลขจากบล็อกของผู้อื่นไม่มีประโยชน์ในกรณีนี้ เพราะหน้าเว็บที่ agent ของคุณเปิดจะเป็นตัวกำหนดคำตอบ ให้วัดผลบนเครื่องที่คุณจะใช้งานจริง โดยทดสอบกับเว็บไซต์ที่คุณจะเข้าชม:
sudo systemd-run --unit=browser-probe -p MemoryMax=2G -p MemorySwapMax=0 -p WorkingDirectory=/srv/agent /usr/bin/node worker.js
systemctl status browser-probeบน Ubuntu 24.04 บรรทัด Memory: ในผลลัพธ์ดังกล่าวจะรายงานทั้งการใช้งานปัจจุบันและสูงสุดของ unit นั้น ให้รัน worker โดยเปิดทีละหนึ่งหน้า จดบันทึกค่าสูงสุดไว้ จากนั้นทำซ้ำโดยเปิดสองหน้าเพื่อดูว่าหน้าเว็บที่สองมีต้นทุนจริงเท่าใด จากนั้นการคำนวณ concurrency ให้ใช้วิธีทางคณิตศาสตร์: นำ RAM ทั้งหมดที่มี ลบด้วยจำนวนที่ระบบส่วนอื่นต้องใช้ เผื่อพื้นที่ไว้สักสองสามร้อย MB แล้วหารด้วยค่า peak ต่อ worker ที่วัดได้ สำหรับการเลือกขนาดเครื่องที่รองรับงานนี้ โปรดดู RAM และ CPU ที่ agent VPS ต้องการ
ให้บังคับใช้ตัวเลขดังกล่าวในสองจุด ในโค้ดของคุณ ให้ใช้ worker pool แบบคงที่หรือ semaphore เพื่อให้คำขอที่เข้ามาพร้อมกันจำนวนมากเข้าคิวแทนที่จะเปิด browser ใหม่ ในระดับ OS ให้ใช้ cgroup limit เพื่อป้องกันไม่ให้บั๊กในคิวทำให้เครื่องล่ม:
[Service]
MemoryMax=2G
MemorySwapMax=0
TasksMax=512
Restart=alwaysMemorySwapMax=0 มีความสำคัญมากกว่าที่เห็น หากไม่มีการตั้งค่านี้ cgroup จะผลักดันหน้าหน่วยความจำไปที่ swap เมื่อถึงขีดจำกัด ทำให้เครื่องยังทำงานอยู่แต่ทุกคำขอจะช้าลง ซึ่งวินิจฉัยได้ยากกว่าความล้มเหลวที่ชัดเจน หากมีการตั้งค่านี้ kernel จะสั่งยุติ browser tree ภายใน cgroup นั้น และ systemd จะรีสตาร์ท unit ทำให้ sshd ยังคงทำงานต่อไปได้ การควบคุมแบบเดียวกันใน Compose คือ mem_limit, shm_size และ init ซึ่งครอบคลุมอยู่ใน การตั้งค่าขีดจำกัดหน่วยความจำใน Docker Compose
เก็บ endpoint ของเบราว์เซอร์ไว้ไม่ให้เข้าถึงได้จากอินเทอร์เน็ตสาธารณะ
Playwright สามารถรันเบราว์เซอร์ในรูปแบบเซิร์ฟเวอร์และส่ง WebSocket URL ให้กับเอเจนต์ของคุณได้:
const { chromium } = require('playwright');
const server = await chromium.launchServer({ port: 3000 });
console.log(server.wsEndpoint());endpoint ดังกล่าวไม่มีระบบล็อกอิน เอกสาร API ของ Playwright ระบุไว้อย่างชัดเจนว่า: "กระบวนการหรือหน้าเว็บใดๆ (รวมถึงสิ่งที่รันอยู่ใน Playwright) ที่ทราบ wsPath สามารถเข้าควบคุมผู้ใช้ OS ได้" ค่าเริ่มต้นของ host คือ localhost ซึ่ง "ยอมรับการเชื่อมต่อจาก loopback interface เท่านั้น" และเอกสารยังเตือนว่าการระบุ address อย่างชัดเจน เช่น 0.0.0.0 จะเป็นการ "เปิดเผย RPC ของเบราว์เซอร์ให้กับทุกสิ่งที่สามารถเข้าถึงพอร์ตที่กำลังฟังอยู่ได้" ส่วน --remote-debugging-port ของ Chrome นั้นแย่กว่า โปรโตคอล DevTools ไม่มีการตรวจสอบสิทธิ์ใดๆ ทั้งสิ้น และขึ้นอยู่กับการผูกไว้กับ loopback เพียงอย่างเดียว
ตรวจสอบสิ่งที่คุณได้เปิดเผยไว้จริง และตรวจสอบจากเครื่องอื่นรวมถึงจาก VPS ของคุณด้วย:
ss -ltnpหากพอร์ตของเบราว์เซอร์ถูกผูกไว้กับ 0.0.0.0 ถือเป็นช่องโหว่ โปรดจำไว้ว่าผู้ให้บริการส่วนใหญ่มี network firewall แยกต่างหากในแผงควบคุม ซึ่งกฎ ufw ของคุณไม่ทราบเรื่องนี้ ให้เข้าถึง endpoint จากเครื่องอื่นผ่าน SSH tunnel หรือ private VPN แทน:
ssh -N -L 3000:127.0.0.1:3000 you@your-vpsความเสี่ยงในที่นี้มีมากกว่าแค่การถูกขโมยเวลาใช้งานเบราว์เซอร์ เบราว์เซอร์ที่คุณสามารถควบคุมได้เปรียบเสมือนเครื่องมือทำ request forgery ที่วางอยู่ภายในเครือข่ายของคุณ ใครก็ตามที่เข้าถึง socket นั้นได้สามารถสั่งให้มันดึงข้อมูล http://127.0.0.1:8080, หน้าผู้ดูแลระบบฐานข้อมูลของคุณ หรือ cloud metadata address ที่ 169.254.169.254 แล้วอ่านการตอบกลับจากหน้าเว็บนั้นได้ firewall ของคุณจะเห็นว่าคำขอนั้นมาจากตัว VPS เอง ซึ่งเป็นสิ่งที่ได้รับอนุญาต ให้ถือว่า control endpoint มีความสำคัญเท่ากับการเข้าถึง shell บนเครื่องนั้น
เซิร์ฟเวอร์ MCP ก็มีลักษณะเดียวกัน npx @playwright/mcp@latest --headless --port 8931 ให้บริการผ่าน HTTP บน localhost และ --host 0.0.0.0 คือ flag ที่เปลี่ยนเครื่องมือภายในให้กลายเป็นเครื่องมือสาธารณะ README ของโปรเจกต์ระบุไว้อย่างชัดเจนว่า Playwright MCP "ไม่ใช่ขอบเขตความปลอดภัย" ให้คงพอร์ตไว้ที่ loopback และปล่อยให้เอเจนต์เข้าถึงผ่าน tunnel เดียวกันแทน
หน้าเว็บที่เอเจนต์ของคุณอ่านถือเป็นอินพุตที่ไม่น่าเชื่อถือ
เอเจนต์ที่ท่องเว็บแบบเปิดจะนำข้อความที่เขียนโดยบุคคลภายนอกป้อนเข้าสู่โมเดล ซึ่งเป็นโมเดลเดียวกับที่เก็บคำสั่งของคุณไว้ หน้าเว็บอาจมีข้อความที่ระบุถึงโมเดลนั้น โดยสั่งให้ละทิ้งงานที่ทำอยู่ เรียกใช้เครื่องมือ หรือโพสต์ข้อมูลไปยัง URL ใดๆ โมเดลจะได้รับทั้งสองส่วนเป็นข้อความเหมือนกัน จึงไม่มีวิธีที่เชื่อถือได้ในการแยกแยะระหว่างคำพูดของหน้าเว็บกับคำสั่งของคุณ คุณควรออกแบบระบบให้หน้าเว็บที่เป็นอันตรายมีช่องทางในการโจมตีน้อยที่สุด
- รันเบราว์เซอร์ภายใต้ OS user ของตนเอง โดยไม่มี SSH keys และไม่มี cloud credentials อยู่ในสภาพแวดล้อมนั้น
- ใช้บริบทใหม่สำหรับแต่ละงาน และใช้
--isolatedร่วมกับ Playwright MCP เพื่อไม่ให้เซสชันบนเว็บไซต์หนึ่งสามารถเข้าถึงได้โดยหน้าเว็บถัดไป - รักษารายการ origin allowlist ไว้ในกรณีที่งานอนุญาตให้ทำได้ Playwright MCP รองรับ
--allowed-originsและ--blocked-originsในรูปแบบรายการที่คั่นด้วยเครื่องหมายอัฒภาค (semicolon) - กำหนดให้มีขั้นตอนที่ต้องอาศัยมนุษย์ก่อนการดำเนินการใดๆ ที่มีการเปลี่ยนแปลงสถานะ เช่น การส่งอีเมลหรือการใช้จ่ายเงิน
วิธีที่ดีกว่าคือการเก็บเบราว์เซอร์ทั้งหมดไว้บนเครื่องที่คุณสามารถทิ้งและสร้างใหม่ได้ ซึ่งเป็นเหตุผลเดียวกับ การรันเอเจนต์เขียนโค้ดใน VM แบบใช้แล้วทิ้ง หากงานที่แท้จริงของเอเจนต์คือการค้นหาแทนที่จะเป็นการท่องเว็บแบบไม่จำกัด การใช้เครื่องมือที่เฉพาะเจาะจงจะปลอดภัยกว่าการใช้เบราว์เซอร์เต็มรูปแบบ: ทักษะการค้นหาที่สนับสนุนโดย SearXNG ของคุณเอง จะส่งคืนผลลัพธ์โดยไม่ต้องโหลดหน้าเว็บที่เป็นอันตรายนั้นเลย
FAQ
ทำไม Chromium ถึงค้างใน Docker แต่ทำงานได้ปกติบน VPS เครื่องเดียวกัน?
เพราะคอนเทนเนอร์ได้รับ /dev/shm ขนาด 64 MB เป็นค่าเริ่มต้น ในขณะที่โฮสต์มีขนาดใหญ่กว่ามาก Chromium จะส่งเนื้อหาที่เรนเดอร์ผ่านพื้นที่หน่วยความจำร่วมดังกล่าว ดังนั้นหน้าเว็บที่หนักจะทำให้หน่วยความจำเต็มและตัวเรนเดอร์จะหยุดทำงาน ให้รัน df -h /dev/shm ภายในคอนเทนเนอร์เพื่อตรวจสอบ จากนั้นให้เริ่มการทำงานด้วย --ipc=host ซึ่งจะใช้หน่วยความจำร่วมของโฮสต์ หรือใช้ --shm-size=1g เพื่อขยายขนาดหน่วยความจำของคอนเทนเนอร์เอง ส่วน --disable-dev-shm-usage เป็นเพียงการย้ายปัญหาไปไว้ที่ /tmp เท่านั้น
การใช้ --no-sandbox ปลอดภัยหรือไม่หาก VPS ไม่ได้รันอย่างอื่นเลย?
ไม่ปลอดภัย Sandbox คือสิ่งที่ป้องกันไม่ให้หน้าเว็บที่เป็นอันตรายเข้าถึงส่วนอื่นของเครื่องได้ และเอกสารของ Chromium ระบุว่า flag นี้ "ปิดใช้งานฟีเจอร์ความปลอดภัยที่สำคัญของ Chromium และไม่ควรใช้เมื่อท่องเว็บสาธารณะ" ตัวแทน (agent) ที่ติดตามลิงก์ถือว่ากำลังท่องเว็บสาธารณะอยู่ ให้แก้ไขที่ต้นเหตุแทน: อย่ารันเบราว์เซอร์ด้วยสิทธิ์ root และบน Ubuntu 24.04 ให้เพิ่ม AppArmor profile ที่มี userns, สำหรับ path ของ binary เบราว์เซอร์ เพื่ออนุญาตให้ใช้ unprivileged user namespaces สำหรับโปรแกรมนั้นเพียงโปรแกรมเดียว
ฉันสามารถรันเบราว์เซอร์กี่ตัวบน VPS ขนาดเล็ก?
ให้วัดค่าจริง อย่าใช้วิธีเดาตัวเลข Chromium จะเริ่มกระบวนการเรนเดอร์หนึ่งตัวต่อหนึ่งเว็บไซต์ ดังนั้นคำตอบจึงขึ้นอยู่กับหน้าเว็บที่คุณเปิด ให้รัน worker หนึ่งตัวภายใต้ systemd-run โดยตั้งค่า MemoryMax ไว้ อ่านค่าสูงสุดจากบรรทัด Memory: ใน systemctl status จากนั้นนำ RAM ที่ว่างอยู่มาหารด้วยค่าสูงสุดนั้นและเผื่อพื้นที่ไว้ด้วย ให้บังคับใช้ผลลัพธ์สองชั้น โดยใช้คิวในโค้ดของคุณและใช้ MemoryMax ใน unit file เพื่อให้คำขอที่เข้ามาพร้อมกันจำนวนมากรอคิวแทนที่จะทำให้เครื่องต้องทำ swapping
ตัวแทนของฉันสามารถเชื่อมต่อกับเบราว์เซอร์จากเครื่องอื่นได้หรือไม่?
ได้ แต่ห้าม bind พอร์ตไปยัง 0.0.0.0 โดยเด็ดขาด ทั้ง endpoint ของ Playwright server และพอร์ต Chrome DevTools จะยอมรับไคลเอนต์ใดก็ตามที่เข้าถึงได้โดยไม่มีการตรวจสอบรหัสผ่าน ให้คงการฟังพอร์ตไว้ที่ 127.0.0.1 และส่งการเชื่อมต่อผ่าน SSH tunnel หรือ VPN ส่วนตัว ตรวจสอบด้วย ss -ltnp บนเซิร์ฟเวอร์และตรวจสอบพอร์ตจากภายนอก รวมถึงตรวจสอบ network firewall แยกต่างหากของผู้ให้บริการของคุณด้วย
ทำไมภาพหน้าจอของฉันถึงว่างเปล่าทั้งที่หน้าเว็บโหลดเสร็จแล้ว?
เกิดจากฟอนต์ไม่เพียงพอ หากไม่มีฟอนต์ที่รองรับสคริปต์ของหน้าเว็บ ข้อความจะแสดงเป็นกล่องว่างหรือหายไปเลย ทำให้หน้าเว็บที่มีรูปภาพน้อยดูเหมือนว่างเปล่า ให้รัน fc-match "sans-serif:lang=ko" สำหรับแต่ละภาษาที่คุณดึงข้อมูล ติดตั้ง fonts-noto-core และ fonts-noto-cjk เมื่อคำตอบเป็นแบบ fallback ทั่วไป และรีสตาร์ทเบราว์เซอร์เพื่อให้ fontconfig โหลดแคชใหม่ คอนเทนเนอร์ที่ไม่มีฟอนต์เลยจะบันทึก Fontconfig error: Cannot load default config file ใน log ตอนเริ่มต้นระบบ