dieter-medium/bidi2pdf/remote-chrome

Claude Code and agent skill by dieter-medium/bidi2pdf

Trust tier 3
  • community
Source check pending

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-chrome

SKILL.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.hint and 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-pdf skill's

preflight); run nothing that changes the system without the user's yes.

  • bidi2pdf's results first. Read its --json result 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:

  1. 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.

  1. 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.

  1. No extra capability, and every client passes --no-sandbox (DISABLE_CHROME_SANDBOX=true for 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

bash
bidi2pdf render --url http://app:3000/report --output report.pdf \
--remote-browser-url http://remote-chrome:3000/session

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:

bash
bidi2pdf sessions --remote-browser-url http://remote-chrome:3000/session
bidi2pdf sweep --remote-browser-url http://remote-chrome:3000/session --dry-run
bidi2pdf sweep --remote-browser-url http://remote-chrome:3000/session

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

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
vskill VERIFIED badge
[![vskill VERIFIED](https://verified-skill.com/api/v1/skills/dieter-medium/bidi2pdf/remote-chrome/badge)](https://verified-skill.com/skills/dieter-medium/bidi2pdf/remote-chrome)

Something wrong with this skill or its page? Tell us.