VPS에서 헤드리스 브라우저 실행 시 오류 해결 방법
VPS 환경에서 Playwright 기반 Chromium을 구동할 때 발생하는 /dev/shm 부족, 샌드박스 설정, 폰트 누락 문제를 해결합니다. 에이전트 운영 시 필수적인 프로세스 관리와 리소스 제한 설정 가이드를 확인하여 안정적인 크롤링 환경을 구축하십시오.
현재 실행 중인 환경
VPS에서 구동하는 헤드리스 브라우저는 창이 없는 Chromium이며, 사람이 아닌 코드가 이를 제어합니다. 서버 환경에서 이는 에이전트가 로컬 소켓을 통해 통신하는 장기 실행 프로세스 트리입니다. 설치는 명령어 한 줄로 완료되지만, 그 이후의 작업이 핵심입니다. 브라우저가 시스템 자원을 점유하는 한도를 설정하고, 제어용 엔드포인트가 외부 인터넷에 노출되지 않도록 차단해야 합니다.
이 가이드는 도구 선택이 이미 완료되었고 이제 운영 단계에 진입한 사용자를 가정합니다. 아직 크롤러나 추출 도구를 비교 중이라면 자체 호스팅 Firecrawl 대안을 먼저 확인한 뒤 돌아오십시오. 아래의 모든 내용은 Playwright의 Chromium을 기준으로 합니다. Playwright는 자체 브라우저 빌드와 의존성 설치 도구를 포함하고 있어, 베어메탈 Ubuntu VPS와 컨테이너 내부에서 동일한 명령어가 작동합니다. 버전은 2026년 8월 기준입니다.
의존성 문제를 겪지 않고 Chromium 설치하기
npm i -D playwright@1.62.0
npx playwright install --with-deps chromium--with-deps는 Chromium에 필요한 공유 라이브러리와 폰트를 위해 apt를 실행하며, 필요한 시점에 root 권한을 요청합니다. 브라우저 빌드 자체는 명령을 실행한 사용자의 ~/.cache/ms-playwright 디렉터리로 다운로드됩니다. 서버 환경에서는 서비스 실행 사용자가 일반적으로 로그인한 사용자와 다르기 때문에 이 점이 중요합니다. 관리자 권한으로 sudo npx playwright install-deps chromium을 사용하여 시스템 패키지를 한 번 설치한 다음, 설치 명령과 서비스 유닛 모두에 PLAYWRIGHT_BROWSERS_PATH=/opt/pw-browsers을 설정하여 하나의 복사본을 공유하도록 하십시오. 브라우저를 찾을 수 없는 서비스는 실행 시 검색 경로를 명시한 오류 메시지를 출력하며 실패합니다.
Playwright 버전을 고정하십시오. 각 릴리스는 특정 브라우저 빌드와 연결되어 있으므로, 버전을 고정하지 않은 npm update는 실행 중인 서비스의 브라우저를 예고 없이 변경할 수 있습니다. 2026년 8월 기준으로 Playwright 1.62가 최신 버전입니다.
두 가지 Chromium 빌드가 존재하며 이들은 서로 다른 프로그램입니다. 기본 다운로드 방식은 헤드리스 모드에서만 실행되는 더 작은 바이너리인 헤드리스 셸이며, npx playwright install --with-deps --only-shell은 이것만을 설치합니다. 전체 브라우저는 chromium 채널을 통해 얻을 수 있으며, Playwright 브라우저 문서에서는 이를 "실제 Chrome 브라우저이며, 따라서 더 정확하고 신뢰할 수 있으며 더 많은 기능을 제공한다"고 설명합니다. 대량의 데이터를 가져올 때는 셸을 사용하십시오. 사이트가 다르게 동작하여 원인을 파악해야 할 때는 전체 브라우저를 사용하십시오.
컨테이너에서 헤드리스 브라우저가 충돌하는 이유
Docker는 각 컨테이너에 64 MB의 /dev/shm를 할당합니다. Docker 문서에는 "크기를 생략하면 시스템은 64m을 사용합니다"라고 명시되어 있습니다. Chromium은 렌더링된 콘텐츠를 이 공유 메모리 영역을 통해 프로세스 간에 전달하므로, 무거운 페이지 하나가 이 영역을 가득 채울 수 있습니다. 그러면 렌더러가 종료되고 클라이언트는 로컬 노트북에서는 잘 작동하던 페이지에서 대상이 충돌했다는 오류를 보고합니다. 설정을 변경하기 전에 컨테이너 내부에서 크기를 먼저 확인하십시오.
df -h /dev/shm두 가지 확실한 해결책이 있으며, 이들은 병행하는 것이 아니라 선택적인 대안입니다. --ipc=host은 컨테이너를 호스트 IPC 네임스페이스에 배치하여 호스트의 /dev/shm를 사용하게 하며, 이는 일반적으로 RAM의 절반 크기입니다. Playwright의 Docker 가이드는 이 설정을 권장하는데, 그렇지 않으면 "Chromium이 메모리 부족으로 충돌할 수 있기" 때문입니다. 이 방식의 대가는 컨테이너와 호스트 간의 IPC 격리가 해제된다는 점입니다. --shm-size=1g는 프라이빗 네임스페이스를 유지하면서 마운트 크기만 늘립니다.
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이 디스크에 위치한다면, 충돌을 피하는 대신 렌더링 속도 저하와 디스크 쓰기 부하를 얻게 됩니다. 만약 /tmp가 tmpfs라면 데이터는 다시 RAM으로 돌아가며 크기 제한이 완전히 사라지는데, 이는 브라우저가 작은 VPS의 자원을 모두 소모하게 만드는 원인이 됩니다. 대신 /dev/shm을 적절한 크기로 설정하십시오.
--no-sandbox 옵션의 실제 비용
Chromium은 Linux 사용자 네임스페이스를 기반으로 구축된 샌드박스 내에서 모든 렌더러를 격리합니다. 이 샌드박스는 악의적인 페이지와 서버 사이의 경계 역할을 합니다. 샌드박스가 시작되지 않으면 Chromium은 실행을 거부하며, 로그에는 다음과 같은 줄이 남습니다.
Failed to move to new namespace: PID namespaces supported, Network namespace supported, but failed: errno = Operation not permitted일반적인 해결책은 --no-sandbox을 사용하는 것입니다. Chromium의 자체 보안 문서에서는 이 옵션의 대가를 명확히 밝히고 있습니다. 해당 플래그는 "Chromium의 핵심 보안 기능을 비활성화하므로, 오픈 웹을 탐색할 때는 절대 사용해서는 안 됩니다." 링크를 따라가는 에이전트는 정의상 오픈 웹을 탐색하는 것과 같습니다. 따라서 실제 원인을 찾아야 합니다.
대부분의 사례는 두 가지 원인으로 압축됩니다. 브라우저를 root 권한으로 실행하면 샌드박스가 비활성화됩니다. 이미 가진 권한을 낮출 수 없기 때문입니다. 이것이 Playwright 이미지에 pwuser라는 일반 사용자가 포함된 이유입니다. Ubuntu 24.04 이상에서는 AppArmor가 권한 없는 사용자 네임스페이스를 제한하며, 기본 프로필에 포함되지 않은 경로에 있는 Chromium 바이너리는 실행이 거부됩니다. ~/.cache/ms-playwright 아래에 다운로드된 Playwright는 정확히 이러한 경로에 해당합니다. 다음 두 가지를 확인하십시오.
id -u
sysctl kernel.apparmor_restrict_unprivileged_userns
sudo dmesg | grep -i userns_createsysctl에서 출력된 1와 apparmor="DENIED" operation="userns_create"를 포함하는 커널 로그는 두 번째 원인을 확인해 줍니다. /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을 사용하여 프로필을 로드하십시오. 해당 경로에는 브라우저 리비전이 포함되어 있으므로 Playwright가 업데이트될 때마다 경로가 변경됩니다. 위의 glob 패턴을 사용하면 업데이트 후에도 설정이 유지됩니다. 특정 경로 하나만 지정하여 작성된 프로필은 경로가 바뀌면 조용히 매칭을 멈추며, 관련 없어 보이는 업데이트 이후 브라우저 실행이 다시 실패하게 됩니다.
스크린샷이 빈 화면이나 상자로 가득 차게 나오는 이유
스크린샷이 비어 있거나 빈 사각형으로 가득 차 있다면, 이는 렌더링 버그보다는 폰트 문제일 가능성이 큽니다. 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으로 확인되거나 실제 글리프가 없는 폰트로 대체된다면, fonts-noto-core과 fonts-noto-cjk을 설치한 뒤 다시 확인하십시오. Fontconfig는 결과를 캐시하므로 폰트 설치 후 브라우저를 재시작해야 합니다. 폰트가 전혀 없는 환경에서 이미지를 생성하면 시작 시 Fontconfig error: Cannot load default config file 로그가 남으며 모든 페이지가 빈 화면으로 렌더링됩니다.
로케일과 시간대는 폰트와 별개이며, 단순히 모양뿐만 아니라 페이지에 표시되는 내용 자체를 바꿉니다. 컨테이너는 일반적으로 LANG이 설정되어 있지 않고 TZ이 UTC로 되어 있습니다. 이 때문에 사이트는 영어로 표시되고 타임스탬프는 UTC로 출력되며, 에이전트는 해당 국가의 사용자가 보는 시간과 일치하지 않는 시간을 보고하게 됩니다. 머신 단위가 아닌 브라우저 컨텍스트별로 설정하면, 하나의 브라우저로 여러 지역의 작업을 처리할 수 있습니다.
const context = await browser.newContext({
locale: 'en-GB',
timezoneId: 'Europe/Paris',
});브라우저 프로세스 누수로 인해 시스템이 스왑을 사용하는 이유
"좀비"라는 이름은 서로 다른 두 가지 문제를 지칭합니다. 진정한 의미의 좀비 프로세스는 부모 프로세스가 wait()를 호출하지 않아 종료된 프로세스를 말합니다. 이는 PID 항목만 점유할 뿐 메모리를 소비하지 않습니다. 컨테이너에서 브라우저를 PID 1로 실행할 때 이러한 현상이 발생하는데, PID 1은 기본적으로 리퍼(reaper) 역할을 수행하지 않기 때문입니다. Docker의 --init 플래그는 "신호를 전달하고 프로세스를 수거하는" 작은 init 프로세스를 실행하여 이 문제를 정확히 해결합니다. Compose에서는 동일한 기능을 init: true로 설정할 수 있습니다.
실제로 시스템이 스왑을 사용하게 만드는 누수는 성격이 다릅니다. 이는 종료되지 않고 살아 있는 Chromium 프로세스들입니다. 이 문제는 작업이 newContext()와 close() 사이에서 예외를 발생시키거나, 제어 스크립트가 강제 종료되면서 브라우저 트리만 고아 프로세스로 남을 때 발생합니다. 가장 심각한 경우는 요청마다 새로운 브라우저를 실행하는 코드입니다. 다음 명령어로 개수를 확인하십시오.
pgrep -c -f 'headless_shell|chrome'
ps -eo pid,ppid,rss,etime,comm --sort=-rss | head -20작업이 끝나면 프로세스 개수는 유휴 상태의 값으로 돌아와야 합니다. 만약 하루 동안 개수가 계속 증가한다면, 실행 플래그가 아닌 코드에서 해결책을 찾아야 합니다. finally 블록 내에서 컨텍스트를 닫고, SIGTERM 시점에 브라우저를 종료하며, 한 달 내내 실행하는 대신 일정 작업 횟수마다 브라우저를 재시작하십시오. systemd 환경에서는 서비스 중지나 재시작 시 해당 유닛의 cgroup에 속한 모든 프로세스가 종료되므로, sudo systemctl restart browser.service를 사용하면 확실하게 초기화할 수 있습니다. 터미널 멀티플렉서 내부에서 수동으로 실행한 브라우저는 이러한 보장이 없으므로, 세션이 종료되어도 고아 프로세스가 계속 살아남게 됩니다.
브라우저 컨텍스트 하나당 필요한 RAM 용량은 얼마인가
"브라우저 하나"가 하나의 프로세스를 의미하는 것이 아니므로 질문을 정확히 해야 합니다. Chromium은 브라우저 프로세스, GPU 프로세스, 유틸리티 프로세스, 그리고 사이트당 하나의 렌더러 프로세스를 실행하며, 사이트 격리(site isolation) 기능은 사이트 간 iframe에 대해서도 별도의 렌더러를 할당합니다. BrowserContext은 동일한 트리 내의 별도 쿠키 저장소 및 스토리지 영역이므로, 두 번째 컨텍스트를 생성하는 비용은 저렴합니다. 반면 두 번째 페이지는 렌더러 프로세스를 새로 시작하므로 비용이 적지 않으며, 광고가 많은 페이지는 여러 개의 프로세스를 추가로 생성합니다.
따라서 측정해야 할 수치는 본인의 워크로드 환경에서 전체 트리 구조가 사용하는 최대 메모리(peak memory)입니다. 타인의 블로그에 기재된 수치는 의미가 없습니다. 에이전트가 여는 페이지에 따라 결과가 달라지기 때문입니다. 실제 사용할 머신에서 방문할 사이트를 대상으로 직접 측정하십시오:
sudo systemd-run --unit=browser-probe -p MemoryMax=2G -p MemorySwapMax=0 -p WorkingDirectory=/srv/agent /usr/bin/node worker.js
systemctl status browser-probeUbuntu 24.04 환경에서 해당 출력의 Memory: 라인은 해당 유닛의 현재 사용량과 최대 사용량을 모두 보고합니다. 워커를 한 번에 한 페이지씩 실행하여 최대치를 기록한 뒤, 두 페이지를 열어 두 번째 페이지가 실제로 소모하는 비용을 확인하십시오. 동시성 계산은 산술적으로 접근합니다. 전체 RAM에서 시스템이 필요로 하는 용량을 빼고, 수백 MB의 여유 공간을 확보한 뒤, 측정된 워커당 최대 메모리 사용량으로 나눕니다. 이 작업을 수행할 머신의 사양을 결정하려면 에이전트 VPS에 필요한 RAM 및 CPU 용량을 참조하십시오.
이 수치를 두 곳에서 강제하십시오. 코드 내에서는 고정된 워커 풀이나 세마포어를 사용하여, 에이전트 요청이 급증할 때 브라우저를 무분별하게 실행하지 않고 대기열에 쌓이도록 합니다. 운영체제 수준에서는 cgroup 제한을 사용하여, 대기열의 버그가 머신 전체를 다운시키는 상황을 방지하십시오:
[Service]
MemoryMax=2G
MemorySwapMax=0
TasksMax=512
Restart=alwaysMemorySwapMax=0는 보이는 것보다 훨씬 중요합니다. 이 설정이 없으면 cgroup은 제한에 도달했을 때 페이지를 스왑으로 밀어내며, 이로 인해 시스템은 유지되지만 모든 요청이 느려집니다. 이는 깔끔한 실패보다 진단하기가 훨씬 어렵습니다. 이 설정을 사용하면 커널이 해당 cgroup 내의 브라우저 트리를 종료시키고, systemd가 유닛을 재시작하며, sshd은 정상적으로 유지됩니다. Docker Compose에서 동일한 제어를 수행하는 설정은 mem_limit, shm_size, init이며, 자세한 내용은 Docker Compose에서 메모리 제한 설정하기에서 다룹니다.
브라우저 엔드포인트를 공용 인터넷에서 격리하기
Playwright는 브라우저를 서버로 실행하여 에이전트에 WebSocket URL을 제공할 수 있습니다.
const { chromium } = require('playwright');
const server = await chromium.launchServer({ port: 3000 });
console.log(server.wsEndpoint());해당 엔드포인트에는 로그인 절차가 없습니다. Playwright API 문서에는 다음과 같이 명시되어 있습니다. "wsPath을 알고 있는 모든 프로세스나 웹 페이지(Playwright에서 실행 중인 페이지 포함)는 OS 사용자의 제어권을 탈취할 수 있습니다." 기본 호스트는 localhost이며 "루프백 인터페이스에서만 연결을 허용"합니다. 또한 문서는 0.0.0.0와 같이 명시적인 주소를 전달하면 "수신 대기 중인 포트에 도달할 수 있는 모든 대상에게 브라우저 RPC를 노출한다"고 경고합니다. Chrome 자체의 --remote-debugging-port은 상황이 더 나쁩니다. DevTools 프로토콜은 어떠한 인증도 제공하지 않으며 전적으로 루프백 바인딩에 의존합니다.
실제로 무엇이 공개되어 있는지 확인하십시오. VPS 내부뿐만 아니라 외부의 다른 기기에서도 확인해야 합니다.
ss -ltnp0.0.0.0에 바인딩된 브라우저 포트에서 응답이 온다면 이는 보안 취약점입니다. 대부분의 클라우드 제공업체는 제어판에서 별도의 네트워크 방화벽을 운영하며, 사용자의 ufw 규칙은 이를 인지하지 못한다는 점을 기억하십시오. 대신 SSH 터널이나 사설 VPN을 통해 다른 기기에서 엔드포인트에 접근하십시오.
ssh -N -L 3000:127.0.0.1:3000 you@your-vps여기서 발생하는 위험은 단순히 브라우저 사용 시간을 도용당하는 수준을 넘어섭니다. 제어 가능한 브라우저는 네트워크 내부에서 작동하는 요청 위조 도구와 같습니다. 해당 소켓에 접근할 수 있는 공격자는 http://127.0.0.1:8080, 데이터베이스 관리 페이지 또는 169.254.169.254의 클라우드 메타데이터 주소를 가져오게 한 뒤, 페이지 응답을 읽어낼 수 있습니다. 방화벽은 이 요청을 VPS 자체에서 발생하는 것으로 간주하여 허용합니다. 제어 엔드포인트를 해당 서버의 셸 접근 권한과 동일하게 취급하십시오.
MCP 서버도 동일한 구조를 가집니다. npx @playwright/mcp@latest --headless --port 8931는 localhost에서 HTTP로 서비스를 제공하며, --host 0.0.0.0은 로컬 도구를 공용 도구로 전환하는 플래그입니다. 프로젝트 README에는 Playwright MCP가 "보안 경계가 아님"이 명시되어 있습니다. 포트를 루프백에 유지하고 에이전트가 동일한 터널을 통해 접근하도록 하십시오.
에이전트가 읽는 페이지는 신뢰할 수 없는 입력입니다
웹을 탐색하는 에이전트는 낯선 사람이 작성한 텍스트를 사용자의 지침이 포함된 모델에 전달합니다. 페이지에는 모델에게 작업을 중단하거나, 도구를 호출하거나, 특정 URL로 데이터를 전송하라는 지시가 포함될 수 있습니다. 모델은 두 내용을 모두 텍스트로 받기 때문에 페이지의 단어와 사용자의 지침을 확실하게 구분할 방법이 없습니다. 적대적인 페이지가 악용할 수 있는 여지를 최소화하도록 환경을 설계하십시오.
- 브라우저는 별도의 OS 사용자로 실행하고, 해당 환경에는 SSH 키나 클라우드 자격 증명을 포함하지 마십시오.
- 작업마다 새로운 컨텍스트를 사용하고,
--isolated와 Playwright MCP를 사용하여 한 사이트의 세션이 다음 페이지로 이어지지 않도록 하십시오. - 작업 허용 범위 내에서 출처 허용 목록(origin allowlist)을 유지하십시오. Playwright MCP는
--allowed-origins및--blocked-origins를 세미콜론으로 구분된 목록으로 받습니다. - 메일 발송이나 결제와 같이 상태를 변경하는 모든 작업 전에는 반드시 사람의 확인 단계를 거치도록 하십시오.
더 나은 방법은 브라우저 전체를 폐기하고 재구축할 수 있는 머신에서 실행하는 것입니다. 이는 일회용 VM에서 코딩 에이전트 실행하기와 같은 논리입니다. 에이전트의 실제 작업이 자유로운 브라우징이 아닌 검색이라면, 전체 브라우저보다 더 제한적인 도구가 안전합니다. 직접 구축한 SearXNG 기반 검색 기능은 적대적인 페이지를 직접 로드하지 않고도 결과를 반환합니다.
FAQ
Chromium이 호스트에서는 잘 작동하는데 Docker 컨테이너에서는 왜 충돌합니까?
컨테이너는 기본적으로 64 MB의 /dev/shm을 할당받지만, 호스트는 훨씬 더 큰 용량을 사용하기 때문입니다. Chromium은 렌더링된 콘텐츠를 이 공유 메모리 영역을 통해 전달하므로, 무거운 페이지를 불러오면 메모리가 가득 차서 렌더러가 종료됩니다. 컨테이너 내부에서 df -h /dev/shm을 실행하여 이를 확인하십시오. 이후 호스트의 공유 메모리를 사용하는 --ipc=host 옵션을 추가하거나, 컨테이너 자체의 공유 메모리 크기를 늘리는 --shm-size=1g 옵션을 사용하여 시작하십시오. --disable-dev-shm-usage는 문제를 /tmp로 옮길 뿐입니다.
VPS에서 다른 작업을 하지 않는다면 --no-sandbox는 안전합니까?
아닙니다. 샌드박스는 악성 페이지가 시스템의 나머지 부분에 접근하지 못하도록 막는 역할을 합니다. Chromium 문서에 따르면 이 플래그는 "Chromium의 핵심 보안 기능을 비활성화하므로 공개 웹을 탐색할 때는 절대 사용해서는 안 됩니다". 링크를 따라가는 에이전트 역시 공개 웹을 탐색하는 것과 같습니다. 대신 근본 원인을 해결하십시오. 브라우저를 root 권한으로 실행하지 말고, Ubuntu 24.04에서는 브라우저 바이너리 경로에 userns,을 포함하는 AppArmor 프로필을 추가하여 해당 프로그램에 대해서만 권한 없는 사용자 네임스페이스를 허용하십시오.
작은 VPS에서 브라우저를 몇 개나 실행할 수 있습니까?
숫자를 맹신하지 말고 직접 측정하십시오. Chromium은 사이트마다 하나의 렌더러 프로세스를 시작하므로, 열어두는 페이지에 따라 결과가 달라집니다. MemoryMax을 설정한 상태에서 systemd-run을 사용하여 워커를 하나 실행하고, systemctl status의 Memory: 라인에서 최대 사용량을 확인하십시오. 그 후 가용 RAM을 해당 최대치로 나누고 여유 공간을 확보하십시오. 코드 내에 큐를 구현하고 유닛 파일에 MemoryMax을 설정하여 이 제한을 두 번 적용하십시오. 이렇게 하면 요청이 몰려도 시스템이 스왑(swap)되는 대신 대기하게 됩니다.
에이전트가 다른 머신에서 브라우저로 연결할 수 있습니까?
가능하지만, 포트를 0.0.0.0에 바인딩해서는 절대 안 됩니다. Playwright 서버 엔드포인트와 Chrome DevTools 포트는 비밀번호 없이 접근 가능한 모든 클라이언트를 허용합니다. 리스너는 127.0.0.1에 유지하고 SSH 터널이나 사설 VPN을 통해 연결하십시오. 서버에서 ss -ltnp로 확인하고 외부에서 포트 상태를 점검하십시오. 또한 클라우드 제공업체의 별도 네트워크 방화벽 설정도 확인해야 합니다.
페이지는 정상적으로 로드되었는데 왜 스크린샷이 빈 화면으로 나옵니까?
폰트가 누락되었기 때문입니다. 페이지의 스크립트를 지원하는 폰트가 없으면 텍스트가 빈 상자로 표시되거나 아예 나타나지 않아, 이미지가 적은 페이지는 빈 화면처럼 보입니다. 스크랩하려는 각 언어에 대해 fc-match "sans-serif:lang=ko"를 실행하고, 일반적인 대체 폰트가 필요한 경우 fonts-noto-core 및 fonts-noto-cjk을 설치하십시오. 그 후 브라우저를 재시작하여 fontconfig 캐시를 다시 로드해야 합니다. 폰트가 전혀 없는 컨테이너는 시작 시 Fontconfig error: Cannot load default config file을 로그에 남깁니다.