Menjalankan Chromium Headless untuk AI Agent di VPS
Chromium headless di VPS dapat gagal karena /dev/shm terlalu kecil, sandbox, font yang hilang, atau proses bocor. Atur batasnya sebelum agent terkena masalah.
Yang Anda jalankan
Browser headless pada VPS adalah Chromium tanpa jendela, yang dikendalikan oleh kode Anda, bukan oleh pengguna. Pada server, browser ini berjalan sebagai pohon proses jangka panjang yang berkomunikasi dengan agent Anda melalui socket lokal. Instalasinya hanya memerlukan satu perintah. Tantangan utamanya adalah pengoperasian setelah instalasi. Batasi penggunaan resource mesin oleh browser, dan jangan ekspos endpoint kontrolnya ke Internet publik.
Panduan ini mengasumsikan bahwa pilihan tool sudah ditentukan dan Anda sekarang perlu mengoperasikannya. Jika Anda masih membandingkan crawler dan extractor, mulai dari alternatif Firecrawl yang di-host sendiri, lalu kembali ke sini. Semua contoh di bawah menggunakan Chromium milik Playwright, karena Playwright menyertakan build browser dan installer dependensinya sendiri. Dengan demikian, perintah yang sama dapat digunakan pada VPS Ubuntu tanpa konfigurasi tambahan maupun di dalam container. Versi yang digunakan adalah versi terbaru per August 2026.
Instal Chromium tanpa menebak dependensi
npm i -D playwright@1.62.0
npx playwright install --with-deps chromium--with-deps menjalankan apt untuk pustaka bersama dan font yang diperlukan Chromium, lalu meminta akses root saat diperlukan. Build browser itu sendiri diunduh ke ~/.cache/ms-playwright untuk pengguna yang menjalankan perintah. Hal ini penting pada server karena pengguna service biasanya bukan pengguna yang Anda gunakan untuk login. Instal paket sistem satu kali sebagai admin dengan sudo npx playwright install-deps chromium, lalu tetapkan PLAYWRIGHT_BROWSERS_PATH=/opt/pw-browsers pada perintah instalasi dan unit service agar satu salinan dapat digunakan bersama. Service yang tidak dapat mengakses browser akan gagal saat dijalankan dan menampilkan pesan yang menyebutkan path yang dicari.
Tetapkan versi Playwright secara eksplisit. Setiap rilis terkait dengan satu build browser, sehingga npm update tanpa versi tetap dapat mengganti browser yang digunakan oleh service yang sedang berjalan. Playwright 1.62 adalah versi terbaru per August 2026.
Terdapat 2 build Chromium, dan keduanya bukan program yang sama. Unduhan default adalah headless shell, yaitu binary yang lebih kecil dan hanya berjalan dalam mode headless; npx playwright install --with-deps --only-shell hanya menginstal komponen tersebut. Browser lengkap diperoleh melalui channel chromium, yang dalam dokumentasi browser Playwright disebut sebagai "the real Chrome browser, and is thus more authentic, reliable, and offers more features". Gunakan shell untuk pengambilan data dalam jumlah besar. Gunakan browser lengkap ketika perilaku suatu situs berbeda dan Anda perlu mengetahui penyebabnya.
Mengapa headless browser mengalami crash di dalam container
Docker memberikan setiap container /dev/shm sebesar 64 MB. Dokumentasi Docker menyatakan dengan jelas: "Jika ukurannya dihilangkan sepenuhnya, sistem menggunakan 64m". Chromium meneruskan konten yang dirender antarproses melalui area shared memory tersebut, sehingga satu halaman yang berat dapat memenuhinya. Proses renderer kemudian berhenti, dan client Anda melaporkan target yang mengalami crash, meskipun halaman tersebut berjalan normal di laptop Anda. Pastikan ukurannya dari dalam container sebelum mengubah apa pun.
df -h /dev/shmAda dua solusi yang benar, dan keduanya merupakan alternatif, bukan pasangan yang harus digunakan bersamaan. --ipc=host menempatkan container di namespace IPC milik host sehingga container menggunakan /dev/shm milik host, yang biasanya berukuran setengah dari RAM. Panduan Docker Playwright merekomendasikan opsi ini karena tanpa opsi tersebut, "Chromium dapat kehabisan memori dan mengalami crash". Konsekuensinya, isolasi IPC antara container dan host tidak lagi tersedia. --shm-size=1g mempertahankan namespace privat dan hanya memperbesar ukuran mount tersebut.
docker run --rm -it --init --ipc=host --user pwuser mcr.microsoft.com/playwright:v1.62.0-noble /bin/bashFlag --disable-dev-shm-usage adalah jawaban yang akan Anda temukan pada sebagian besar hasil pencarian, tetapi fungsinya berbeda: flag ini memindahkan file tersebut dari /dev/shm ke direktori sementara. Jika /tmp berada pada disk, Anda menukar crash dengan proses rendering yang lebih lambat dan penulisan ke disk. Jika /tmp adalah tmpfs, data tetap berada di RAM tanpa batas ukuran sama sekali. Ini dapat membuat browser menghabiskan RAM pada VPS kecil. Atur ukuran /dev/shm dengan benar.
Biaya sebenarnya dari --no-sandbox
Chromium mengisolasi setiap renderer dalam sandbox yang dibangun berdasarkan Linux user namespaces. Sandbox tersebut menjadi batas antara halaman berbahaya dan server Anda. Jika sandbox tidak dapat dijalankan, Chromium menolak untuk berjalan, dan log akan memuat baris seperti ini:
Failed to move to new namespace: PID namespaces supported, Network namespace supported, but failed: errno = Operation not permittedSaran yang biasanya diberikan adalah --no-sandbox. Dokumentasi keamanan Chromium sendiri menjelaskan risikonya secara tegas: flag tersebut “menonaktifkan fitur keamanan penting Chromium dan tidak boleh digunakan saat menjelajahi web terbuka”. Agen yang mengikuti tautan pada dasarnya sedang menjelajahi web terbuka. Cari penyebab sebenarnya.
Dua penyebab mencakup hampir semua kasus. Menjalankan browser sebagai root menonaktifkan sandbox karena browser tidak dapat menurunkan hak akses yang sudah dimilikinya. Karena itu, image Playwright menyediakan user biasa bernama pwuser. Pada Ubuntu 24.04 dan versi setelahnya, AppArmor membatasi user namespaces yang tidak memiliki hak istimewa. Binary Chromium pada path yang tidak dicakup oleh profile yang disediakan akan ditolak. Download Playwright di bawah ~/.cache/ms-playwright tepat merupakan path seperti itu. Periksa keduanya:
id -u
sysctl kernel.apparmor_restrict_unprivileged_userns
sudo dmesg | grep -i userns_createNilai 1 dari sysctl, ditambah baris kernel yang memuat apparmor="DENIED" operation="userns_create", mengonfirmasi penyebab kedua. Izinkan binary tersebut di /etc/apparmor.d/pw-chromium agar pembatasan tetap berlaku untuk semua hal lain di server:
abi <abi/4.0>,
include <tunables/global>
profile pw-chromium /home/*/.cache/ms-playwright/*/chrome-linux/{chrome,headless_shell} flags=(unconfined) {
userns,
}Muat dengan sudo apparmor_parser -r /etc/apparmor.d/pw-chromium. Path tersebut memuat revisi browser, sehingga berubah pada setiap upgrade Playwright. Globs di atas tetap berfungsi. Profile yang ditulis untuk satu path tertentu akan berhenti cocok tanpa pesan, dan browser kembali gagal setelah pembaruan yang tampaknya tidak berkaitan.
Mengapa screenshot tampil kosong atau penuh kotak
Screenshot yang kosong atau penuh dengan persegi panjang kosong biasanya disebabkan oleh masalah font, bukan bug rendering. install-deps menyediakan basis yang berfungsi: fonts-liberation, fonts-freefont-ttf, fonts-noto-color-emoji, fonts-unifont, fonts-ipafont-gothic untuk bahasa Jepang, fonts-wqy-zenhei untuk bahasa Mandarin, dan fonts-tlwg-loma-otf untuk bahasa Thai. Set tersebut tidak menyertakan Noto CJK, sehingga bahasa Korea dan beberapa skrip lain menggunakan font cadangan yang dapat ditemukan fontconfig. Minta fontconfig memeriksanya, bukan menebak:
fc-match "sans-serif:lang=ko"
fc-match "sans-serif:lang=ar"
fc-list | wc -lJika bahasa yang Anda perlukan diarahkan ke unifont atau ke font cadangan yang tidak memiliki glyph nyata, instal fonts-noto-core dan fonts-noto-cjk, lalu jalankan pemeriksaan lagi. Fontconfig menyimpan hasilnya dalam cache, jadi restart browser setelah menginstal font. Image yang dipangkas tanpa font sama sekali mencatat Fontconfig error: Cannot load default config file saat startup dan merender setiap halaman sebagai halaman kosong.
Locale dan zona waktu terpisah dari font. Keduanya mengubah isi halaman, bukan hanya tampilannya. Container biasanya memiliki LANG yang tidak disetel dan TZ pada UTC, sehingga situs menyajikan bahasa Inggris dan menampilkan timestamp UTC, sementara agent Anda melaporkan waktu yang tidak sesuai dengan tampilan bagi pengguna di negara tersebut. Setel keduanya per browser context, bukan per mesin, sehingga satu browser dapat menjalankan tugas untuk berbagai wilayah.
const context = await browser.newContext({
locale: 'en-GB',
timezoneId: 'Europe/Paris',
});Mengapa proses browser yang bocor membuat server menggunakan swap
Dua masalah berbeda menggunakan istilah "zombie". Zombie yang sebenarnya adalah proses yang sudah selesai, tetapi proses induknya tidak pernah memanggil wait(). Proses ini hanya mempertahankan entri PID dan tidak melakukan hal lain, sehingga tidak menggunakan memori. Anda akan mengumpulkan proses seperti ini ketika browser berjalan sebagai PID 1 di dalam container, karena PID 1 tidak memiliki reaper bawaan. Flag --init milik Docker memperbaiki masalah tersebut secara khusus dengan menjalankan init kecil yang "meneruskan sinyal dan membersihkan proses". Di Compose, pengaturan yang sama adalah init: true.
Kebocoran yang benar-benar membuat server menggunakan swap berbeda: proses Chromium yang masih berjalan dan tidak ditutup oleh siapa pun. Hal ini terjadi ketika sebuah task gagal di antara newContext() dan close(), atau ketika script pengendali dihentikan dan meninggalkan tree browser tanpa induk. Kasus terburuknya adalah kode yang menjalankan browser baru untuk setiap request. Hitung prosesnya:
pgrep -c -f 'headless_shell|chrome'
ps -eo pid,ppid,rss,etime,comm --sort=-rss | head -20Jumlah tersebut seharusnya kembali ke nilai saat idle di antara task. Jika jumlahnya terus meningkat selama sehari, perbaikannya ada di kode Anda, bukan pada flag peluncuran: tutup context di dalam blok finally, tutup browser saat SIGTERM, dan recycle browser setelah jumlah task tertentu, bukan menjalankan satu browser selama sebulan. Dengan systemd, tindakan stop atau restart akan menghentikan semua proses dalam cgroup unit tersebut, sehingga sudo systemctl restart browser.service merupakan reset yang andal. Browser yang dijalankan secara manual di dalam terminal multiplexer tidak memiliki jaminan tersebut, dan proses tanpa induknya dapat tetap berjalan setelah session berakhir.
Berapa RAM yang dibutuhkan satu browser context
Ajukan pertanyaan ini secara tepat, karena “satu browser” bukan berarti satu proses. Chromium menjalankan satu browser process, satu GPU process, beberapa utility process, dan satu renderer process untuk setiap situs. Site isolation juga membuat iframe lintas situs memiliki renderer sendiri. Sebuah BrowserContext adalah cookie jar dan area penyimpanan terpisah di dalam tree yang sama, sehingga context kedua tidak banyak menambah beban. Namun, page kedua berbeda, karena page tersebut memulai renderer process, dan page yang dipenuhi iklan dapat memulai beberapa process.
Jadi, angka yang perlu diukur adalah penggunaan memori puncak untuk seluruh tree berdasarkan workload Anda sendiri. Angka dari blog orang lain tidak berguna dalam kasus ini, karena halaman yang dibuka agent Anda menentukan hasilnya. Lakukan pengukuran pada mesin yang akan digunakan dan terhadap situs yang akan dikunjungi:
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 saat ini dan penggunaan puncak untuk unit tersebut. Jalankan worker dengan satu page setiap kali, catat penggunaan puncaknya, lalu ulangi dengan dua page terbuka untuk mengetahui biaya nyata page kedua. Setelah itu, concurrency dapat dihitung secara aritmetis: ambil total RAM, kurangi kebutuhan komponen lain pada mesin, sisakan headroom beberapa ratus MB, lalu bagi dengan penggunaan puncak terukur per worker. Untuk menentukan ukuran mesin yang mendasarinya, lihat berapa RAM dan CPU yang dibutuhkan VPS agent.
Terapkan batas tersebut di dua tempat. Dalam kode, gunakan fixed worker pool atau semaphore agar lonjakan request dari agent masuk ke antrean, bukan meluncurkan browser baru. Pada OS, gunakan cgroup limit agar bug pada antrean tidak ikut menjatuhkan mesin:
[Service]
MemoryMax=2G
MemorySwapMax=0
TasksMax=512
Restart=alwaysMemorySwapMax=0 lebih penting daripada yang terlihat. Tanpa pengaturan ini, cgroup memindahkan page ke swap saat mencapai limit. Mesin tetap berjalan, tetapi setiap request menjadi lambat. Kondisi ini lebih sulit didiagnosis daripada kegagalan yang jelas. Dengan pengaturan ini, kernel menghentikan browser tree di dalam cgroup tersebut, systemd me-restart unit, dan sshd tetap bertahan. Kontrol yang sama dalam Compose adalah mem_limit, shm_size, dan init, yang dibahas dalam mengatur memory limit di Docker Compose.
Jangan ekspos endpoint browser ke Internet publik
Playwright dapat menjalankan browser sebagai server dan memberikan URL WebSocket kepada agent Anda:
const { chromium } = require('playwright');
const server = await chromium.launchServer({ port: 3000 });
console.log(server.wsEndpoint());Endpoint tersebut tidak memiliki login. Dokumentasi API Playwright menyatakannya secara langsung: "Proses atau halaman web apa pun (termasuk yang berjalan di Playwright) yang mengetahui wsPath dapat mengambil alih OS user." Host default adalah localhost, "yang hanya menerima koneksi dari interface loopback", dan dokumentasi memperingatkan bahwa penggunaan alamat eksplisit seperti 0.0.0.0 "mengekspos RPC browser kepada apa pun yang dapat menjangkau port yang listening". --remote-debugging-port milik Chrome sendiri lebih berisiko. Protokol DevTools tidak memiliki autentikasi dalam bentuk apa pun dan sepenuhnya bergantung pada binding ke loopback.
Periksa apa yang benar-benar telah Anda publikasikan. Periksa juga dari mesin kedua, selain dari VPS:
ss -ltnpApa pun yang berjalan pada port browser yang di-bind ke 0.0.0.0 merupakan temuan keamanan. Ingat bahwa sebagian besar provider menjalankan firewall jaringan terpisah pada control panel mereka. Aturan ufw Anda tidak mengetahui firewall tersebut. Akses endpoint dari mesin lain melalui SSH tunnel atau VPN privat:
ssh -N -L 3000:127.0.0.1:3000 you@your-vpsRisikonya lebih besar daripada sekadar seseorang mencuri waktu browser. Browser yang dapat Anda kendalikan merupakan mesin request forgery yang berada di dalam jaringan Anda. Siapa pun yang dapat menjangkau socket tersebut dapat membuatnya mengambil http://127.0.0.1:8080, halaman administrasi database Anda, atau alamat metadata cloud di 169.254.169.254, lalu membaca responsnya dari halaman tersebut. Firewall Anda melihat request yang berasal dari VPS itu sendiri, sehingga request tersebut diizinkan. Perlakukan control endpoint ini setara dengan akses shell ke mesin tersebut.
MCP server memiliki pola yang sama. npx @playwright/mcp@latest --headless --port 8931 melayani melalui HTTP pada localhost, sedangkan --host 0.0.0.0 adalah flag yang mengubah tool lokal menjadi tool publik. README proyek tersebut menyatakan dengan jelas bahwa Playwright MCP "bukan security boundary". Pertahankan port pada loopback dan biarkan agent mengaksesnya melalui tunnel yang sama.
Halaman yang dibaca agent Anda merupakan input yang tidak tepercaya
Agent yang menjelajahi web terbuka memasukkan teks yang ditulis orang lain ke dalam model yang juga menerima instruksi Anda. Sebuah halaman dapat memuat teks yang ditujukan kepada model tersebut, yang memerintahkannya untuk meninggalkan tugas, memanggil tool, atau mengirim data ke URL. Model menerima semuanya sebagai teks, sehingga tidak memiliki cara yang andal untuk membedakan kata-kata dari halaman dan instruksi Anda. Rancang konfigurasi agar halaman berbahaya memiliki sedikit peluang untuk memengaruhi sistem.
- Jalankan browser dengan user OS khusus, tanpa SSH key dan tanpa kredensial cloud di environment-nya.
- Gunakan context baru untuk setiap tugas, dan
--isolateddengan Playwright MCP, sehingga session pada satu situs tidak tersedia bagi halaman berikutnya. - Pertahankan allowlist origin jika job memerlukannya. Playwright MCP menerima
--allowed-originsdan--blocked-originssebagai daftar yang dipisahkan dengan titik koma. - Wajibkan langkah yang dilakukan manusia sebelum tindakan apa pun yang mengubah state, seperti mengirim email atau membelanjakan uang.
Lebih baik lagi, jalankan seluruh browser pada mesin yang dapat Anda hapus dan bangun ulang. Alasannya sama seperti menjalankan coding agent dalam VM sekali pakai. Jika tugas utama agent hanya melakukan pencarian, bukan menjelajah secara bebas, tool yang lebih terbatas lebih aman daripada browser lengkap: skill pencarian yang didukung SearXNG milik Anda sendiri mengembalikan hasil tanpa pernah memuat halaman berbahaya tersebut.
FAQ
Mengapa Chromium crash di Docker, tetapi berjalan normal pada VPS yang sama secara langsung?
Karena container secara default mendapatkan /dev/shm sebesar 64 MB, sedangkan host memiliki yang jauh lebih besar. Chromium meneruskan konten yang dirender melalui area shared memory tersebut. Halaman yang berat dapat memenuhi area itu sehingga proses renderer berhenti. Jalankan df -h /dev/shm di dalam container untuk memastikannya. Kemudian jalankan browser dengan --ipc=host, yang menggunakan shared memory milik host, atau dengan --shm-size=1g, yang memperbesar shared memory milik container. --disable-dev-shm-usage hanya memindahkan masalah ke /tmp.
Apakah --no-sandbox aman jika VPS tidak menjalankan apa pun selain itu?
Tidak. Sandbox mencegah halaman berbahaya mengakses bagian lain dari mesin. Dokumentasi Chromium menyatakan bahwa flag tersebut "menonaktifkan fitur keamanan penting Chromium dan tidak boleh digunakan saat menjelajah web terbuka". Agent yang mengikuti tautan berarti menjelajah web terbuka. Perbaiki penyebabnya. Jangan jalankan browser sebagai root. Pada Ubuntu 24.04, tambahkan profil AppArmor yang memuat userns, untuk path binary browser agar user namespace yang tidak memiliki hak istimewa diizinkan hanya untuk program tersebut.
Berapa banyak browser yang dapat saya jalankan pada VPS kecil?
Ukur kapasitasnya, bukan menyalin angka. Chromium memulai satu proses renderer untuk setiap situs. Jadi, jumlahnya bergantung pada halaman yang dibuka. Jalankan satu worker dengan systemd-run, atur MemoryMax, lalu baca penggunaan puncaknya dari baris Memory: di systemctl status. Setelah itu, bagi RAM yang tersedia dengan penggunaan puncak tersebut dan sisakan ruang cadangan. Terapkan batas tersebut dua kali: gunakan queue dalam kode dan MemoryMax di file unit, agar lonjakan permintaan menunggu, bukan membuat mesin menggunakan swap.
Dapatkah agent saya terhubung ke browser dari mesin lain?
Ya, tetapi jangan pernah mengikat port ke 0.0.0.0. Endpoint server Playwright dan port Chrome DevTools menerima koneksi dari client mana pun yang dapat menjangkaunya, tanpa kata sandi. Biarkan listener tetap pada 127.0.0.1, lalu teruskan koneksi melalui SSH tunnel atau VPN privat. Verifikasi dengan ss -ltnp di server dan lakukan pemeriksaan port dari luar. Periksa juga network firewall terpisah milik provider Anda.
Mengapa screenshot saya kosong padahal halaman jelas sudah dimuat?
Font tidak tersedia. Jika tidak ada font yang mendukung skrip halaman, teks dirender sebagai kotak kosong atau tidak dirender sama sekali. Akibatnya, halaman yang minim gambar tampak kosong. Jalankan fc-match "sans-serif:lang=ko" untuk setiap bahasa yang Anda scrape. Instal fonts-noto-core dan fonts-noto-cjk jika hasilnya adalah fallback generik. Kemudian restart browser agar fontconfig memuat ulang cache-nya. Container yang sama sekali tidak memiliki font akan mencatat Fontconfig error: Cannot load default config file saat startup.