Patchright Docker
Stealth browser in Docker. Patchright (undetected Playwright fork) + xvfb. Bypasses Cloudflare, bot detection, and headless browser fingerprinting without opening a Chrome window.
When to Use
webfetchorcurlreturns 403, Cloudflare challenge, or "Access Denied"- A site requires JavaScript rendering that webfetch can't handle
- You need to interact with a page (click, fill, scroll) before extracting data
- You need a screenshot of a protected page
When NOT to Use
- URL works fine with
webfetchorcurl(use those first, they're faster) - Complex multi-step automation with login, 2FA, or session management (use dedicated scripts like SW or AA)
- You need to run many requests quickly (each invocation starts a fresh browser, ~10-15s overhead)
Prerequisites
docker pull ghcr.io/borski/patchright-docker
Fetch Mode (Default)
Pass a URL and get back page content. Stealth curl replacement.
# Basic fetch (returns body text)
docker run --rm ghcr.io/borski/patchright-docker "https://example.com"
# Extract specific element(s)
docker run --rm ghcr.io/borski/patchright-docker "https://example.com" --selector "table"
docker run --rm ghcr.io/borski/patchright-docker "https://example.com" --selector "h1" --selector ".content"
# Get HTML instead of text
docker run --rm ghcr.io/borski/patchright-docker "https://example.com" --selector "table" --html
# JSON output (title, url, content)
docker run --rm ghcr.io/borski/patchright-docker "https://example.com" --json
# Execute JavaScript
docker run --rm ghcr.io/borski/patchright-docker "https://example.com" --js "document.title"
Cloudflare Bypass
Use --warmup to visit the site's homepage first. This establishes cookies and passes Cloudflare challenges before navigating to the protected page.
docker run --rm ghcr.io/borski/patchright-docker \
"https://protected-site.com/data" \
--warmup "https://protected-site.com"
Interactions Before Extraction
Click buttons, fill forms, then extract the result.
# Click a button, wait, extract
docker run --rm ghcr.io/borski/patchright-docker "https://example.com" \
--click "#load-more" --wait 3000 --selector ".results"
# Fill a search form and submit
docker run --rm ghcr.io/borski/patchright-docker "https://example.com" \
--fill "#search" "query text" --click "#submit" --wait 3000 --selector ".results"
Screenshots
# Viewport screenshot
docker run --rm -v ./output:/output ghcr.io/borski/patchright-docker \
"https://example.com" --screenshot /output/page.png
# Full page screenshot
docker run --rm -v ./output:/output ghcr.io/borski/patchright-docker \
"https://example.com" --screenshot /output/page.png --full-page
Script Mode
Mount any Python script for complex automation. The container has Patchright and Chromium pre-installed. xvfb is already running.
docker run --rm -v ./my_script.py:/scripts/my_script.py \
ghcr.io/borski/patchright-docker script /scripts/my_script.py [args...]
Your script can use the full Patchright API:
from patchright.sync_api import sync_playwright
import tempfile
with sync_playwright() as p:
ctx = p.chromium.launch_persistent_context(
user_data_dir=tempfile.mkdtemp(),
headless=False, # xvfb handles display
viewport={"width": 1440, "height": 900},
)
page = ctx.pages[0] if ctx.pages else ctx.new_page()
page.goto("https://example.com")
print(page.title())
ctx.close()
All Options
| Option | Description |
|---|---|
--selector SEL |
CSS selector to extract. Repeatable. Default: body |
--html |
Return inner HTML instead of text |
--json |
JSON output: {title, url, selectors, content} |
--screenshot PATH |
Save screenshot (mount volume for output) |
--full-page |
Full page screenshot |
--wait MS |
Wait after load, milliseconds. Default: 5000 |
--timeout MS |
Navigation timeout. Default: 30000 |
--warmup URL |
Visit URL first to establish session (Cloudflare bypass) |
--js EXPR |
Execute JavaScript, return result |
--click SEL |
Click selector before extracting. Repeatable. |
--fill SEL VALUE |
Fill input before extracting. Repeatable. |
--user-agent STR |
Override user agent |
Common Patterns
Cloudflare-protected table
docker run --rm ghcr.io/borski/patchright-docker \
"https://seats.aero/tools/releases?source=aeroplan" \
--warmup "https://seats.aero" \
--selector "table" 2>/dev/null
JSON + jq pipeline
docker run --rm ghcr.io/borski/patchright-docker \
"https://example.com" --json 2>/dev/null | jq -r '.content'
Multiple selectors combined
docker run --rm ghcr.io/borski/patchright-docker \
"https://example.com" \
--selector "h1" --selector "article" --json 2>/dev/null
Performance
- ~10-15 seconds per invocation (browser startup + page load + Cloudflare challenge if present)
- Each run creates a fresh browser context (no cookie persistence between runs)
- Redirect stderr to
/dev/nullto suppress Chromium/xvfb noise:2>/dev/null - For repeated access to the same site, a custom script with session reuse will be faster
Troubleshooting
- "Cloudflare challenge did not resolve": Some sites have aggressive bot detection that even Patchright can't bypass. Try adding
--wait 10000for more time, or--user-agentwith a realistic string. - Timeout: Increase
--timeout 60000for slow pages. - Empty output: The selector might not exist. Try without
--selectorfirst to see the full page text, then narrow down. - stderr noise: Chromium and xvfb print warnings to stderr. Pipe
2>/dev/nullfor clean output.