Cara Jalankan Headless Browser pada VPS untuk AI Agent
Jalankan headless browser pada VPS dengan stabil. Elakkan ralat /dev/shm, isu sandbox, dan proses tergantung dengan konfigurasi sumber yang betul untuk ejen AI anda.
Perkara yang anda jalankan
Pelayar tanpa kepala (headless browser) pada VPS ialah Chromium tanpa tetingkap, yang dipacu oleh kod anda dan bukannya manusia. Pada pelayan, ia merupakan pepohon proses jangka hayat panjang yang berhubung dengan ejen anda melalui soket tempatan. Pemasangannya hanya memerlukan satu arahan. Kerja sebenar bermula selepas itu. Anda perlu mengehadkan sumber yang boleh diambil oleh pelayar daripada mesin, dan pastikan titik akhir kawalannya tidak terdedah kepada internet awam.
Panduan ini mengandaikan pilihan alat telah dibuat dan anda kini perlu mengendalikannya. Jika anda masih membandingkan perangkak (crawler) dan pengekstrak, mulakan dengan alternatif Firecrawl yang dihoskan sendiri dan kembali semula ke sini. Semua kandungan di bawah menggunakan Chromium daripada Playwright, kerana Playwright membekalkan binaan pelayar dan pemasang dependensinya sendiri, jadi arahan yang sama berfungsi pada VPS Ubuntu kosong mahupun di dalam kontena. Versi adalah terkini setakat Ogos 2026.
Memasang Chromium tanpa meneka dependensi
npm i -D playwright@1.62.0
npx playwright install --with-deps chromium--with-deps menjalankan apt untuk pustaka kongsi dan fon yang diperlukan oleh Chromium, serta meminta kebenaran root apabila tiba masanya. Binaan pelayar itu sendiri dimuat turun ke ~/.cache/ms-playwright bagi pengguna yang menjalankan perintah tersebut. Perkara ini penting pada pelayan, kerana pengguna servis biasanya bukan pengguna yang anda gunakan untuk log masuk. Pasang pakej sistem sekali sahaja sebagai pentadbir dengan sudo npx playwright install-deps chromium, kemudian tetapkan PLAYWRIGHT_BROWSERS_PATH=/opt/pw-browsers dalam kedua-dua perintah pemasangan dan unit servis supaya satu salinan dikongsi. Servis yang tidak dapat mengesan pelayarnya akan gagal semasa pelancaran dengan mesej yang menyatakan laluan yang dicari.
Tetapkan versi Playwright. Setiap keluaran terikat pada satu binaan pelayar, jadi npm update yang tidak ditetapkan versinya boleh menukar pelayar di bawah servis yang sedang berjalan. Playwright 1.62 adalah versi semasa setakat Ogos 2026.
Terdapat dua binaan Chromium dan ia bukanlah program yang sama. Muat turun lalai adalah shell tanpa kepala (headless shell), iaitu binari lebih kecil yang hanya berjalan tanpa kepala, dan npx playwright install --with-deps --only-shell memasang itu sahaja. Pelayar penuh adalah apa yang anda peroleh dengan saluran chromium, yang dokumentasi pelayar Playwright panggil sebagai "pelayar Chrome sebenar, dan oleh itu lebih autentik, boleh dipercayai, serta menawarkan lebih banyak ciri". Gunakan shell untuk pengambilan pukal. Gunakan pelayar penuh apabila sesuatu tapak berkelakuan berbeza dan anda perlu mengetahui puncanya.
Mengapa pelayar tanpa kepala (headless browser) terhempas dalam kontena
Docker memberikan setiap kontena /dev/shm sebanyak 64 MB. Dokumentasi Docker menyatakan dengan jelas: "Jika anda mengabaikan saiz sepenuhnya, sistem menggunakan 64m". Chromium memindahkan kandungan yang dirender antara prosesnya melalui kawasan memori kongsi tersebut, jadi satu halaman yang berat boleh memenuhinya. Renderer kemudian mati dan klien anda melaporkan sasaran terhempas, pada halaman yang berfungsi dengan baik pada komputer riba anda. Sahkan saiz tersebut dari dalam kontena sebelum anda mengubah apa-apa.
df -h /dev/shmTerdapat dua penyelesaian sebenar dan ia adalah alternatif, bukan pasangan. --ipc=host meletakkan kontena dalam ruang nama IPC hos supaya ia menggunakan /dev/shm hos, yang biasanya separuh daripada RAM. Panduan Docker Playwright mengesyorkannya, kerana tanpanya "Chromium boleh kehabisan memori dan terhempas". Kosnya ialah anda menggugurkan pengasingan IPC antara kontena dan hos. --shm-size=1g mengekalkan ruang nama peribadi dan hanya menjadikan mount tersebut lebih besar.
docker run --rm -it --init --ipc=host --user pwuser mcr.microsoft.com/playwright:v1.62.0-noble /bin/bashFlag --disable-dev-shm-usage ialah jawapan yang akan anda temui dalam kebanyakan hasil carian, dan ia melakukan sesuatu yang berbeza: ia mengalihkan fail tersebut keluar dari /dev/shm ke dalam direktori sementara. Jika /tmp terletak pada cakera, anda menukar ralat terhempas dengan rendering yang lebih perlahan dan penulisan cakera. Jika /tmp ialah tmpfs, data tersebut kembali ke dalam RAM tanpa had saiz langsung, yang merupakan salah satu cara pelayar menghabiskan sumber VPS yang kecil. Tetapkan saiz /dev/shm dengan betul sebaliknya.
Kos sebenar penggunaan --no-sandbox
Chromium mengasingkan setiap renderer dalam sandbox yang dibina berasaskan user namespaces Linux. Sandbox tersebut merupakan sempadan antara halaman berniat jahat dengan pelayan anda. Apabila ia gagal bermula, Chromium enggan berjalan, dan log akan memaparkan baris seperti ini:
Failed to move to new namespace: PID namespaces supported, Network namespace supported, but failed: errno = Operation not permittedNasihat biasa yang diberikan ialah --no-sandbox. Dokumentasi keselamatan Chromium sendiri menyatakan dengan jelas tentang harganya: flag tersebut "melumpuhkan ciri keselamatan kritikal Chromium dan tidak boleh digunakan sama sekali semasa melayari web terbuka". Ejen yang mengikuti pautan secara definisinya sedang melayari web terbuka. Cari punca sebenar.
Dua punca merangkumi hampir setiap kes. Menjalankan pelayar sebagai root melumpuhkan sandbox, kerana ia tidak boleh menggugurkan keistimewaan yang sudah dimilikinya; itulah sebabnya imej Playwright menyertakan pengguna biasa bernama pwuser. Pada Ubuntu 24.04 dan versi seterusnya, AppArmor menyekat unprivileged user namespaces, dan binari Chromium pada laluan yang tidak dilindungi oleh mana-mana profil akan dinafikan akses. Muat turun Playwright di bawah ~/.cache/ms-playwright adalah laluan yang sedemikian. Periksa kedua-duanya:
id -u
sysctl kernel.apparmor_restrict_unprivileged_userns
sudo dmesg | grep -i userns_create1 daripada sysctl berserta baris kernel yang mengandungi apparmor="DENIED" operation="userns_create" mengesahkan punca kedua. Benarkan binari tersebut dalam /etc/apparmor.d/pw-chromium, yang mengekalkan sekatan untuk semua perkara lain pada mesin tersebut:
abi <abi/4.0>,
include <tunables/global>
profile pw-chromium /home/*/.cache/ms-playwright/*/chrome-linux/{chrome,headless_shell} flags=(unconfined) {
userns,
}Muatkan ia dengan sudo apparmor_parser -r /etc/apparmor.d/pw-chromium. Laluan tersebut mengandungi semakan pelayar, jadi ia berubah pada setiap naik taraf Playwright. Glob di atas mampu bertahan melaluinya. Profil yang ditulis berdasarkan satu laluan tepat akan berhenti sepadan secara senyap, dan pelayar akan mula gagal semula selepas kemas kini yang kelihatan tidak berkaitan.
Mengapa tangkapan skrin kelihatan kosong atau dipenuhi kotak
Tangkapan skrin yang kosong, atau yang dipenuhi dengan petak kosong, biasanya berpunca daripada masalah fon dan bukannya pepijat pemaparan. install-deps menarik asas yang berfungsi: fonts-liberation, fonts-freefont-ttf, fonts-noto-color-emoji, fonts-unifont, fonts-ipafont-gothic untuk bahasa Jepun, fonts-wqy-zenhei untuk bahasa Cina, fonts-tlwg-loma-otf untuk bahasa Thai. Tiada Noto CJK dalam set tersebut, jadi bahasa Korea dan beberapa skrip lain akan menggunakan apa sahaja yang ditemui oleh fontconfig. Tanya fontconfig daripada meneka:
fc-match "sans-serif:lang=ko"
fc-match "sans-serif:lang=ar"
fc-list | wc -lJika bahasa yang anda perlukan diselesaikan kepada unifont atau kepada sandaran tanpa glif yang sebenar, pasang fonts-noto-core dan fonts-noto-cjk, kemudian jalankan semakan semula. Fontconfig menyimpan hasil carian dalam cache, jadi mulakan semula pelayar selepas memasang fon. Imej yang dilucutkan tanpa sebarang fon akan mencatat Fontconfig error: Cannot load default config file semasa permulaan dan memaparkan setiap halaman sebagai kosong.
Lokal dan zon waktu adalah berasingan daripada fon, dan ia mengubah kandungan halaman, bukan sekadar rupanya. Bekas (container) biasanya mempunyai LANG yang tidak ditetapkan dan TZ pada UTC, jadi tapak web akan menghidangkan kandungan dalam bahasa Inggeris dan mencetak cap waktu UTC, manakala ejen anda melaporkan waktu yang tidak sepadan dengan apa yang dilihat oleh seseorang di negara tersebut. Tetapkan perkara ini mengikut konteks pelayar dan bukannya mengikut mesin, supaya satu pelayar boleh melaksanakan tugas untuk wilayah yang berbeza.
const context = await browser.newContext({
locale: 'en-GB',
timezoneId: 'Europe/Paris',
});Mengapa proses pelayar yang bocor menyebabkan pelayan menggunakan swap
Dua masalah berbeza berkongsi nama "zombie". Zombie sebenar ialah proses yang telah selesai tetapi induknya tidak pernah memanggil wait(). Ia hanya memegang entri PID dan tidak menggunakan memori. Anda akan mengumpul proses ini apabila pelayar berjalan sebagai PID 1 di dalam kontena, kerana PID 1 tidak mempunyai fungsi reaper lalai. Flag --init pada Docker menyelesaikan masalah ini dengan menjalankan init kecil yang "menghantar isyarat dan menuai proses". Dalam Compose, perkara yang sama dilakukan melalui init: true.
Kebocoran yang sebenarnya menyebabkan pelayan anda menggunakan swap adalah berbeza: proses Chromium yang masih hidup tetapi tidak ditutup oleh sesiapa. Ia berlaku apabila tugasan terhenti antara newContext() dan close(), atau apabila skrip kawalan dimatikan dan meninggalkan pepohon pelayar dalam keadaan terbiar (orphaned). Versi paling buruk ialah kod yang melancarkan pelayar baharu bagi setiap permintaan. Kira jumlahnya:
pgrep -c -f 'headless_shell|chrome'
ps -eo pid,ppid,rss,etime,comm --sort=-rss | head -20Kiraan tersebut sepatutnya kembali kepada nilai melahu (idle) antara tugasan. Jika jumlahnya meningkat sepanjang hari, penyelesaiannya terletak pada kod anda dan bukannya pada flag pelancaran: tutup konteks dalam blok finally, tutup pelayar pada SIGTERM, dan kitar semula pelayar selepas bilangan tugasan yang tetap dan bukannya membiarkannya berjalan selama sebulan. Di bawah systemd, arahan stop atau restart akan mematikan segala-galanya dalam cgroup unit tersebut, jadi sudo systemctl restart browser.service merupakan tetapan semula yang boleh dipercayai. Pelayar yang dimulakan secara manual di dalam terminal multiplexer tidak mempunyai jaminan sedemikian, dan proses anak yatimnya akan terus hidup melebihi sesi tersebut.
Berapakah RAM yang diperlukan oleh satu konteks pelayar
Ajukan soalan ini dengan tepat, kerana "satu pelayar" bukan bermaksud satu proses. Chromium menjalankan proses pelayar, proses GPU, proses utiliti, dan satu proses renderer bagi setiap tapak web; pengasingan tapak (site isolation) juga memberikan iframe rentas-tapak (cross-site) renderer tersendiri. BrowserContext ialah bekas kuki dan ruang storan berasingan di dalam pepohon yang sama, jadi konteks kedua adalah murah. Halaman kedua pula tidak, kerana ia memulakan proses renderer, dan halaman yang sarat dengan iklan akan memulakan beberapa proses sekaligus.
Oleh itu, angka yang perlu diukur ialah memori puncak bagi keseluruhan pepohon di bawah beban kerja anda sendiri. Angka daripada blog orang lain tidak berguna di sini, kerana halaman yang dibuka oleh ejen anda menentukan jawapannya. Ukur pada mesin yang akan anda gunakan, berdasarkan tapak web yang akan anda lawati:
sudo systemd-run --unit=browser-probe -p MemoryMax=2G -p MemorySwapMax=0 -p WorkingDirectory=/srv/agent /usr/bin/node worker.js
systemctl status browser-probePada Ubuntu 24.04, baris Memory: dalam output tersebut melaporkan penggunaan semasa dan puncak bagi unit berkenaan. Jalankan pekerja (worker) dengan satu halaman pada satu masa, catatkan puncaknya, kemudian ulangi dengan dua halaman dibuka untuk melihat kos sebenar halaman kedua. Konkurensi kemudiannya adalah pengiraan aritmetik: ambil jumlah RAM, tolak keperluan sistem yang lain, simpan beberapa ratus MB sebagai ruang penampan, dan bahagikan dengan puncak yang diukur bagi setiap pekerja. Untuk menentukan saiz mesin bagi tujuan ini, lihat berapa banyak RAM dan CPU yang diperlukan oleh VPS ejen.
Kuatkuasakan angka tersebut di dua tempat. Dalam kod anda, gunakan pool pekerja tetap atau semaphore, supaya lonjakan permintaan ejen akan beratur dan bukannya melancarkan pelayar baharu. Dalam OS, gunakan had cgroup, supaya pepijat dalam baris gilir tidak menyebabkan mesin terhenti:
[Service]
MemoryMax=2G
MemorySwapMax=0
TasksMax=512
Restart=alwaysMemorySwapMax=0 lebih penting daripada yang disangka. Tanpanya, cgroup akan menolak halaman ke dalam swap apabila had dicapai; mesin kekal hidup tetapi setiap permintaan menjadi perlahan, yang lebih sukar didiagnosis berbanding kegagalan yang jelas. Dengan tetapan ini, kernel akan mematikan pepohon pelayar di dalam cgroup tersebut, systemd akan memulakan semula unit, dan sshd akan terus berfungsi. Kawalan yang sama dalam Compose ialah mem_limit, shm_size dan init, yang dibincangkan dalam menetapkan had memori dalam Docker Compose.
Pastikan endpoint pelayar tidak terdedah kepada internet awam
Playwright boleh menjalankan pelayar sebagai pelayan dan memberikan ejen anda URL WebSocket:
const { chromium } = require('playwright');
const server = await chromium.launchServer({ port: 3000 });
console.log(server.wsEndpoint());Endpoint tersebut tidak mempunyai log masuk. Dokumentasi API Playwright menyatakan perkara ini secara terus: "Mana-mana proses atau halaman web (termasuk yang berjalan dalam Playwright) yang mengetahui wsPath boleh mengambil alih pengguna OS." Hos lalai ialah localhost, yang "hanya menerima sambungan daripada antara muka loopback", dan dokumentasi memberi amaran bahawa memberikan alamat eksplisit seperti 0.0.0.0 "mendedahkan RPC pelayar kepada apa sahaja yang boleh mencapai port yang mendengar tersebut". --remote-debugging-port milik Chrome sendiri adalah lebih buruk. Protokol DevTools tidak mempunyai sebarang bentuk pengesahan dan bergantung sepenuhnya kepada ikatan pada loopback.
Semak perkara yang telah anda terbitkan, dan semaknya daripada mesin kedua serta daripada VPS:
ss -ltnpApa-apa sahaja pada port pelayar yang terikat kepada 0.0.0.0 adalah satu penemuan (finding). Ingat bahawa kebanyakan penyedia menjalankan firewall rangkaian berasingan dalam panel kawalan mereka, yang tidak diketahui oleh peraturan ufw anda. Capai endpoint tersebut daripada mesin lain melalui terowong SSH atau VPN peribadi sebaliknya:
ssh -N -L 3000:127.0.0.1:3000 you@your-vpsRisiko di sini adalah lebih besar daripada seseorang mencuri masa pelayar. Pelayar yang boleh anda kawal ialah mesin pemalsuan permintaan (request-forgery) yang berada di dalam rangkaian anda. Sesiapa yang mencapai soket tersebut boleh memintanya mengambil http://127.0.0.1:8080, halaman admin pangkalan data anda, atau alamat metadata awan di 169.254.169.254, dan kemudian membaca respons daripada halaman tersebut. Firewall anda melihat permintaan datang daripada VPS itu sendiri, yang dibenarkan. Anggap endpoint kawalan tersebut setara dengan akses shell pada kotak tersebut.
Pelayan MCP mempunyai bentuk yang sama. npx @playwright/mcp@latest --headless --port 8931 beroperasi melalui HTTP pada localhost, dan --host 0.0.0.0 ialah flag yang menukarkan alat tempatan kepada alat awam. README projek tersebut menyatakan dengan jelas bahawa Playwright MCP "bukanlah sempadan keselamatan". Pastikan port berada pada loopback dan biarkan ejen mencapainya melalui terowong yang sama.
Halaman yang dibaca oleh ejen anda merupakan input yang tidak dipercayai
Ejen yang melayari web terbuka menyalurkan teks yang ditulis oleh pihak asing ke dalam model yang turut menyimpan arahan anda. Sesuatu halaman boleh mengandungi teks yang ditujukan kepada model tersebut, mengarahkannya untuk mengabaikan tugasan, memanggil alat, atau menghantar data ke URL tertentu. Model menerima kedua-duanya sebagai teks, jadi ia tidak mempunyai cara yang boleh dipercayai untuk membezakan kata-kata halaman tersebut daripada arahan anda. Reka persediaan anda supaya halaman yang berniat jahat mempunyai ruang yang terhad untuk bertindak.
- Jalankan pelayar di bawah pengguna OSnya sendiri, tanpa kunci SSH dan tanpa kelayakan awan dalam persekitarannya.
- Gunakan konteks baharu bagi setiap tugasan, dan
--isolateddengan Playwright MCP, supaya sesi pada satu tapak tidak tersedia untuk halaman seterusnya. - Kekalkan senarai putih asal (origin allowlist) sekiranya tugasan membenarkannya. Playwright MCP menerima
--allowed-originsdan--blocked-originssebagai senarai yang dipisahkan dengan titik bertindih. - Wajibkan langkah pengesahan manusia sebelum sebarang tindakan yang mengubah status, seperti menghantar e-mel atau membelanjakan wang.
Lebih baik lagi, simpan keseluruhan pelayar pada mesin yang boleh anda lupus dan bina semula, yang merupakan hujah yang sama seperti menjalankan ejen pengekodan dalam VM pakai buang. Jika tugas sebenar ejen adalah carian dan bukannya pelayaran terbuka, alat yang lebih khusus adalah lebih selamat daripada pelayar penuh: kemahiran carian yang disokong oleh SearXNG anda sendiri memberikan hasil tanpa perlu memuatkan halaman yang berniat jahat tersebut.
FAQ
Mengapa Chromium ranap dalam Docker tetapi berfungsi dengan baik pada VPS yang sama secara terus?
Kerana kontena mendapat /dev/shm bersaiz 64 MB secara lalai, manakala hos mempunyai saiz yang jauh lebih besar. Chromium menghantar kandungan yang dirender melalui kawasan memori kongsi tersebut, jadi halaman yang berat akan memenuhinya dan proses renderer akan mati. Jalankan df -h /dev/shm di dalam kontena untuk mengesahkan, kemudian mulakannya sama ada dengan --ipc=host, yang menggunakan memori kongsi hos, atau dengan --shm-size=1g, yang membesarkan saiz memori kontena itu sendiri. --disable-dev-shm-usage hanya memindahkan masalah tersebut ke /tmp.
Adakah --no-sandbox selamat jika VPS tidak menjalankan apa-apa servis lain?
Tidak. Sandbox adalah mekanisme yang menghalang halaman berniat jahat daripada mencapai bahagian lain mesin, dan dokumentasi Chromium menyatakan bahawa flag tersebut "melumpuhkan ciri keselamatan kritikal Chromium dan tidak boleh digunakan semasa melayari web terbuka". Ejen yang mengikuti pautan dianggap sedang melayari web terbuka. Selesaikan punca masalah sebaliknya: jangan jalankan pelayar sebagai root, dan pada Ubuntu 24.04, tambahkan profil AppArmor yang membawa userns, untuk laluan binari pelayar, supaya namespace pengguna tanpa keistimewaan dibenarkan untuk program tersebut sahaja.
Berapa banyak pelayar yang boleh saya jalankan pada VPS kecil?
Ukur sendiri, jangan sekadar mengikut angka orang lain. Chromium memulakan satu proses renderer bagi setiap tapak web, jadi jawapannya bergantung pada halaman yang anda buka. Jalankan satu worker di bawah systemd-run dengan MemoryMax ditetapkan, baca nilai puncak daripada baris Memory: dalam systemctl status, kemudian bahagikan RAM percuma anda dengan nilai puncak tersebut dan simpan sedikit ruang tambahan. Kuatkuasakan had tersebut dengan dua cara: menggunakan baris gilir dalam kod anda dan MemoryMax dalam fail unit, supaya lonjakan permintaan akan menunggu dan bukannya menyebabkan mesin mengalami swapping.
Bolehkah ejen saya menyambung ke pelayar dari mesin lain?
Boleh, tetapi jangan sekali-kali mengikat port ke 0.0.0.0. Endpoint pelayan Playwright dan port Chrome DevTools kedua-duanya menerima mana-mana klien yang boleh mencapainya, tanpa sebarang kata laluan. Kekalkan listener pada 127.0.0.1 dan bawa sambungan tersebut melalui SSH tunnel atau VPN peribadi. Sahkan dengan ss -ltnp pada pelayan dan lakukan pemeriksaan port dari luar, serta periksa juga firewall rangkaian berasingan daripada pembekal anda.
Mengapa tangkapan skrin saya kosong sedangkan halaman jelas telah dimuatkan?
Font tidak mencukupi. Tanpa font yang merangkumi skrip halaman, teks akan dirender sebagai kotak kosong atau tidak muncul langsung, jadi halaman yang kurang imej akan kelihatan kosong. Jalankan fc-match "sans-serif:lang=ko" bagi setiap bahasa yang anda scrape, pasang fonts-noto-core dan fonts-noto-cjk apabila jawapannya adalah fallback generik, dan mulakan semula pelayar supaya fontconfig memuatkan semula cache-nya. Kontena yang langsung tiada font akan mencatatkan Fontconfig error: Cannot load default config file semasa permulaan.