# Patchright Docker

> Stealth browser in Docker via Patchright (undetected Playwright). Bypasses Cloudflare, bot detection, and headless fingerprinting. No Chrome popup. Use when curl/webfetch gets blocked (403, Cloudflare challenge, "Access Denied") or when you need to interact with a page that requires a real browser. Triggers on "Cloudflare blocked", "403 from curl", "stealth fetch", "scrape protected page", "patchright", or any URL that returns a Cloudflare challenge.

- Skill: `borski/patchright-docker` (Agent Skill)
- Install (CLI): `npx skillmds@latest add borski/patchright-docker`
- Raw SKILL.md: https://api.skillmd.com/api/skills/borski/patchright-docker/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: DevOps & Infra
- Author: borski (https://skillmd.com/u/borski)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/borski/patchright-docker

---


# 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

- `webfetch` or `curl` returns 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 `webfetch` or `curl` (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

```bash
docker pull ghcr.io/borski/patchright-docker
```

## Fetch Mode (Default)

Pass a URL and get back page content. Stealth curl replacement.

```bash
# 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.

```bash
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.

```bash
# 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

```bash
# 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.

```bash
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:

```python
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

```bash
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

```bash
docker run --rm ghcr.io/borski/patchright-docker \
  "https://example.com" --json 2>/dev/null | jq -r '.content'
```

### Multiple selectors combined

```bash
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/null` to 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 10000` for more time, or `--user-agent` with a realistic string.
- **Timeout**: Increase `--timeout 60000` for slow pages.
- **Empty output**: The selector might not exist. Try without `--selector` first to see the full page text, then narrow down.
- **stderr noise**: Chromium and xvfb print warnings to stderr. Pipe `2>/dev/null` for clean output.

