Jak uruchomić headless Chromium na VPS dla AI agentów
Dowiedz się jak poprawnie skonfigurować Chromium na VPS. Rozwiązujemy problemy z błędem /dev/shm, brakującymi czcionkami oraz flagami sandbox. Zoptymalizuj środowisko dla agentów AI.
Uruchamiane oprogramowanie
Przeglądarka bez interfejsu graficznego (headless) na VPS to Chromium działające bez okna, sterowane kodem zamiast użytkownika. Na serwerze jest to długotrwałe drzewo procesów, z którym agent komunikuje się przez lokalny socket. Instalacja wymaga jednego polecenia. Cała praca zaczyna się po tym kroku. Należy ograniczyć zasoby, które przeglądarka może pobrać z maszyny, oraz odizolować jej punkt końcowy sterowania od publicznego Internetu.
Niniejszy przewodnik zakłada, że wybór narzędzia został już dokonany i konieczna jest jego obsługa. Jeśli porównywanie crawlerów i ekstraktorów nadal trwa, należy zacząć od samodzielnie hostowanych alternatyw Firecrawl i wrócić tutaj. Wszystkie poniższe instrukcje wykorzystują Chromium z pakietu Playwright, ponieważ Playwright dostarcza własną kompilację przeglądarki oraz własny instalator zależności, dzięki czemu te same polecenia działają na czystym VPS z Ubuntu oraz wewnątrz kontenera. Wersje są aktualne na sierpień 2026.
Instalacja Chromium bez zgadywania zależności
npm i -D playwright@1.62.0
npx playwright install --with-deps chromium--with-deps uruchamia apt w celu pobrania bibliotek współdzielonych i czcionek wymaganych przez Chromium, żądając uprawnień root w odpowiednim momencie. Sama kompilacja przeglądarki pobierana jest do ~/.cache/ms-playwright dla użytkownika, który wywołał polecenie. Ma to znaczenie na serwerze, ponieważ użytkownik usługi zazwyczaj różni się od użytkownika, na którego konto loguje się administrator. Pakiety systemowe należy zainstalować jednorazowo jako administrator za pomocą sudo npx playwright install-deps chromium, a następnie ustawić PLAYWRIGHT_BROWSERS_PATH=/opt/pw-browsers zarówno w poleceniu instalacji, jak i w jednostce usługi, aby współdzielić jedną kopię. Usługa, która nie ma dostępu do przeglądarki, kończy działanie przy starcie, wyświetlając komunikat ze ścieżką, w której poszukiwano plików.
Należy przypiąć wersję Playwright. Każde wydanie jest powiązane z konkretną kompilacją przeglądarki, więc nieprzypięte npm update może spowodować podmianę przeglądarki w działającej usłudze. Wersja Playwright 1.62 jest aktualna na sierpień 2026.
Istnieją dwie kompilacje Chromium i nie są one tym samym programem. Domyślne pobieranie dotyczy wersji headless shell, czyli mniejszego pliku binarnego działającego wyłącznie w trybie bezgłowym, a npx playwright install --with-deps --only-shell instaluje tylko ten komponent. Pełna przeglądarka jest dostępna w kanale chromium, co dokumentacja przeglądarek Playwright określa jako „prawdziwą przeglądarkę Chrome, która jest bardziej autentyczna, niezawodna i oferuje więcej funkcji”. Wersję shell należy stosować do masowego pobierania danych. Pełnej przeglądarki należy używać, gdy witryna zachowuje się w sposób nietypowy i konieczne jest zdiagnozowanie przyczyny.
Dlaczego przeglądarka bez interfejsu graficznego ulega awarii w kontenerze
Docker przydziela każdemu kontenerowi /dev/shm o rozmiarze 64 MB. Dokumentacja Dockera jest jednoznaczna: „Jeśli całkowicie pominiesz rozmiar, system użyje 64m”. Chromium przesyła wyrenderowaną zawartość między swoimi procesami za pośrednictwem tego współdzielonego obszaru pamięci, więc jedna złożona strona może go zapełnić. Proces renderowania kończy się wtedy niepowodzeniem, a klient zgłasza awarię celu na stronie, która działa poprawnie na komputerze lokalnym. Przed wprowadzeniem jakichkolwiek zmian sprawdź rozmiar z wnętrza kontenera.
df -h /dev/shmIstnieją dwa właściwe rozwiązania i są one alternatywami, a nie zestawem. --ipc=host umieszcza kontener w przestrzeni nazw IPC hosta, dzięki czemu korzysta on z /dev/shm hosta, która zazwyczaj stanowi połowę pamięci RAM. Przewodnik po Dockerze dla Playwright zaleca to rozwiązanie, ponieważ bez niego „Chromium może wyczerpać pamięć i ulec awarii”. Kosztem jest utrata izolacji IPC między kontenerem a hostem. --shm-size=1g zachowuje prywatną przestrzeń nazw i po prostu zwiększa rozmiar punktu montowania.
docker run --rm -it --init --ipc=host --user pwuser mcr.microsoft.com/playwright:v1.62.0-noble /bin/bashFlaga --disable-dev-shm-usage to odpowiedź, którą można znaleźć w większości wyników wyszukiwania, ale działa ona inaczej: przenosi te pliki z /dev/shm do katalogu tymczasowego. Jeśli /tmp znajduje się na dysku, zamieniasz awarię na wolniejsze renderowanie i zapisy na dysku. Jeśli /tmp jest systemem tmpfs, dane wracają do pamięci RAM bez żadnego limitu rozmiaru, co jest jednym ze sposobów, w jaki przeglądarka może zużyć zasoby małego serwera VPS. Zamiast tego należy odpowiednio skonfigurować rozmiar /dev/shm.
Jaki jest rzeczywisty koszt --no-sandbox
Chromium izoluje każdy proces renderowania w piaskownicy opartej na przestrzeniach nazw użytkownika (user namespaces) systemu Linux. Ta piaskownica stanowi granicę między wrogą stroną internetową a serwerem. Gdy nie można jej uruchomić, Chromium odmawia pracy, a w dzienniku pojawia się następujący wpis:
Failed to move to new namespace: PID namespaces supported, Network namespace supported, but failed: errno = Operation not permittedTypowa porada to użycie --no-sandbox. Dokumentacja bezpieczeństwa Chromium jasno określa cenę tego rozwiązania: flaga ta „wyłącza krytyczne funkcje bezpieczeństwa Chromium i nigdy nie powinna być używana podczas przeglądania otwartej sieci”. Agent podążający za linkami z definicji przegląda otwartą sieć. Należy znaleźć rzeczywistą przyczynę.
Dwie przyczyny obejmują niemal wszystkie przypadki. Uruchomienie przeglądarki jako root wyłącza piaskownicę, ponieważ proces nie może zrzucić uprawnień, które już posiada. Dlatego obraz Playwright zawiera zwykłego użytkownika o nazwie pwuser. W systemie Ubuntu 24.04 i nowszych, AppArmor ogranicza nieuprzywilejowane przestrzenie nazw użytkownika, a binaria Chromium znajdujące się w ścieżce nieobjętej żadnym dostarczonym profilem są blokowane. Pobranie Playwright w ~/.cache/ms-playwright jest dokładnie taką ścieżką. Należy sprawdzić obie kwestie:
id -u
sysctl kernel.apparmor_restrict_unprivileged_userns
sudo dmesg | grep -i userns_createWynik 1 z polecenia sysctl oraz linia w jądrze zawierająca apparmor="DENIED" operation="userns_create" potwierdzają drugą przyczynę. Należy zezwolić na to jedno binarne w /etc/apparmor.d/pw-chromium, co utrzyma ograniczenia dla wszystkich pozostałych elementów w systemie:
abi <abi/4.0>,
include <tunables/global>
profile pw-chromium /home/*/.cache/ms-playwright/*/chrome-linux/{chrome,headless_shell} flags=(unconfined) {
userns,
}Załaduj konfigurację za pomocą sudo apparmor_parser -r /etc/apparmor.d/pw-chromium. Ścieżka zawiera wersję przeglądarki, więc zmienia się przy każdej aktualizacji Playwright. Powyższe wzorce (globs) przetrwają tę zmianę. Profil napisany dla jednej, konkretnej ścieżki przestaje działać bez ostrzeżenia, a przeglądarka zaczyna zawodzić po aktualizacji, która wydawała się nieistotna.
Dlaczego zrzuty ekranu są puste lub zawierają same kwadraty
Pusty zrzut ekranu lub obraz wypełniony pustymi prostokątami zazwyczaj wynika z problemów z czcionkami, a nie z błędu renderowania. install-deps pobiera działającą bazę: fonts-liberation, fonts-freefont-ttf, fonts-noto-color-emoji, fonts-unifont, fonts-ipafont-gothic dla języka japońskiego, fonts-wqy-zenhei dla chińskiego, fonts-tlwg-loma-otf dla tajskiego. W tym zestawie brakuje Noto CJK, więc w przypadku języka koreańskiego i kilku innych skryptów system korzysta z czcionek zastępczych wskazanych przez fontconfig. Zamiast zgadywać, należy zapytać fontconfig:
fc-match "sans-serif:lang=ko"
fc-match "sans-serif:lang=ar"
fc-list | wc -lJeśli język, który jest istotny, wskazuje na unifont lub na czcionkę zastępczą bez poprawnych glifów, należy zainstalować fonts-noto-core oraz fonts-noto-cjk, a następnie ponownie wykonać sprawdzenie. Fontconfig buforuje wyniki, dlatego po instalacji czcionek należy zrestartować przeglądarkę. W przypadku braku jakichkolwiek czcionek, okrojony obraz rejestruje Fontconfig error: Cannot load default config file podczas uruchamiania i renderuje każdą stronę jako pustą.
Ustawienia regionalne (locale) oraz strefa czasowa są niezależne od czcionek i wpływają na treść strony, a nie tylko na jej wygląd. Kontener zazwyczaj ma nieustawione LANG oraz TZ ustawione na UTC, przez co serwisy wyświetlają język angielski i znaczniki czasu w formacie UTC, a agent raportuje czasy niezgodne z tymi, które widzi użytkownik w danym kraju. Ustawienia te należy konfigurować w kontekście przeglądarki, a nie całego systemu, dzięki czemu jedna przeglądarka może obsługiwać zadania dla różnych regionów.
const context = await browser.newContext({
locale: 'en-GB',
timezoneId: 'Europe/Paris',
});Dlaczego wyciekające procesy przeglądarki powodują swapowanie serwera
Dwa różne problemy dzielą nazwę "zombie". Prawdziwy proces zombie to zakończony proces, którego proces nadrzędny nigdy nie wywołał wait(). Zajmuje on jedynie wpis PID i nic więcej, więc nie zużywa pamięci. Zjawisko to występuje, gdy przeglądarka działa jako PID 1 w kontenerze, ponieważ PID 1 nie posiada domyślnego mechanizmu zbierania procesów (reaper). Flaga --init w Dockerze rozwiązuje ten problem, uruchamiając niewielki proces init, który "przekazuje sygnały i zbiera procesy". W Compose odpowiada za to init: true.
Wyciek, który faktycznie powoduje swapowanie serwera, jest inny: to działające procesy Chromium, których nikt nie zamknął. Dzieje się tak, gdy zadanie przerywa działanie między newContext() a close() lub gdy skrypt sterujący zostaje zabity, pozostawiając drzewo procesów przeglądarki osierocone. Najgorszy wariant to kod uruchamiający nową przeglądarkę dla każdego żądania. Należy je zliczyć:
pgrep -c -f 'headless_shell|chrome'
ps -eo pid,ppid,rss,etime,comm --sort=-rss | head -20Liczba ta powinna wracać do wartości spoczynkowej między zadaniami. Jeśli rośnie w ciągu dnia, poprawka leży w kodzie, a nie we flagach uruchomieniowych: należy zamykać kontekst w bloku finally, zamykać przeglądarkę w SIGTERM oraz restartować przeglądarkę po określonej liczbie zadań, zamiast utrzymywać jedną instancję przez miesiąc. W systemd polecenie stop lub restart zabija wszystko w cgroup jednostki, więc sudo systemctl restart browser.service stanowi niezawodny reset. Przeglądarka uruchomiona ręcznie wewnątrz multipleksera terminala nie ma takiej gwarancji, a jej osierocone procesy działają dłużej niż sesja.
Ile pamięci RAM wymaga jeden kontekst przeglądarki
Zadaj to pytanie precyzyjnie, ponieważ „jedna przeglądarka” nie oznacza jednego procesu. Chromium uruchamia proces przeglądarki, proces GPU, procesy narzędziowe oraz jeden proces renderowania na każdą witrynę, a izolacja witryn sprawia, że ramki iframe z innych domen również otrzymują własny proces renderowania. BrowserContext to oddzielny kontener plików cookie i obszar pamięci wewnątrz tego samego drzewa, więc drugi kontekst jest mało kosztowny. Druga strona jest kosztowna, ponieważ uruchamia procesy renderowania, a strona z dużą liczbą reklam uruchamia ich kilka.
Zatem wartością do zmierzenia jest szczytowe zużycie pamięci dla całego drzewa przy Twoim obciążeniu. Liczba z cudzego bloga jest tutaj bezwartościowa, ponieważ to strony otwierane przez Twojego agenta decydują o wyniku. Dokonaj pomiaru na maszynie, której będziesz używać, w odniesieniu do witryn, które będziesz odwiedzać:
sudo systemd-run --unit=browser-probe -p MemoryMax=2G -p MemorySwapMax=0 -p WorkingDirectory=/srv/agent /usr/bin/node worker.js
systemctl status browser-probeW systemie Ubuntu 24.04 wiersz Memory: w tym wyjściu raportuje zarówno bieżące, jak i szczytowe zużycie dla jednostki. Uruchom proces roboczy z jedną stroną na raz, zanotuj wartość szczytową, a następnie powtórz test z dwiema otwartymi stronami, aby sprawdzić, ile faktycznie kosztuje druga strona. Współbieżność jest wtedy kwestią arytmetyki: weź całkowitą ilość pamięci RAM, odejmij to, czego potrzebuje reszta systemu, zachowaj kilkaset MB zapasu i podziel przez zmierzone szczytowe zużycie na proces roboczy. Informacje o doborze parametrów maszyny pod tym kątem znajdują się w ile pamięci RAM i CPU potrzebuje VPS agenta.
Wymuś tę liczbę w dwóch miejscach. W kodzie użyj stałej puli procesów roboczych lub semafora, aby nagły wzrost liczby żądań agenta był kolejkowany zamiast uruchamiania kolejnych przeglądarek. W systemie operacyjnym użyj limitu cgroup, aby błąd w kolejce nie spowodował awarii całej maszyny:
[Service]
MemoryMax=2G
MemorySwapMax=0
TasksMax=512
Restart=alwaysMemorySwapMax=0 ma większe znaczenie, niż się wydaje. Bez tego ustawienia cgroup wypycha strony do pamięci swap po osiągnięciu limitu, więc serwer działa dalej, podczas gdy każde żądanie staje się wolne, co jest trudniejsze do zdiagnozowania niż czysta awaria. Dzięki niemu jądro zabija drzewo przeglądarki wewnątrz tej grupy cgroup, systemd restartuje jednostkę, a sshd przetrwa. Te same mechanizmy kontrolne w Compose to mem_limit, shm_size oraz init, omówione w ustawianie limitów pamięci w Docker Compose.
Ograniczenie dostępu do punktu końcowego przeglądarki spoza sieci publicznej
Playwright umożliwia uruchomienie przeglądarki w trybie serwera i przekazanie agentowi adresu URL WebSocket:
const { chromium } = require('playwright');
const server = await chromium.launchServer({ port: 3000 });
console.log(server.wsEndpoint());Ten punkt końcowy nie posiada mechanizmu logowania. Dokumentacja API Playwright stwierdza to wprost: „Każdy proces lub strona internetowa (w tym te uruchomione wewnątrz Playwright), która zna wsPath, może przejąć kontrolę nad użytkownikiem systemu operacyjnego”. Domyślnym hostem jest localhost, „akceptujący połączenia wyłącznie z interfejsu loopback”, a dokumentacja ostrzega, że przekazanie jawnego adresu, takiego jak 0.0.0.0, „ujawnia RPC przeglądarki każdemu, kto ma dostęp do portu nasłuchującego”. Własny protokół --remote-debugging-port przeglądarki Chrome jest jeszcze bardziej ryzykowny. Protokół DevTools nie posiada żadnego uwierzytelniania i w całości polega na powiązaniu z interfejsem loopback.
Sprawdź, co faktycznie zostało udostępnione, weryfikując to zarówno z poziomu drugiego urządzenia, jak i z samego VPS:
ss -ltnpKażda usługa przeglądarki powiązana z adresem 0.0.0.0 stanowi lukę bezpieczeństwa. Należy pamiętać, że większość dostawców stosuje zewnętrzny firewall sieciowy w panelu sterowania, o którym reguły ufw nie mają żadnej wiedzy. Zamiast tego, uzyskaj dostęp do punktu końcowego z innej maszyny za pośrednictwem tunelu SSH lub prywatnej sieci VPN:
ssh -N -L 3000:127.0.0.1:3000 you@your-vpsRyzyko jest tu znacznie większe niż tylko nieautoryzowane wykorzystanie zasobów przeglądarki. Przeglądarka, którą można sterować, staje się narzędziem do fałszowania żądań (request forgery) działającym wewnątrz Twojej sieci. Każdy, kto uzyska dostęp do tego gniazda, może wymusić pobranie http://127.0.0.1:8080, strony administracyjnej bazy danych lub adresu metadanych chmury pod 169.254.169.254, a następnie odczytać odpowiedź ze strony. Twój firewall zinterpretuje żądanie jako pochodzące z samego VPS, co jest dozwolone. Traktuj punkt końcowy sterowania na równi z dostępem do powłoki (shell) na tym serwerze.
Serwery MCP mają analogiczną strukturę. npx @playwright/mcp@latest --headless --port 8931 działa przez HTTP na localhost, a --host 0.0.0.0 to flaga, która zmienia lokalne narzędzie w publicznie dostępne. Plik README projektu jasno wskazuje, że Playwright MCP „nie stanowi bariery bezpieczeństwa”. Utrzymuj port na interfejsie loopback i pozwól agentowi łączyć się z nim za pośrednictwem tego samego tunelu.
Strony odczytywane przez agenta stanowią niezaufane dane wejściowe
Agent przeglądający otwarty internet wprowadza tekst napisany przez nieznane osoby do modelu, który przechowuje również Twoje instrukcje. Strona może zawierać tekst skierowany do tego modelu, nakazujący mu porzucenie zadania, wywołanie narzędzia lub wysłanie danych pod określony adres URL. Model otrzymuje oba rodzaje treści jako tekst, więc nie posiada niezawodnej metody odróżnienia słów ze strony od Twoich poleceń. Zaprojektuj środowisko tak, aby wroga strona miała ograniczone pole manewru.
- Uruchamiaj przeglądarkę na dedykowanym użytkowniku systemu operacyjnego, bez kluczy SSH i poświadczeń chmurowych w środowisku.
- Używaj świeżego kontekstu dla każdego zadania i
--isolatedz Playwright MCP, aby sesja z jednej witryny nie była dostępna dla kolejnej strony. - Utrzymuj listę dozwolonych źródeł (allowlist), jeśli zadanie na to pozwala. Playwright MCP przyjmuje
--allowed-originsoraz--blocked-originsjako listy rozdzielane średnikami. - Wymagaj potwierdzenia przez człowieka przed każdą akcją zmieniającą stan, taką jak wysyłanie wiadomości e-mail lub wydawanie pieniędzy.
Jeszcze lepszym rozwiązaniem jest utrzymywanie całej przeglądarki na maszynie, którą można zutylizować i odtworzyć, co jest tym samym argumentem, co uruchamianie agentów programistycznych w tymczasowej maszynie wirtualnej. Jeśli rzeczywistym zadaniem agenta jest wyszukiwanie, a nie przeglądanie dowolnych stron, węższe narzędzie jest bezpieczniejsze niż pełna przeglądarka: umiejętność wyszukiwania wspierana przez własny SearXNG zwraca wyniki bez konieczności ładowania wrogiej strony.
FAQ
Dlaczego Chromium ulega awarii w Docker, mimo że działa poprawnie na tym samym VPS uruchomiony bezpośrednio?
Kontener otrzymuje domyślnie 64 MB /dev/shm, podczas gdy host dysponuje znacznie większym obszarem. Chromium przesyła wyrenderowaną zawartość przez ten współdzielony obszar pamięci, więc ciężka strona zapełnia go, co powoduje śmierć procesu renderowania. Uruchom df -h /dev/shm wewnątrz kontenera, aby to potwierdzić, a następnie uruchom go z flagą --ipc=host, która wykorzystuje współdzieloną pamięć hosta, lub z --shm-size=1g, która zwiększa pamięć samego kontenera. --disable-dev-shm-usage jedynie przenosi problem do /tmp.
Czy --no-sandbox jest bezpieczne, jeśli na VPS nie działa nic innego?
Nie. Piaskownica (sandbox) zapobiega przedostaniu się złośliwej zawartości strony do reszty systemu, a dokumentacja Chromium podaje, że ta flaga „wyłącza krytyczne funkcje bezpieczeństwa Chromium i nigdy nie powinna być używana podczas przeglądania otwartej sieci”. Agent podążający za linkami przegląda otwartą sieć. Zamiast tego należy usunąć przyczynę: nie uruchamiaj przeglądarki jako root, a w systemie Ubuntu 24.04 dodaj profil AppArmor z wpisem userns, dla ścieżki pliku binarnego przeglądarki, aby zezwolić na użycie nieuprzywilejowanych przestrzeni nazw użytkownika dla tego konkretnego programu.
Ile przeglądarek mogę uruchomić na małym VPS?
Należy to zmierzyć, a nie przyjmować arbitralną liczbę. Chromium uruchamia jeden proces renderujący na każdą witrynę, więc odpowiedź zależy od otwieranych stron. Uruchom jednego agenta z użyciem systemd-run przy ustawionym MemoryMax, odczytaj szczytowe zużycie z linii Memory: w systemctl status, a następnie podziel dostępną pamięć RAM przez tę wartość, zachowując margines bezpieczeństwa. Wymuś wynik dwukrotnie, stosując kolejkę w kodzie oraz MemoryMax w pliku jednostki, aby nagły wzrost liczby żądań powodował oczekiwanie, zamiast przełączania pamięci na dysk (swapping).
Czy mój agent może połączyć się z przeglądarką z innej maszyny?
Tak, ale nigdy poprzez powiązanie portu z 0.0.0.0. Punkt końcowy serwera Playwright oraz port Chrome DevTools akceptują każdego klienta, który ma do nich dostęp, bez żadnego hasła. Pozostaw nasłuchiwanie na 127.0.0.1 i przesyłaj połączenie przez tunel SSH lub prywatną sieć VPN. Zweryfikuj dostępność za pomocą ss -ltnp na serwerze oraz sprawdź port z zewnątrz; sprawdź również oddzielny firewall sieciowy u dostawcy usług.
Dlaczego moje zrzuty ekranu są puste, mimo że strona wyraźnie się załadowała?
Brakuje czcionek. Bez czcionki obsługującej skrypt strony, tekst jest renderowany jako puste pola lub w ogóle nie jest wyświetlany, przez co strona zawierająca niewiele obrazów wygląda na pustą. Uruchom fc-match "sans-serif:lang=ko" dla każdego języka, który jest pobierany, zainstaluj fonts-noto-core oraz fonts-noto-cjk, gdy odpowiedzią jest ogólny zestaw zastępczy, a następnie zrestartuj przeglądarkę, aby fontconfig odświeżył swoją pamięć podręczną. Kontener bez żadnych czcionek loguje Fontconfig error: Cannot load default config file podczas uruchamiania.