dieter-medium/bidi2pdf/remote-chrome
Claude Code and agent skill by dieter-medium/bidi2pdf
- community
Checking that the SKILL.md file is still in the repository.
This skill should be used when bidi2pdf (or bidi2pdf-rails) should render in a shared Chrome container instead of a local browser - setting up the chromedriver container with Docker Compose, connecting to it, sizing it, keeping sessions from piling up, and printing HTML that nobody vouches for. Also when renders fail with "session not created", hang, or the container eats memory.
Install
npx vskill@latest install dieter-medium/bidi2pdf/remote-chromeSKILL.md
Run a shared Chrome for bidi2pdf
bidi2pdf talks to chromedriver over HTTP and WebDriver BiDi. A chromedriver container that several processes share is cheaper and more predictable than a browser per process, and keeps Chrome out of the application's own container.
Does not: cover Kubernetes or a cloud browser service, and does not make an untrusted page safe on its own (section 5 is what it takes).
Ground rules
- bidi2pdf is the renderer. If it fails, report its
error.code,error.hintand exit code;
never make the PDF or screenshot another way (Chrome's own print-to-PDF, another PDF engine).
- Install nothing unasked. Name what is missing and suggest a way (the
render-pdfskill's
preflight); run nothing that changes the system without the user's yes.
- bidi2pdf's results first. Read its
--jsonresult instead of inspecting the PDF with other
tools.
1. The container
references/compose.remote-chrome.yml is a working Compose service, taken from an application that runs it in development and production. The parts that matter, and why:
- Image
dieters877565/chromedriver(chromedriver + Chromium, hyphenation dictionaries
included). It answers on port 3000; the new-session URL is http://<service>:3000/session.
- No published port, an internal network only. Whoever reaches port 3000 drives a browser: keep
it to the containers that render. No VNC (ENABLE_VNC: "false").
- Chrome's sandbox needs to create namespaces, which Docker's default seccomp profile only allows
with CAP_SYS_ADMIN. Three ways, from the narrowest:
- The gem's seccomp profile
docker/chrome-driver/chrome.json(`security_opt:
["seccomp=chrome.json"]), no extra capability. It allows the namespace calls Chrome uses, but also some Docker's default refuses (mount, bpf, ptrace, perf_event_open`); verify on your host that sessions start before relying on it.
cap_add: [SYS_ADMIN]- works with the default profile, and is what the reference file uses. It
is a broad capability: keep the container on its internal network and give it nothing else.
- No extra capability, and every client passes
--no-sandbox(DISABLE_CHROME_SANDBOX=truefor the
CLI). Chrome then runs every page without its own sandbox - only for pages you trust. Some CI runners drop --cap-add silently; there only this works.
- Limits from measurement, not guesses: one Chrome session runs ~110 threads (Docker counts them
as pids) and a few hundred MB. pids_limit: 1024, mem_limit: 2g with memswap_limit equal (no swap: a Chrome out of memory is killed and replaced instead of freezing every render), shm_size: 256m. At 512 pids and 1 GB, four concurrent renders crashed or froze it.
- `WATCHDOG_ENABLED: "true"` stops chromedriver when it stops answering or memory thrashes;
restart: unless-stopped then starts a fresh container. The image's health check (/status) only marks it unhealthy - Docker never restarts an unhealthy container by itself.
- `LANG`/`LANGUAGE` set the browser's locale: date and number formats in form fields follow it.
In CI, wait for the container to be healthy before the first render; "Connection refused" on port 3000 right after start means it was not ready yet.
2. Connect
The page URL must be reachable from the browser container, not from where the command runs: localhost there is the browser itself. Use the application's service name, and for a Rails app make the PDF's asset host that same name, or every stylesheet fails and the PDF is blank.
3. Keep sessions from piling up
A process that crashes or is killed mid-render leaves its Chrome session open on the shared chromedriver, and each one holds memory. bidi2pdf records the sessions it opens on this machine (SessionRegistry); look and clean up with:
sweep closes recorded sessions older than --older-than (600 s), unresponsive ones, and the oldest above --max-sessions; never one younger than --min-age or one a running process holds. Leave --scope all to a chromedriver your application owns alone - it also closes sessions other programs opened. Run sweep from cron or a timer, and --dry-run first.
Code that opens its own sessions (a test driver, a script) passes a SessionRegistry to Bidi2pdf::Bidi::Session.new(..., registry: Bidi2pdf::SessionRegistry.new(url)) and closes the session in an ensure, with a time limit on the close: a hung close is the most common leak.
4. Size it
Renders run in parallel on one chromedriver, one session (or a tab of one) each. Measure your own pages: start with 2 GB, run the peak number of renders at once, and watch docker stats for memory and pids. Raise mem_limit and pids_limit together.
5. Printing HTML nobody vouches for
A page from a user is a page that runs its scripts in your browser. Before navigating:
tab.disable_script_execution- the page's JavaScript does not run at all.- Give the browser container no direct internet access: route it through an egress proxy that
refuses private and internal addresses, so a page cannot make the browser fetch your internal services. Never put it on the same network as your database.
- Prefer printing HTML you built (
render_html_content,--html-file) over handing Chrome a URL a
user typed.
Before production, test the isolation with hostile pages, not by reading the configuration. Render pages that try, each on its own: to fetch an internal service and your cloud metadata address, to load file:///etc/passwd and other file: URLs, to open new windows, to run scripts despite disable_script_execution, and to exhaust memory. Each must fail or stay contained, and the chromedriver must keep serving other renders. Repeat it after changing the image, the capabilities or the seccomp profile.
Security
- Security scanPassed, 90 of 100Pattern checks on the skill's files.
- Model reviewPassed
Quality evaluation
- Task evaluationNot runNo evaluation yet. Run one in Skill Studio.
Source
- SKILL.md in the repositoryCheckingclaude-plugin/skills/remote-chrome/SKILL.md
Popularity
- Repository stars
- 58
- 7-day trend score
- 0
Stars and forks are for the whole GitHub repository, not this skill alone. Not refreshed yet.
Works with
Any agent vskill installs to: 56 targets in all. These 39 have a page on this site.
- Claude Code
- Cursor
- GitHub Copilot
- Windsurf
- Codex
- Gemini CLI
- Cline
- Amp
- Roo Code
- Goose
- Continue
- Kilo Code
- Junie
- Trae
- OpenHands
- Qwen Code
- AI Agent
- Tabnine Chat
- Sourcegraph Cody
- Amazon Q
- Firebase Genkit
- Sweep
- Devin
- Phind
- Mentat
- Claude Desktop
- Aider
- GPT Pilot
- Plandex
- bolt.new
- v0
- Replit Agent
- Anse
- TypingMind
- BetterChatGPT
- LibreChat
- Warp AI
- Pieces
- Fig AI
Embed badge
[](https://verified-skill.com/skills/dieter-medium/bidi2pdf/remote-chrome)