Skip to content

DanielD2G/BrowserGrid

Repository files navigation

BrowserGrid

Multi-browser grid with automatic fingerprint injection. Manage pools of Camoufox, Chromium, and Firefox instances — each with a unique, realistic browser fingerprint — through a single API.

SDK  ──POST /api/sessions──▶  Grid  ──▶  fpservice (generates fingerprint)
                                │
               ┌────────────────┼────────────────┐
               ▼                ▼                ▼
          Camoufox          Chromium          Firefox
       (C++ env vars)    (context opts +   (context opts +
                          init scripts)     init scripts)
               │                │                │
               └───── ws_endpoint ◀──────────────┘
                          │
SDK  ◀── BrowserContext ◀─┘

Anti-Detection Results

Tested against CreepJS and BrowserScan — the two most rigorous fingerprint detection tools available.

CreepJS

Camoufox Chromium Firefox
Camoufox CreepJS Chromium CreepJS Firefox CreepJS

BrowserScan

Camoufox Chromium Firefox
Camoufox BrowserScan Chromium BrowserScan Firefox BrowserScan

Each browser gets a unique, consistent fingerprint per session. Camoufox provides the deepest spoofing (C++ level), while Chromium and Firefox use Playwright context options and init scripts.

Fingerprint Injection Depth

Browser Method What gets spoofed
Camoufox C++ level via CAMOU_CONFIG_* env vars Canvas, WebGL, fonts, navigator, screen, timezone — native spoofing undetectable by JS
Chromium Playwright context options + JS init scripts userAgent, viewport, locale, timezone, navigator props, screen geometry
Firefox Playwright context options + JS init scripts Same as Chromium

Quick Start

Single Command (no dependencies, no Compose)

Run BrowserGrid as a single container — grid + fpservice + all browsers in one image:

docker run -d --name browsergrid \
  -p 9090:9090 \
  --shm-size=2g \
  ghcr.io/danield2g/browsergrid-standalone:latest

That's it. Dashboard at http://localhost:9090, API at http://localhost:9090/api/health.

With custom options:

docker run -d --name browsergrid \
  -p 9090:9090 \
  --shm-size=2g \
  -e MAX_WORKERS=50 \
  -e IDLE_TIMEOUT=120 \
  -e API_KEY=your-secret-key \
  -v browsergrid-data:/data \
  ghcr.io/danield2g/browsergrid-standalone:latest

One-Line Setup (Docker Compose)

Download the production compose file and start:

curl -fsSL https://raw.githubusercontent.com/DanielD2G/BrowserGrid/main/setup.sh | bash

Or manually:

curl -fsSL https://raw.githubusercontent.com/DanielD2G/BrowserGrid/main/docker-compose.prod.yml -o docker-compose.prod.yml
docker compose -f docker-compose.prod.yml up -d

Build from Source

git clone https://github.com/DanielD2G/BrowserGrid.git
cd BrowserGrid
docker compose up -d

This uses docker/Dockerfile.all which includes Camoufox, Chromium, and Firefox — all precompiled and ready to use.

Single Browser

For smaller images, use a browser-specific Dockerfile:

# docker-compose.yml
services:
  grid:
    build:
      context: .
      dockerfile: docker/Dockerfile.camoufox  # or .chromium, .firefox
docker compose build && docker compose up -d

Verify

curl http://localhost:9090/api/health

Open http://localhost:9090 for the dashboard with live view.

Install the SDK

Python:

pip install ./sdk

TypeScript / JavaScript (Node & Bun):

npm install browsergrid-sdk
# or
bun add browsergrid-sdk

TypeScript SDK

Basic — Connect & Browse

import { BrowserGrid } from "browsergrid-sdk";

const grid = new BrowserGrid("http://localhost:9090");
const { context, close } = await grid
  .configure({ os: "windows", browserType: "chromium" })
  .connect();

const page = await context.newPage();
await page.goto("https://example.com");
await page.screenshot({ path: "screenshot.png" });
await close();

Async Dispose (Node 18+ / Bun)

await using conn = await new BrowserGrid("http://localhost:9090")
  .configure({ os: "windows" })
  .connect();

const page = await conn.context.newPage();
await page.goto("https://example.com");
// automatically closed on scope exit

Manual Playwright Control

const grid = new BrowserGrid("http://localhost:9090");
const session = await grid
  .configure({ browserType: "chromium", os: "windows" })
  .getSession();

console.log(session.wsEndpoint);      // "ws://localhost:9090/ws?session_id=..."
console.log(session.contextOptions);   // { user_agent: "...", viewport: {...} }
console.log(session.initScripts);      // ["Object.defineProperty(navigator, ...)"]

// Connect manually with playwright-core
import { chromium } from "playwright-core";
const browser = await chromium.connect(session.wsEndpoint);
const context = await browser.newContext(session.contextOptions);
for (const script of session.initScripts) {
  await context.addInitScript(script);
}
const page = await context.newPage();
await page.goto("https://example.com");
await context.close();
await browser.close();
await grid.kill();

WebSocket-Only

const ws = await new BrowserGrid("http://localhost:9090")
  .configure({ os: "windows" })
  .getWs();
// "ws://localhost:9090/ws?session_id=abc-123"

Downloads

const grid = new BrowserGrid("http://localhost:9090");
const { context, close } = await grid
  .configure({ browserType: "camoufox" })
  .connect();

const page = await context.newPage();
await page.goto("https://example.com/export");
// ... trigger a download ...

const files = await grid.listDownloads();
await grid.saveDownload(files[0].name, "./downloads/");
await close();

Authentication

const grid = new BrowserGrid("http://localhost:9090", "your-secret-key");

Persistent Contexts

// Session 1: login and accumulate cookies
{
  await using conn = await new BrowserGrid("http://localhost:9090")
    .configure({ contextKey: "my-user", browserType: "chromium", os: "windows" })
    .connect();
  const page = await conn.context.newPage();
  await page.goto("https://example.com/login");
  // cookies saved automatically on close
}

// Session 2: same fingerprint, cookies restored
{
  await using conn = await new BrowserGrid("http://localhost:9090")
    .configure({ contextKey: "my-user", browserType: "chromium" })
    .connect();
  const page = await conn.context.newPage();
  await page.goto("https://example.com/dashboard");
  // already logged in
}

All Configuration Options

import { BrowserGrid } from "browsergrid-sdk";

const grid = new BrowserGrid("http://localhost:9090");
grid.configure({
  browserType: "camoufox",   // "camoufox" | "chromium" | "firefox"
  os: "windows",             // "windows" | "macos" | "linux"
  headless: false,
  proxy: "http://proxy:8080",
  geoip: true,               // auto-set timezone/locale from IP
  locale: "en-US",
  blockImages: false,
  blockWebrtc: true,
  humanize: true,
  contextKey: "my-user",     // reuse fingerprint + cookies
  wait: true,                // wait for capacity (default: true)
  waitTimeout: 300,          // seconds to wait (default: 300)
});

Python SDK

Basic — Context Manager

from browser_grid import BrowserGrid

# Camoufox (default) — deepest fingerprint spoofing
with BrowserGrid("http://localhost:9090").configure(os="windows") as ctx:
    page = ctx.new_page()
    page.goto("https://example.com")
    page.screenshot(path="screenshot.png")

# Chromium
with BrowserGrid("http://localhost:9090").configure(browser_type="chromium", os="macos") as ctx:
    page = ctx.new_page()
    page.goto("https://example.com")

# Firefox (Playwright's Firefox, not Camoufox)
with BrowserGrid("http://localhost:9090").configure(browser_type="firefox", os="linux") as ctx:
    page = ctx.new_page()
    page.goto("https://example.com")

The context manager returns a Playwright BrowserContext with the fingerprint already injected. Create pages, navigate, take screenshots — standard Playwright API.

Async

from browser_grid import AsyncBrowserGrid

async with AsyncBrowserGrid("http://localhost:9090").configure(browser_type="chromium", os="windows") as ctx:
    page = await ctx.new_page()
    await page.goto("https://example.com")

Manual Playwright Control

Use get_session() when you need direct access to the connection details:

from browser_grid import BrowserGrid
from playwright.sync_api import sync_playwright

grid = BrowserGrid("http://localhost:9090")
session = grid.configure(browser_type="chromium", os="windows").get_session()

print(session.browser_type)      # "chromium"
print(session.ws_endpoint)       # "ws://localhost:9090/ws?session_id=..."
print(session.context_options)   # {"user_agent": "...", "viewport": {...}, ...}
print(session.init_scripts)      # ["Object.defineProperty(navigator, ...)"]

# Connect manually
with sync_playwright() as pw:
    browser = pw.chromium.connect(session.ws_endpoint)
    context = browser.new_context(**session.context_options)
    for script in session.init_scripts:
        context.add_init_script(script)
    page = context.new_page()
    page.goto("https://example.com")
    context.close()
    browser.close()

grid.kill()  # destroy the session

WebSocket-Only

For integrations that just need the raw WebSocket URL:

ws = BrowserGrid("http://localhost:9090").configure(os="windows").get_ws()
# "ws://localhost:9090/ws?session_id=abc-123"

All Configuration Options

from browser_grid import BrowserGrid, SessionConfig

config = SessionConfig(
    browser_type="camoufox",   # "camoufox" | "chromium" | "firefox"
    os="windows",              # "windows" | "macos" | "linux"
    headless=False,            # True for headless mode
    proxy_server="http://proxy:8080",
    proxy_username="user",
    proxy_password="pass",
    geoip=True,                # auto-set timezone/locale from IP geolocation
    locale="en-US",
    block_images=False,
    block_webrtc=True,
    humanize=True,             # human-like mouse/keyboard timing
)

with BrowserGrid("http://localhost:9090").configure(config) as ctx:
    page = ctx.new_page()
    page.goto("https://example.com")

Authentication

# Start grid with API key
docker compose up -d  # set API_KEY env var
grid = BrowserGrid("http://localhost:9090", api_key="your-secret-key")

Persistent Contexts

Use context_key to reuse the same fingerprint and cookies across sessions. The grid saves the fingerprint on first use, and the SDK automatically persists storage state (cookies, localStorage) on exit.

# Session 1: browse and accumulate cookies
with BrowserGrid("http://localhost:9090").configure(
    context_key="my-user", browser_type="chromium", os="windows"
) as ctx:
    page = ctx.new_page()
    page.goto("https://example.com/login")
    # ... login, cookies are saved automatically on exit

# Session 2: same fingerprint, cookies restored
with BrowserGrid("http://localhost:9090").configure(
    context_key="my-user", browser_type="chromium"
) as ctx:
    page = ctx.new_page()
    page.goto("https://example.com/dashboard")
    # Already logged in — cookies were restored

Delete a context to reset the identity:

curl -X DELETE http://localhost:9090/api/contexts/my-user

REST API

All endpoints accept an optional Authorization: Bearer <api_key> header.

Create Session

curl -X POST http://localhost:9090/api/sessions \
  -H "Content-Type: application/json" \
  -d '{"browser_type": "chromium", "os": "windows"}'

Response:

{
  "id": "abc-123",
  "ws_endpoint": "ws://localhost:9090/ws?session_id=abc-123",
  "browser_type": "chromium",
  "context_options": {
    "user_agent": "Mozilla/5.0 ... Chrome/145.0.0.0 ...",
    "viewport": {"width": 1920, "height": 1080}
  },
  "init_scripts": ["Object.defineProperty(navigator, 'hardwareConcurrency', {get: () => 8});..."],
  "created_at": "2026-03-01T06:43:25Z"
}

WebSocket Connect

GET /ws?session_id=abc-123        # reconnect to existing session
GET /ws?os=windows&browser_type=chromium  # create + connect in one step

Per-Session Downloads

Each session has a dedicated download directory. Files downloaded by the browser are accessible via the API while the session is active.

List downloads:

curl http://localhost:9090/api/sessions/{id}/downloads
{
  "session_id": "abc-123",
  "files": [{"name": "report.pdf", "size": 245760, "modified": "2026-03-02T21:30:00Z"}],
  "count": 1
}

Download a file:

curl -O http://localhost:9090/api/sessions/{id}/downloads/report.pdf

Returns the file with Content-Disposition: attachment and auto-detected content type. Path traversal attempts are blocked.

SDK usage:

from browser_grid import BrowserGrid

grid = BrowserGrid("http://localhost:9090")

with grid.configure(browser_type="camoufox") as ctx:
    page = ctx.new_page()
    page.goto("https://example.com/export")
    # ... trigger a download in the browser ...

    # List files downloaded by this session
    files = grid.list_downloads()
    print(files)  # [{"name": "report.pdf", "size": 245760, "modified": "..."}]

    # Get file contents as bytes
    data = grid.get_download("report.pdf")

    # Save directly to disk
    grid.save_download("report.pdf", "./downloads/")

Async variant:

from browser_grid import AsyncBrowserGrid

grid = AsyncBrowserGrid("http://localhost:9090")

async with grid.configure(browser_type="camoufox") as ctx:
    # ... trigger download ...
    files = await grid.list_downloads()
    await grid.save_download(files[0]["name"], "./downloads/")

Other Endpoints

Method Path Description
GET /api/sessions List all active sessions
GET /api/sessions/{id} Get session details
DELETE /api/sessions/{id} Destroy a session
GET /api/sessions/{id}/downloads List downloaded files
GET /api/sessions/{id}/downloads/{filename} Download a file
GET /api/contexts/{key} Get context info (exists, has_fingerprint, has_state)
PUT /api/contexts/{key}/state Save storage state for a context
DELETE /api/contexts/{key} Delete a persistent context
GET /api/health Health check (includes fpservice status)
GET /api/stats Pool stats (active, max, available)

Live View

The dashboard at http://localhost:9090 includes a real-time live view of any active session. Each browser runs on its own Xvfb display, and the dashboard streams JPEG frames over WebSocket at up to 10 FPS.

Features:

  • Screen streaming — watch what the browser is rendering in real time
  • Mouse & keyboard input — click, type, and scroll directly in the live view
  • Navigation controls — URL bar, back/forward, reload from the dashboard
  • Session list — see all active sessions with status, browser type, and duration

Click the eye icon next to any session in the dashboard to open its live view. Disable in production with LIVE_VIEW=false.

Docker Images

Four Dockerfile variants in docker/. All use a single-layer install to minimize image size (no duplicate binaries across layers).

Dockerfile Browsers Use case
Dockerfile.standalone All + fpservice embedded Single docker run, no Compose needed
Dockerfile.all Camoufox + Chromium + Firefox Full flexibility (default, ~3.4 GB)
Dockerfile.camoufox Camoufox only Smallest image, deepest spoofing
Dockerfile.chromium Chromium only Chrome fingerprints only
Dockerfile.firefox Firefox only Firefox fingerprints only

Switch image in docker-compose.yml:

grid:
  build:
    context: .
    dockerfile: docker/Dockerfile.camoufox  # or .chromium, .firefox, .all

Configuration

Environment Variables

Variable Default Description
MAX_WORKERS 10 Maximum concurrent browser instances
IDLE_TIMEOUT 300 Seconds before idle sessions are killed
FP_SERVICE_URL http://fpservice:8484 Fingerprint service URL
API_KEY (empty) API authentication key (empty = no auth)
CAMOUFOX_PATH /opt/camoufox/camoufox-bin Camoufox binary path
CHROMIUM_PATH (auto-detect) Chromium binary path
FIREFOX_PATH (auto-detect) Firefox binary path
CONTEXT_STORE_DIR /data/contexts Directory for persistent context storage
CHROMIUM_ARGS (defaults) Comma-separated Chromium flags (see below). none = no extra flags
HEADLESS false Run browsers headless
LIVE_VIEW true Enable live view screen streaming (disable in production)

CLI Flags

browser-grid \
  --host 0.0.0.0 \
  --port 9090 \
  --max-workers 20 \
  --idle-timeout 600 \
  --camoufox-path /opt/camoufox/camoufox-bin \
  --chromium-path /usr/bin/chromium \
  --firefox-path /usr/bin/firefox \
  --headless

Chromium Launch Flags

By default, Chromium launches with flags that disable unused features inside Docker/Xvfb:

Flag What it disables
--disable-dev-shm-usage Uses /tmp instead of /dev/shm (avoids 64 MB Docker limit)
--disable-background-networking Background network requests (update checks, etc.)
--disable-breakpad Crash reporter / dump collection
--disable-component-update Component auto-updater
--disable-extensions Extension subsystem
--disable-sync Chrome sync service
--disable-translate Built-in translation service
--metrics-recording-only Disables metrics reporting (keeps internal recording)
--no-first-run First-run setup tasks and welcome page

Override via CHROMIUM_ARGS (comma-separated):

environment:
  # Custom flags
  - CHROMIUM_ARGS=--disable-dev-shm-usage,--disable-extensions,--disable-gpu
  # Or disable all extra flags
  - CHROMIUM_ARGS=none
  # Unset = use defaults above

Performance & Scaling

RAM per Browser Instance

Measured with 3 concurrent instances per browser on a clean container restart (Dockerfile.all):

Browser RAM per Instance Notes
Camoufox ~194 MiB Lightest — C++ engine, no JS overhead
Chromium ~255 MiB
Firefox ~256 MiB Playwright's Firefox (not Camoufox)

Container baseline (no browsers): ~17 MiB.

Startup Speed by Browser

Browser Avg Startup Notes
Chromium ~1.3s Fastest
Firefox ~1.6s
Camoufox ~2.1s Loads C++ fingerprint config

Docker Image Size

Image Size
Dockerfile.all (Camoufox + Chromium + Firefox) ~3.4 GB

Scaling Recommendations

Server RAM Recommended MAX_WORKERS shm_size
4 GB 10-15 1g
8 GB 20-30 2g
16 GB 40-60 4g
32 GB 80-120 8g

Example high-concurrency config:

services:
  grid:
    environment:
      - MAX_WORKERS=50
      - IDLE_TIMEOUT=120
    shm_size: "4g"
    deploy:
      resources:
        limits:
          memory: 12g

Testing

Reliability Audit (Concurrent Browser Tests)

Tests all three browsers against anti-detection sites at increasing concurrency:

python3 -u tests/test_reliability_audit.py

Runs 4 phases:

  1. Sequential — 1 session per browser (3 total)
  2. 3 concurrent — one per browser in parallel
  3. 10 concurrent — mixed browsers
  4. 20 concurrent — stress test

Each session navigates to CreepJS, Browserleaks, and BrowserScan, takes full-page screenshots, and reports timing. Results and screenshots are saved to tests/reliability_audit/.

Long Stress Test (30-60 min)

Simulates sustained production traffic with configurable concurrency:

python3 -u tests/long_stress_test.py --duration 45 --target 15

Features:

  • Continuous session rotation — sessions open/close in waves to maintain target concurrency
  • Mixed close modes — 75% clean close, 15% kill via API, 10% abandon (tests orphan cleanup)
  • Multi-site browsing — each session visits 2-4 sites from 6 categories (news, tech, shopping, social, anti-detect, general)
  • Multiple tabs — 40% of sessions open extra tabs
  • Server-side concurrency tracking — checks /api/stats to prevent session accumulation from abandoned sessions
  • 10-minute insight snapshots — periodic reports with pool stats, CPU, memory, health status
  • Reusable — saves full JSON report to tests/long_stress/report.json

Options:

Flag Default Description
--duration 45 Test duration in minutes
--target 15 Target concurrent sessions
--output tests/long_stress Output directory

Validated result: 732 sessions over 45 min, 0 failures, stable memory (~5-7 GB under load).

Architecture

┌──────────────────────────────────────────────────────────┐
│  Docker Compose                                          │
│                                                          │
│  ┌─────────────┐    ┌──────────────────────────────────┐ │
│  │  fpservice   │    │  grid                            │ │
│  │  (Python)    │◀───│  (Go binary)                     │ │
│  │              │    │                                  │ │
│  │  browserforge│    │  ┌──────────┐  ┌──────────────┐ │ │
│  │  webgl db    │    │  │  Worker   │  │  Worker Pool │ │ │
│  │  fonts.json  │    │  │  Pool     │  │  Health Loop │ │ │
│  │              │    │  ├──────────┤  └──────────────┘ │ │
│  └─────────────┘    │  │ Worker 1 │ ← node launchServer│ │
│                      │  │ Worker 2 │   .js → Playwright │ │
│                      │  │ Worker N │   Browser Servers  │ │
│                      │  └──────────┘                    │ │
│                      │       ▲ WebSocket proxy          │ │
│                      │       │                          │ │
│                      │  ┌────┴──────┐                   │ │
│                      │  │ HTTP/WS   │ :9090             │ │
│                      │  │ Server    │                   │ │
│                      │  └───────────┘                   │ │
│                      └──────────────────────────────────┘ │
└──────────────────────────────────────────────────────────┘
         ▲
         │  SDK (Python / TypeScript) / REST API / WebSocket
         │
    ┌────┴────┐
    │  Client  │
    └─────────┘

Request Flow

  1. Client calls POST /api/sessions with config
  2. Grid calls fpservice to generate a browser-appropriate fingerprint
  3. Grid spawns a Playwright browser server via node launchServer.js
  4. For Camoufox: fingerprint injected at C++ level via env vars
  5. For Chromium/Firefox: fingerprint returned as context options + JS init scripts
  6. Client connects via WebSocket, SDK applies context options and scripts
  7. Client gets a BrowserContext with the fingerprint baked in

File Structure

BrowserGrid/
├── grid/                          # Go server
│   ├── cmd/browser-grid/          # Binary entrypoint
│   ├── internal/
│   │   ├── context/               # Persistent context storage (fingerprint + state)
│   │   ├── fingerprint/           # FP generation + Playwright options builder
│   │   ├── screencast/            # Live view (X11 capture, input dispatch)
│   │   ├── server/                # HTTP/WS server + routes + dashboard
│   │   ├── session/               # Session lifecycle
│   │   ├── worker/                # Browser process management + pool
│   │   └── proxy/                 # WebSocket bidirectional proxy
│   ├── go.mod
│   └── go.sum
├── fpservice/                     # Python fingerprint microservice
│   ├── main.py                    # Flask app (browserforge + WebGL + fonts)
│   ├── data/
│   │   ├── browserforge.yml       # Fingerprint field mapping
│   │   └── fonts.json             # OS-specific font lists
│   ├── requirements.txt
│   └── Dockerfile
├── sdk/                           # Client SDKs
│   ├── typescript/                # TypeScript/JS SDK (npm: browsergrid-sdk)
│   └── browser_grid/             # Python SDK
│       ├── __init__.py
│       ├── client.py              # BrowserGrid / AsyncBrowserGrid
│       └── types.py               # SessionConfig / SessionResult
├── docker/
│   ├── Dockerfile.all             # All browsers
│   ├── Dockerfile.camoufox       # Camoufox only
│   ├── Dockerfile.chromium       # Chromium only
│   └── Dockerfile.firefox        # Firefox only
├── launchServer.js                # Node.js bridge to Playwright's launchServer
├── docker-compose.yml
├── entrypoint.sh                  # Xvfb + grid startup
└── tests/
    ├── test_browsers.py           # Screenshot tests (CreepJS, Browserleaks, BrowserScan)
    ├── test_integration.py        # Session creation, capacity limits, idle cleanup
    ├── test_persistent_context.py # Context persistence (fingerprint + storage state)
    ├── test_reliability_audit.py  # 4-phase concurrent browser reliability audit
    ├── test_sdk_retry.py          # SDK retry logic
    ├── test_downloads.py          # Per-session download retrieval
    ├── test_stale_cleanup.py      # Idle session cleanup
    ├── long_stress_test.py        # 30-60 min stress test with mixed close modes
    └── stress_test.py             # Concurrent session benchmarks

Backward Compatibility

The SDK provides aliases for the original CamoufoxGrid and StealthGrid names:

from browser_grid import CamoufoxGrid  # alias for BrowserGrid
from browser_grid import StealthGrid   # alias for BrowserGrid

The get_ws() method still works for WebSocket-only usage.

About

Multi-browser automation grid with anti-detection fingerprinting, live view, and per-session download retrieval

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors